diff --git a/docs/builtin-props.md b/docs/builtin-props.md index 6cfdf242..2dec6dbf 100644 --- a/docs/builtin-props.md +++ b/docs/builtin-props.md @@ -23,6 +23,8 @@ Core:range(0,2).push(4) //[0,1,2,4] ### @(_x_: num).to_str(): str 数値を文字列に変換します。 +### @(_x_: num).to_hex(): str +数値から16進数の文字列を生成します。 ## 文字列 ### #(_v_: str).len @@ -198,6 +200,7 @@ _fromIndex_が負値の時は末尾からの位置(配列の長さ+_fromIndex_ ### @(_v_: arr).sort(_comp_: @(_a_: value, _b_: value)): arr **【この操作は配列を書き換えます】** 配列の並べ替えをします。第1引数 _comp_ として次のような比較関数を渡します。 +安定ソートです。 * _a_ が _b_ より順番的に前の時、負の値を返す * _a_ が _b_ より順番的に後の時、正の値を返す * _a_ が _b_ と順番的に同等の時、0を返す diff --git a/docs/get-started.md b/docs/get-started.md index f9fde75d..77f8a467 100644 --- a/docs/get-started.md +++ b/docs/get-started.md @@ -22,7 +22,7 @@ print("Hello, world!") `"~"`は文字列リテラルです。`"`で囲ったものが文字列になります。 ちなみに、`print( ~ )`には糖衣構文があり、次のようにも書けます: -``` +```js <: "Hello, world!" ``` @@ -86,13 +86,13 @@ print(message) ## 配列 `[]`の中に式をスペースで区切って列挙します。 ``` -["ai" "chan" "kawaii"] +["ai", "chan", "kawaii"] ``` 配列の要素にアクセスするときは、`[]`と書きます。 インデックスは0始まりです。 ``` -let arr = ["ai" "chan" "kawaii"] +let arr = ["ai", "chan", "kawaii"] <: arr[0] // "ai" <: arr[2] // "kawaii" ``` @@ -130,21 +130,7 @@ let obj = {foo: "bar", answer: 42} ``` Core:add(1, 1) ``` - - - - - - - - - - - - - -
演算子標準関数意味
+Core:add加算
-Core:sub減算
*Core:mul乗算
^Core:pow冪算
/Core:div除算
%Core:mod剰余
==Core:eq等しい
&&Core:andかつ
||Core:orまたは
>Core:gt大きい
<Core:lt小さい
+詳しくは→[syntax.md](/docs/syntax.md#%E6%BC%94%E7%AE%97%E5%AD%90) ## ブロック式 ブロック式 `eval { ~ }` を使うと、ブロック内で最後に書かれた式が値として返されます。 @@ -216,7 +202,7 @@ for (100) { ## 繰り返し(配列) `each`を使うと、配列の各アイテムに対し処理を繰り返すことができます: ``` -let items = ["a" "b" "c"] +let items = ["a", "b", "c"] each (let item, items) { <: item } @@ -260,13 +246,13 @@ AiScriptファイルにメタデータを埋め込める機能です。 ### { name: "example" version: 42 - keywords: ["foo" "bar" "baz"] + keywords: ["foo", "bar", "baz"] } ``` ## エラー型 -一部の標準関数は実行失敗時にエラー型の値を返します。 -これによりエラー処理を行うことができます。 +一部の標準関数は実行失敗時にエラー型の値を返します。 +これによりエラー処理を行うことができます。 ``` @validate(str){ let v=Json:parse(str) @@ -274,3 +260,15 @@ AiScriptファイルにメタデータを埋め込める機能です。 else print('successful') } ``` + +## エラーメッセージ +進行不能なエラーが発生するとエラーメッセージが表示されます。 +``` +let scores=[10, 8, 5, 5] +let 3rd=scores[2] // unexpected token: NumberLiteral (Line 2, Column 5) +``` +``` +let arr=[] +arr[0] // Runtime: Index out of range. Index: 0 max: -1 (Line 2, Column 4) +``` +行(Line)、列(Column)は1始まりです。 diff --git a/docs/keywords.md b/docs/keywords.md index 0e2b71de..4f3e2aa7 100644 --- a/docs/keywords.md +++ b/docs/keywords.md @@ -1,10 +1,10 @@ ## 予約語について AiScriptにおける予約語とは、変数や関数の名前として使用することが禁止されている単語のことを言います。 使用するとSyntax Errorとなります。 -``` +```js // matchとforは予約語 let match=null // エラー -@for(){ print('hoge')} // エラー +@for(){ print('hoge') } // エラー ``` ## 使用中の語と使用予定の語 @@ -18,7 +18,7 @@ let match=null // エラー ## 一覧 以下の単語が予約語として登録されています。 ### 使用中の語 -`null`, `true`, `false`, `each`, `for`, `loop`, `break`, `continue`, `match`, `if`, `elif`, `else`, `return`, `eval`, `var`, `let`, `exists` +`null`, `true`, `false`, `each`, `for`, `loop`, `break`, `continue`, `match`, `case`, `default`, `if`, `elif`, `else`, `return`, `eval`, `var`, `let`, `exists` ### 使用予定の語 -`fn`, `namespace`, `meta`, `attr`, `attribute`, `static`, `class`, `struct`, `module`, `while`, `import`, `export` +`as`, `async`, `attr`, `attribute`, `await`, `catch`, `class`, `component`, `constructor`, `dictionary`, `do`, `enum`, `export`, `finally`, `fn`, `hash`, `in`, `interface`, `out`, `private`, `public`, `ref`, `static`, `struct`, `table`, `this`, `throw`, `trait`, `try`, `undefined`, `use`, `using`, `when`, `while`, `yield`, `import`, `is`, `meta`, `module`, `namespace`, `new` diff --git a/docs/literals.md b/docs/literals.md index 604440f3..4e58fa0f 100644 --- a/docs/literals.md +++ b/docs/literals.md @@ -27,11 +27,14 @@ false `'`または`"`が使用可能な通常の文字列リテラルと、`` ` ``を使用し文中に式を含むことができるテンプレートリテラルがあります。 #### エスケープについて -構文の一部として使われている文字は`\`を前置することで使うことができます。 -`'...'`では`\'`、 -`"..."`では`\"`、 -`` `...` ``では`` \` ``、`\{`、`\}`のエスケープがサポートされています。 -改行やタブ文字等のエスケープは未サポートです。 +`\`を前置した文字は、構文の一部ではなく一つの文字として解釈されます。 +例えば`'\''`は`'`、 +`"\""`では`"`、 +``` `\`` ```は`` ` ``、 +`` `\{` ``は`{`、として解釈されます。 +特に構文としての意味を持たない文字の場合、単に`\`が無視されます。例:`'\n'` → `n` +文字`\`を使用したい場合は`'\\'`のように2つ繋げてください。 +エスケープシーケンスは未サポートです。 #### 文字列リテラル ```js @@ -69,29 +72,37 @@ Previous statement is { !true }.` ### 配列 ```js [] // 空の配列 -[1 2 3] // 空白区切り(将来的に廃止予定) -[1, 1+1, 1+1+1] // ,で区切ることも出来る -[ // 改行可 +[1, 1+1, 1+1+1] // コロンで区切ることも出来る +[1, 1+1, 1+1+1,] // 最後の項に,をつけてもよい +[ // 改行区切りも可 + 'hoge' + 'huga' + 'piyo' +] +[ // コロンと改行の併用可 'hoge', 'huga', - 'piyo', // 最後の項に,をつけてもよい + 'piyo', ] ``` +```js +[1 2 3] // 空白区切りは廃止済み +``` ### オブジェクト ```js {} // 空のオブジェクト -{ +{ // 改行区切り a: 12 b: 'hoge' } -{a: 12,b: 'hoge'} // ワンライナー -{a: 12 b: 'hoge'} // 空白区切りは将来的に廃止予定 -{a: 12;b: 'hoge'} // セミコロン区切りは将来的に廃止予定 +{a: 12,b: 'hoge'} // コロン区切り ``` ```js -// :の後に空白必須 -{a:12,b:'hoge'} // Syntax Error +// 空白区切りは廃止済み +{a: 12 b: 'hoge'} // Syntax Error +// セミコロン区切りは廃止済み +{a: 12; b: 'hoge'} // Syntax Error ``` ### 関数 diff --git a/docs/parser/overview.md b/docs/parser/overview.md new file mode 100644 index 00000000..68020192 --- /dev/null +++ b/docs/parser/overview.md @@ -0,0 +1,16 @@ +# AiScriptパーサーの全体像 + +AiScriptのパーサーは2つの段階を経て構文ツリーに変換される。 + +1. ソースコードをトークン列に分割する +2. トークン列を順番に読み取って構文ツリー(AST)を構築する + +ソースコードをトークン列に分割する処理(トークナイズと呼ばれる)は「Scanner」というモジュールが担当する。 +トークン列から構文ツリーを構築する処理(パース)は、syntaxesディレクトリ以下にあるパース関数が担当する。名前がparseから始まっている関数がパース関数。 + +AiScriptのパーサーではトークナイズはまとめて行われない。 +パース関数が次のトークンを要求すると、下位モジュールであるScannerが次のトークンを1つだけ読み取る。 + +Scannerによって現在の読み取り位置(カーソル位置)が保持される。 +また、Scannerの各種メソッドで現在のトークンが期待されたものと一致するかどうかの確認やトークンの種類の取得などを行える。 +これらの機能を利用することにより、パース関数を簡潔に記述できる。 diff --git a/docs/parser/scanner.md b/docs/parser/scanner.md new file mode 100644 index 00000000..3f226e03 --- /dev/null +++ b/docs/parser/scanner.md @@ -0,0 +1,12 @@ +# Scanner 設計資料 + +## 現在のトークンと先読みされたトークン +_tokensの0番には現在のトークンが保持される。また、トークンが先読みされた場合は1番以降にそれらのトークンが保持されていくことになる。 +例えば、次のトークンを1つ先読みした場合は0番に現在のトークンが入り1番に先読みされたトークンが入る。 + +nextメソッドで現在位置が移動すると、それまで0番にあったトークン(現在のトークン)は配列から削除され、1番にあった要素は現在のトークンとなる。 +配列から全てのトークンが無くなった場合はトークンの読み取りが実行される。 + +## CharStream +ScannerはCharStreamを下位モジュールとして利用する。 +CharStreamは入力文字列から一文字ずつ文字を取り出すことができる。 diff --git a/docs/parser/token-streams.md b/docs/parser/token-streams.md new file mode 100644 index 00000000..62b1ca1d --- /dev/null +++ b/docs/parser/token-streams.md @@ -0,0 +1,11 @@ +# TokenStreams +各種パース関数はITokenStreamインターフェースを実装したクラスインスタンスを引数にとる。 + +実装クラス +- Scanner +- TokenStream + +## TokenStream +読み取り済みのトークン列を入力にとるストリーム。 +テンプレート構文の式部分ではトークン列の読み取りだけを先に行い、式の内容の解析はパース時に遅延して行われる。 +この時の読み取り済みのトークン列はTokenStremとしてパース関数に渡される。 diff --git a/docs/std.md b/docs/std.md index c3f804e9..65516602 100644 --- a/docs/std.md +++ b/docs/std.md @@ -105,9 +105,6 @@ _time_offset_ を渡していない場合はローカルのものを参照しま 数が多いため専用のページになっています。→[std-math.md](std-math.md) ## :: Num -### @Num:to_hex(_x_: num): str -数値から16進数の文字列を生成します。 - ### @Num:from_hex(_hex_: str): num 16進数の文字列から数値を生成します。 diff --git a/docs/syntax.md b/docs/syntax.md index a98b8a56..4d491f1d 100644 --- a/docs/syntax.md +++ b/docs/syntax.md @@ -71,7 +71,29 @@ let add2 = @(x, y) { ) { x + y } -@add5(x,y){x+y} // ワンライナー +// 省略可能引数 +@func1(a, b?) { + <: a + <: b // 省略されるとnullになる +} +func1('hoge') // 'hoge' null +// 初期値を設定された引数(省略可能引数と組み合わせて使用可能) +@func2(a, b?, c = 'piyo', d?) { + <: a + <: b + <: c + <: d +} +func2('hoge', 'fuga') // 'hoge' 'fuga' 'piyo' null +// 初期値には変数を使用可能(値は宣言時点で固定) +var v = 'hoge' +@func3(a = v) { + <: a +} +v = 'fuga' +func3() // 'hoge' +// ワンライナー +@func4(a,b?,c=1){<:a;<:b;<:c} ``` ```js // match等の予約語は関数名として使用できない @@ -87,6 +109,8 @@ var func = null @func() { // Runtime Error 'hoge' } +// 省略可能引数構文と初期値構文は併用できない +@func(a? = 1) {} // Syntax Error ``` ### 代入 @@ -182,6 +206,36 @@ each let v, arr{ // Syntax Error } ``` +### while +条件がtrueの間ループを続けます。 +条件が最初からfalseの場合はループは実行されません。 +```js +var count = 0 +while count < 42 { + count += 1 +} +<: count // 42 +// 条件が最初からfalseの場合 +while false { + <: 'hoge' +} // no output +``` + +### do-while +条件がtrueの間ループを続けます。 +条件が最初からfalseであってもループは一度実行されます。 +```js +var count = 0 +do { + count += 1 +} while count < 42 +<: count // 42 +// 条件が最初からfalseの場合 +do { + <: 'hoge' +} while false // hoge +``` + ### loop `break`されるまで無制限にループを行います。 ```js @@ -230,6 +284,42 @@ Ai:kun() // kawaii 値をスクリプト中に直接書き込むことができる構文です。 詳しくは→[literals.md](./literals.md) +### 演算子 +主要な演算を表現します。 +#### 単項演算子 +式に前置して使用します。論理否定(`!`)、正数符号(`+`)、負数符号(`-`)の三種があります。 +#### 二項演算子 +2つの式の間に置いて使用します。四則演算とその派生(`+`,`-`,`*`,`^`,`/`,`%`)、比較演算(`==`,`!=`,`<`,`<=`,`>`,`>=`)、論理演算(`&&`,`||`)があります。 +#### 演算子の優先度 +例えば`1 + 2 * 3`などは`2 * 3`が先に計算されてから`1 +`が行われます。これは`*`の優先度が`+`より高いことによるものです。優先度の一覧は下の表をご覧下さい。 +この優先度は`(1 + 2) * 3`のように`(` `)`で括ることで変えることができます。 +#### 二項演算子の糖衣構文性 +二項演算子は構文解析の過程でそれぞれ対応する組み込み関数に置き換えられます。 +(単項演算子である`!`にも対応する関数`Core:not`が存在しますが、置き換えは行われていません) +何の関数に置き換えられるかは下の表をご覧下さい。 +### 演算子一覧 +上から順に優先度が高くなっています。(一部優先度が同じものもあります) + + + + + + + + + + + + + + + + + + + +
演算子対応する関数意味
^Core:pow冪算
+(単項)なし正数
-(単項)なし負数
!(単項)なし否定
*Core:mul乗算
/Core:div除算
%Core:mod剰余
+Core:add加算
-Core:sub減算
>Core:gt大きい
>=Core:gteq以上
<Core:lt小さい
<=Core:lteq以下
==Core:eq等しい
!=Core:neq等しくない
&&Core:andかつ
||Core:orまたは
+ ### if キーワード`if`に続く式がtrueに評価されるかfalseに評価されるかで条件分岐を行います。 式として扱うことができ、最後の文の値を返します。 @@ -283,11 +373,14 @@ let foo = eval { ```js let x = 1 let y = match x { - 1 => "yes" - 0 => "no" - * => "other" + case 1 => "yes" + case 0 => "no" + default => "other" } <: y // "yes" + +// ワンライナー +<:match x{case 1=>"yes",case 0=>"no",default=>"other"} // "yes" ``` ### exists diff --git a/etc/aiscript.api.md b/etc/aiscript.api.md index 983f4e95..bd332994 100644 --- a/etc/aiscript.api.md +++ b/etc/aiscript.api.md @@ -13,15 +13,6 @@ type AddAssign = NodeBase & { expr: Expression; }; -// Warning: (ae-forgotten-export) The symbol "NodeBase_2" needs to be exported by the entry point index.d.ts -// -// @public (undocumented) -type AddAssign_2 = NodeBase_2 & { - type: 'addAssign'; - dest: Expression_2; - expr: Expression_2; -}; - // @public (undocumented) export const AISCRIPT_VERSION: "0.19.0"; @@ -32,6 +23,8 @@ abstract class AiScriptError extends Error { info?: any; // (undocumented) name: string; + // (undocumented) + pos?: Pos; } // @public @@ -39,6 +32,15 @@ class AiScriptIndexOutOfRangeError extends AiScriptRuntimeError { constructor(message: string, info?: any); } +// @public +class AiScriptNamespaceError extends AiScriptError { + constructor(message: string, pos: Pos, info?: any); + // (undocumented) + name: string; + // (undocumented) + pos: Pos; +} + // @public class AiScriptRuntimeError extends AiScriptError { constructor(message: string, info?: any); @@ -48,16 +50,20 @@ class AiScriptRuntimeError extends AiScriptError { // @public class AiScriptSyntaxError extends AiScriptError { - constructor(message: string, info?: any); + constructor(message: string, pos: Pos, info?: any); // (undocumented) name: string; + // (undocumented) + pos: Pos; } // @public class AiScriptTypeError extends AiScriptError { - constructor(message: string, info?: any); + constructor(message: string, pos: Pos, info?: any); // (undocumented) name: string; + // (undocumented) + pos: Pos; } // @public @@ -75,14 +81,6 @@ type And = NodeBase & { operatorLoc: Loc; }; -// @public (undocumented) -type And_2 = NodeBase_2 & { - type: 'and'; - left: Expression_2; - right: Expression_2; - operatorLoc: Loc; -}; - // @public (undocumented) const ARR: (arr: VArr['value']) => VArr; @@ -92,14 +90,6 @@ type Arr = NodeBase & { value: Expression[]; }; -// Warning: (ae-forgotten-export) The symbol "ChainProp" needs to be exported by the entry point index.d.ts -// -// @public (undocumented) -type Arr_2 = NodeBase_2 & ChainProp & { - type: 'arr'; - value: Expression_2[]; -}; - // @public (undocumented) function assertArray(val: Value | null | undefined): asserts val is VArr; @@ -125,23 +115,16 @@ type Assign = NodeBase & { expr: Expression; }; -// @public (undocumented) -type Assign_2 = NodeBase_2 & { - type: 'assign'; - dest: Expression_2; - expr: Expression_2; -}; - declare namespace Ast { export { isStatement, isExpression, + Pos, Loc, Node_2 as Node, - Statement, - Expression, Namespace, Meta, + Statement, Definition, Attribute, Return, @@ -153,6 +136,7 @@ declare namespace Ast { AddAssign, SubAssign, Assign, + Expression, Not, And, Or, @@ -194,25 +178,12 @@ type Attribute = NodeBase & { value: Expression; }; -// @public (undocumented) -type Attribute_2 = NodeBase_2 & { - type: 'attr'; - name: string; - value: Expression_2; -}; - // @public (undocumented) type Block = NodeBase & { type: 'block'; statements: (Statement | Expression)[]; }; -// @public (undocumented) -type Block_2 = NodeBase_2 & ChainProp & { - type: 'block'; - statements: (Statement_2 | Expression_2)[]; -}; - // @public (undocumented) const BOOL: (bool: VBool['value']) => VBool; @@ -222,12 +193,6 @@ type Bool = NodeBase & { value: boolean; }; -// @public (undocumented) -type Bool_2 = NodeBase_2 & ChainProp & { - type: 'bool'; - value: boolean; -}; - // @public (undocumented) const BREAK: () => Value; @@ -236,17 +201,6 @@ type Break = NodeBase & { type: 'break'; }; -// @public (undocumented) -type Break_2 = NodeBase_2 & { - type: 'break'; -}; - -// @public (undocumented) -function CALL(target: Call_2['target'], args: Call_2['args'], loc?: { - start: number; - end: number; -}): Call_2; - // @public (undocumented) type Call = NodeBase & { type: 'call'; @@ -254,22 +208,6 @@ type Call = NodeBase & { args: Expression[]; }; -// @public (undocumented) -type Call_2 = NodeBase_2 & { - type: 'call'; - target: Expression_2; - args: Expression_2[]; -}; - -// @public (undocumented) -type CallChain = NodeBase_2 & { - type: 'callChain'; - args: Expression_2[]; -}; - -// @public (undocumented) -type ChainMember = CallChain | IndexChain | PropChain; - // @public (undocumented) const CONTINUE: () => Value; @@ -278,67 +216,6 @@ type Continue = NodeBase & { type: 'continue'; }; -// @public (undocumented) -type Continue_2 = NodeBase_2 & { - type: 'continue'; -}; - -declare namespace Cst { - export { - isStatement_2 as isStatement, - isExpression_2 as isExpression, - hasChainProp, - CALL, - INDEX, - PROP, - Node_3 as Node, - Statement_2 as Statement, - Expression_2 as Expression, - Namespace_2 as Namespace, - Meta_2 as Meta, - Definition_2 as Definition, - Attribute_2 as Attribute, - Return_2 as Return, - Each_2 as Each, - For_2 as For, - Loop_2 as Loop, - Break_2 as Break, - Continue_2 as Continue, - AddAssign_2 as AddAssign, - SubAssign_2 as SubAssign, - Assign_2 as Assign, - InfixOperator, - Infix, - Not_2 as Not, - And_2 as And, - Or_2 as Or, - If_2 as If, - Fn_2 as Fn, - Match_2 as Match, - Block_2 as Block, - Exists_2 as Exists, - Tmpl_2 as Tmpl, - Str_2 as Str, - Num_2 as Num, - Bool_2 as Bool, - Null_2 as Null, - Obj_2 as Obj, - Arr_2 as Arr, - Identifier_2 as Identifier, - ChainMember, - CallChain, - IndexChain, - PropChain, - Call_2 as Call, - Index_2 as Index, - Prop_2 as Prop, - TypeSource_2 as TypeSource, - NamedTypeSource_2 as NamedTypeSource, - FnTypeSource_2 as FnTypeSource - } -} -export { Cst } - // @public (undocumented) type Definition = NodeBase & { type: 'def'; @@ -349,16 +226,6 @@ type Definition = NodeBase & { attr: Attribute[]; }; -// @public (undocumented) -type Definition_2 = NodeBase_2 & { - type: 'def'; - name: string; - varType?: TypeSource_2; - expr: Expression_2; - mut: boolean; - attr?: Attribute_2[]; -}; - // @public (undocumented) type Each = NodeBase & { type: 'each'; @@ -367,14 +234,6 @@ type Each = NodeBase & { for: Statement | Expression; }; -// @public (undocumented) -type Each_2 = NodeBase_2 & { - type: 'each'; - var: string; - items: Expression_2; - for: Statement_2 | Expression_2; -}; - // @public (undocumented) function eq(a: Value, b: Value): boolean; @@ -387,6 +246,7 @@ declare namespace errors { NonAiScriptError, AiScriptSyntaxError, AiScriptTypeError, + AiScriptNamespaceError, AiScriptRuntimeError, AiScriptIndexOutOfRangeError, AiScriptUserError @@ -400,23 +260,12 @@ type Exists = NodeBase & { identifier: Identifier; }; -// @public (undocumented) -type Exists_2 = NodeBase_2 & ChainProp & { - type: 'exists'; - identifier: Identifier_2; -}; - // @public (undocumented) function expectAny(val: Value | null | undefined): asserts val is Value; // @public (undocumented) type Expression = If | Fn | Match | Block | Exists | Tmpl | Str | Num | Bool | Null | Obj | Arr | Not | And | Or | Identifier | Call | Index | Prop; -// @public (undocumented) -type Expression_2 = Infix | Not_2 | And_2 | Or_2 | If_2 | Fn_2 | Match_2 | Block_2 | Exists_2 | Tmpl_2 | Str_2 | Num_2 | Bool_2 | Null_2 | Obj_2 | Arr_2 | Identifier_2 | Call_2 | // IR -Index_2 | // IR -Prop_2; - // @public (undocumented) const FALSE: { type: "bool"; @@ -424,13 +273,15 @@ const FALSE: { }; // @public (undocumented) -const FN: (args: VFn['args'], statements: VFn['statements'], scope: VFn['scope']) => VFn; +const FN: (args: VUserFn['args'], statements: VUserFn['statements'], scope: VUserFn['scope']) => VUserFn; // @public (undocumented) type Fn = NodeBase & { type: 'fn'; args: { name: string; + optional: boolean; + default?: Expression; argType?: TypeSource; }[]; retType?: TypeSource; @@ -438,18 +289,7 @@ type Fn = NodeBase & { }; // @public (undocumented) -type Fn_2 = NodeBase_2 & ChainProp & { - type: 'fn'; - args: { - name: string; - argType?: TypeSource_2; - }[]; - retType?: TypeSource_2; - children: (Statement_2 | Expression_2)[]; -}; - -// @public (undocumented) -const FN_NATIVE: (fn: VFn['native']) => VFn; +const FN_NATIVE: (fn: VNativeFn['native']) => VNativeFn; // @public (undocumented) type FnTypeSource = NodeBase & { @@ -458,13 +298,6 @@ type FnTypeSource = NodeBase & { result: TypeSource; }; -// @public (undocumented) -type FnTypeSource_2 = NodeBase_2 & { - type: 'fnTypeSource'; - args: TypeSource_2[]; - result: TypeSource_2; -}; - // @public (undocumented) type For = NodeBase & { type: 'for'; @@ -475,34 +308,15 @@ type For = NodeBase & { for: Statement | Expression; }; -// @public (undocumented) -type For_2 = NodeBase_2 & { - type: 'for'; - var?: string; - from?: Expression_2; - to?: Expression_2; - times?: Expression_2; - for: Statement_2 | Expression_2; -}; - // @public (undocumented) function getLangVersion(input: string): string | null; -// @public (undocumented) -function hasChainProp(x: T): x is T & ChainProp; - // @public (undocumented) type Identifier = NodeBase & { type: 'identifier'; name: string; }; -// @public (undocumented) -type Identifier_2 = NodeBase_2 & ChainProp & { - type: 'identifier'; - name: string; -}; - // @public (undocumented) type If = NodeBase & { type: 'if'; @@ -515,24 +329,6 @@ type If = NodeBase & { else?: Statement | Expression; }; -// @public (undocumented) -type If_2 = NodeBase_2 & { - type: 'if'; - cond: Expression_2; - then: Statement_2 | Expression_2; - elseif: { - cond: Expression_2; - then: Statement_2 | Expression_2; - }[]; - else?: Statement_2 | Expression_2; -}; - -// @public (undocumented) -function INDEX(target: Index_2['target'], index: Index_2['index'], loc?: { - start: number; - end: number; -}): Index_2; - // @public (undocumented) type Index = NodeBase & { type: 'index'; @@ -540,30 +336,6 @@ type Index = NodeBase & { index: Expression; }; -// @public (undocumented) -type Index_2 = NodeBase_2 & { - type: 'index'; - target: Expression_2; - index: Expression_2; -}; - -// @public (undocumented) -type IndexChain = NodeBase_2 & { - type: 'indexChain'; - index: Expression_2; -}; - -// @public (undocumented) -type Infix = NodeBase_2 & { - type: 'infix'; - operands: Expression_2[]; - operators: InfixOperator[]; - operatorLocs: Loc[]; -}; - -// @public (undocumented) -type InfixOperator = '||' | '&&' | '==' | '!=' | '<=' | '>=' | '<' | '>' | '+' | '-' | '*' | '^' | '/' | '%'; - // @public (undocumented) export class Interpreter { constructor(consts: Record, opts?: { @@ -601,9 +373,6 @@ function isBoolean(val: Value): val is VBool; // @public (undocumented) function isExpression(x: Node_2): x is Expression; -// @public (undocumented) -function isExpression_2(x: Node_3): x is Expression_2; - // @public (undocumented) function isFunction(val: Value): val is VFn; @@ -616,19 +385,16 @@ function isObject(val: Value): val is VObj; // @public (undocumented) function isStatement(x: Node_2): x is Statement; -// @public (undocumented) -function isStatement_2(x: Node_3): x is Statement_2; - // @public (undocumented) function isString(val: Value): val is VStr; // @public (undocumented) function jsToVal(val: any): Value; -// @public +// @public (undocumented) type Loc = { - start: number; - end: number; + start: Pos; + end: Pos; }; // @public (undocumented) @@ -637,12 +403,6 @@ type Loop = NodeBase & { statements: (Statement | Expression)[]; }; -// @public (undocumented) -type Loop_2 = NodeBase_2 & { - type: 'loop'; - statements: (Statement_2 | Expression_2)[]; -}; - // @public (undocumented) type Match = NodeBase & { type: 'match'; @@ -654,17 +414,6 @@ type Match = NodeBase & { default?: Statement | Expression; }; -// @public (undocumented) -type Match_2 = NodeBase_2 & ChainProp & { - type: 'match'; - about: Expression_2; - qs: { - q: Expression_2; - a: Statement_2 | Expression_2; - }[]; - default?: Statement_2 | Expression_2; -}; - // @public (undocumented) type Meta = NodeBase & { type: 'meta'; @@ -672,13 +421,6 @@ type Meta = NodeBase & { value: Expression; }; -// @public (undocumented) -type Meta_2 = NodeBase_2 & { - type: 'meta'; - name: string | null; - value: Expression_2; -}; - // @public (undocumented) type NamedTypeSource = NodeBase & { type: 'namedTypeSource'; @@ -686,13 +428,6 @@ type NamedTypeSource = NodeBase & { inner?: TypeSource; }; -// @public (undocumented) -type NamedTypeSource_2 = NodeBase_2 & { - type: 'namedTypeSource'; - name: string; - inner?: TypeSource_2; -}; - // @public (undocumented) type Namespace = NodeBase & { type: 'ns'; @@ -701,17 +436,7 @@ type Namespace = NodeBase & { }; // @public (undocumented) -type Namespace_2 = NodeBase_2 & { - type: 'ns'; - name: string; - members: (Definition_2 | Namespace_2)[]; -}; - -// @public (undocumented) -type Node_2 = Namespace | Meta | Statement | Expression | TypeSource; - -// @public (undocumented) -type Node_3 = Namespace_2 | Meta_2 | Statement_2 | Expression_2 | ChainMember | TypeSource_2; +type Node_2 = Namespace | Meta | Statement | Expression | TypeSource | Attribute; // @public class NonAiScriptError extends AiScriptError { @@ -726,12 +451,6 @@ type Not = NodeBase & { expr: Expression; }; -// @public (undocumented) -type Not_2 = NodeBase_2 & { - type: 'not'; - expr: Expression_2; -}; - // @public (undocumented) const NULL: { type: "null"; @@ -742,11 +461,6 @@ type Null = NodeBase & { type: 'null'; }; -// @public (undocumented) -type Null_2 = NodeBase_2 & ChainProp & { - type: 'null'; -}; - // @public (undocumented) const NUM: (num: VNum['value']) => VNum; @@ -756,12 +470,6 @@ type Num = NodeBase & { value: number; }; -// @public (undocumented) -type Num_2 = NodeBase_2 & ChainProp & { - type: 'num'; - value: number; -}; - // @public (undocumented) const OBJ: (obj: VObj['value']) => VObj; @@ -771,12 +479,6 @@ type Obj = NodeBase & { value: Map; }; -// @public (undocumented) -type Obj_2 = NodeBase_2 & ChainProp & { - type: 'obj'; - value: Map; -}; - // @public (undocumented) type Or = NodeBase & { type: 'or'; @@ -785,14 +487,6 @@ type Or = NodeBase & { operatorLoc: Loc; }; -// @public (undocumented) -type Or_2 = NodeBase_2 & { - type: 'or'; - left: Expression_2; - right: Expression_2; - operatorLoc: Loc; -}; - // @public (undocumented) export class Parser { constructor(); @@ -805,16 +499,16 @@ export class Parser { } // @public (undocumented) -export type ParserPlugin = (nodes: Cst.Node[]) => Cst.Node[]; +export type ParserPlugin = (nodes: Ast.Node[]) => Ast.Node[]; // @public (undocumented) export type PluginType = 'validate' | 'transform'; -// @public (undocumented) -function PROP(target: Prop_2['target'], name: Prop_2['name'], loc?: { - start: number; - end: number; -}): Prop_2; +// @public +type Pos = { + line: number; + column: number; +}; // @public (undocumented) type Prop = NodeBase & { @@ -823,19 +517,6 @@ type Prop = NodeBase & { name: string; }; -// @public (undocumented) -type Prop_2 = NodeBase_2 & { - type: 'prop'; - target: Expression_2; - name: string; -}; - -// @public (undocumented) -type PropChain = NodeBase_2 & { - type: 'propChain'; - name: string; -}; - // @public (undocumented) function reprValue(value: Value, literalLike?: boolean, processedObjects?: Set): string; @@ -848,12 +529,6 @@ type Return = NodeBase & { expr: Expression; }; -// @public (undocumented) -type Return_2 = NodeBase_2 & { - type: 'return'; - expr: Expression_2; -}; - // @public (undocumented) export class Scope { constructor(layerdStates?: Scope['layerdStates'], parent?: Scope, name?: Scope['name'], nsName?: string); @@ -882,10 +557,6 @@ export class Scope { // @public (undocumented) type Statement = Definition | Return | Each | For | Loop | Break | Continue | Assign | AddAssign | SubAssign; -// @public (undocumented) -type Statement_2 = Definition_2 | Return_2 | Attribute_2 | // AST -Each_2 | For_2 | Loop_2 | Break_2 | Continue_2 | Assign_2 | AddAssign_2 | SubAssign_2; - // @public (undocumented) const STR: (str: VStr['value']) => VStr; @@ -895,12 +566,6 @@ type Str = NodeBase & { value: string; }; -// @public (undocumented) -type Str_2 = NodeBase_2 & ChainProp & { - type: 'str'; - value: string; -}; - // @public (undocumented) type SubAssign = NodeBase & { type: 'subAssign'; @@ -908,25 +573,12 @@ type SubAssign = NodeBase & { expr: Expression; }; -// @public (undocumented) -type SubAssign_2 = NodeBase_2 & { - type: 'subAssign'; - dest: Expression_2; - expr: Expression_2; -}; - // @public (undocumented) type Tmpl = NodeBase & { type: 'tmpl'; tmpl: (string | Expression)[]; }; -// @public (undocumented) -type Tmpl_2 = NodeBase_2 & ChainProp & { - type: 'tmpl'; - tmpl: (string | Expression_2)[]; -}; - // @public (undocumented) const TRUE: { type: "bool"; @@ -936,9 +588,6 @@ const TRUE: { // @public (undocumented) type TypeSource = NamedTypeSource | FnTypeSource; -// @public (undocumented) -type TypeSource_2 = NamedTypeSource_2 | FnTypeSource_2; - // @public (undocumented) const unWrapRet: (v: Value) => Value; @@ -985,6 +634,9 @@ declare namespace values { VArr, VObj, VFn, + VUserFn, + VFnArg, + VNativeFn, VReturn, VBreak, VContinue, @@ -1041,18 +693,24 @@ type VError = { info?: Value; }; +// @public (undocumented) +type VFn = VUserFn | VNativeFn; + +// @public (undocumented) +type VFnArg = { + name: string; + type?: Type; + default?: Value; +}; + // @public -type VFn = { - type: 'fn'; - args?: string[]; - statements?: Node_2[]; - native?: (args: (Value | undefined)[], opts: { +type VNativeFn = VFnBase & { + native: (args: (Value | undefined)[], opts: { call: (fn: VFn, args: Value[]) => Promise; topCall: (fn: VFn, args: Value[]) => Promise; registerAbortHandler: (handler: () => void) => void; unregisterAbortHandler: (handler: () => void) => void; }) => Value | Promise | void; - scope?: Scope; }; // @public (undocumented) @@ -1084,6 +742,20 @@ type VStr = { value: string; }; +// Warning: (ae-forgotten-export) The symbol "VFnBase" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type VUserFn = VFnBase & { + native?: undefined; + args: VFnArg[]; + statements: Node_2[]; + scope: Scope; +}; + +// Warnings were encountered during analysis: +// +// src/interpreter/value.ts:46:2 - (ae-forgotten-export) The symbol "Type" needs to be exported by the entry point index.d.ts + // (No @packageDocumentation comment for this package) ``` diff --git a/jest.config.cjs b/jest.config.cjs index 4c87106b..ae2a8b83 100644 --- a/jest.config.cjs +++ b/jest.config.cjs @@ -154,15 +154,15 @@ module.exports = { // The glob patterns Jest uses to detect test files testMatch: [ - "**/__tests__/**/*.[jt]s?(x)", - "**/?(*.)+(spec|test).[tj]s?(x)", + // "**/__tests__/**/*.[jt]s?(x)", + // "**/?(*.)+(spec|test).[tj]s?(x)", "/test/**/*" ], // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped - // testPathIgnorePatterns: [ - // "\\\\node_modules\\\\" - // ], + testPathIgnorePatterns: [ + "/test/testutils.ts", + ], // The regexp pattern or array of patterns that Jest uses to detect test files // testRegex: [], diff --git a/package.json b/package.json index 54b0566d..9f7eec37 100644 --- a/package.json +++ b/package.json @@ -20,14 +20,10 @@ "scripts": { "start": "node ./run", "parse": "node ./parse", - "peg": "peggy --format es --cache -o src/parser/parser.js --allowed-start-rules Preprocess,Main src/parser/parser.peggy && npm run peg-copy", - "peg-debug": "peggy --trace --format es --cache -o src/parser/parser.js --allowed-start-rules Preprocess,Main src/parser/parser.peggy && npm run peg-copy", - "peg-copy": "copyfiles -f src/parser/parser.js built/parser/", "ts": "npm run ts-esm && npm run ts-dts", "ts-esm": "tsc --outDir built/esm", "ts-dts": "tsc --outDir built/dts --declaration true --emitDeclarationOnly true --declarationMap true", - "build": "node scripts/gen-pkg-ts.mjs && npm run peg && npm run ts", - "build-debug": "npm run peg-debug && tsc", + "build": "node scripts/gen-pkg-ts.mjs && npm run ts", "api": "npx api-extractor run --local --verbose", "api-prod": "npx api-extractor run --verbose", "lint": "eslint . --ext .js,.jsx,.ts,.tsx", @@ -46,11 +42,9 @@ "@typescript-eslint/eslint-plugin": "7.1.1", "@typescript-eslint/parser": "7.1.1", "chalk": "5.3.0", - "copyfiles": "2.4.1", "eslint": "8.57.0", "eslint-plugin-import": "2.29.1", "jest": "29.7.0", - "peggy": "4.0.2", "semver": "7.6.2", "ts-jest": "29.1.2", "ts-jest-resolver": "2.0.1", diff --git a/parse.js b/parse.js index 74a859cd..ade1f9cb 100644 --- a/parse.js +++ b/parse.js @@ -1,6 +1,7 @@ import fs from 'fs'; import { Parser } from '@syuilo/aiscript'; +import { inspect } from 'util'; const script = fs.readFileSync('./test.is', 'utf8'); const ast = Parser.parse(script); -console.log(JSON.stringify(ast, null, 2)); +console.log(inspect(ast, { depth: 10 })); diff --git a/src/@types/parser.d.ts b/src/@types/parser.d.ts deleted file mode 100644 index 6f2ff82c..00000000 --- a/src/@types/parser.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { Cst } from '../index.js'; - -declare module '*/parser.js' { - // FIXME: 型指定が効いていない - export const parse: (input: string, options: object) => Cst.Node[]; -} diff --git a/src/error.ts b/src/error.ts index 47f6a87a..4f1ffb53 100644 --- a/src/error.ts +++ b/src/error.ts @@ -1,9 +1,11 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +import type { Pos } from './node.js'; export abstract class AiScriptError extends Error { // name is read by Error.prototype.toString public name = 'AiScript'; public info?: any; + public pos?: Pos; constructor(message: string, info?: any) { super(message); @@ -32,8 +34,8 @@ export class NonAiScriptError extends AiScriptError { */ export class AiScriptSyntaxError extends AiScriptError { public name = 'Syntax'; - constructor(message: string, info?: any) { - super(message, info); + constructor(message: string, public pos: Pos, info?: any) { + super(`${message} (Line ${pos.line}, Column ${pos.column})`, info); } } /** @@ -41,8 +43,18 @@ export class AiScriptSyntaxError extends AiScriptError { */ export class AiScriptTypeError extends AiScriptError { public name = 'Type'; - constructor(message: string, info?: any) { - super(message, info); + constructor(message: string, public pos: Pos, info?: any) { + super(`${message} (Line ${pos.line}, Column ${pos.column})`, info); + } +} + +/** + * Namespace collection errors. + */ +export class AiScriptNamespaceError extends AiScriptError { + public name = 'Namespace'; + constructor(message: string, public pos: Pos, info?: any) { + super(`${message} (Line ${pos.line}, Column ${pos.column})`, info); } } diff --git a/src/index.ts b/src/index.ts index a2420552..f857e8c9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,7 +7,6 @@ import { Scope } from './interpreter/scope.js'; import * as utils from './interpreter/util.js'; import * as values from './interpreter/value.js'; import { Parser, ParserPlugin, PluginType } from './parser/index.js'; -import * as Cst from './parser/node.js'; import * as errors from './error.js'; import * as Ast from './node.js'; import { AISCRIPT_VERSION } from './constants.js'; @@ -18,7 +17,6 @@ export { values }; export { Parser }; export { ParserPlugin }; export { PluginType }; -export { Cst }; export { errors }; export { Ast }; export { AISCRIPT_VERSION }; diff --git a/src/interpreter/index.ts b/src/interpreter/index.ts index eacf7a88..1899731e 100644 --- a/src/interpreter/index.ts +++ b/src/interpreter/index.ts @@ -3,7 +3,7 @@ */ import { autobind } from '../utils/mini-autobind.js'; -import { AiScriptError, NonAiScriptError, AiScriptIndexOutOfRangeError, AiScriptRuntimeError } from '../error.js'; +import { AiScriptError, NonAiScriptError, AiScriptNamespaceError, AiScriptIndexOutOfRangeError, AiScriptRuntimeError } from '../error.js'; import { Scope } from './scope.js'; import { std } from './lib/std.js'; import { assertNumber, assertString, assertFunction, assertBoolean, assertObject, assertArray, eq, isObject, isArray, expectAny, reprValue } from './util.js'; @@ -84,10 +84,6 @@ export class Interpreter { * (ii)Otherwise, just throws a error. * * @remarks This is the same function as that passed to AiScript NATIVE functions as opts.topCall. - * - * @param fn - the function - * @param args - arguments for the function - * @returns Return value of the function, or ERROR('func_failed') when the (i) condition above is fulfilled. */ @autobind public async execFn(fn: VFn, args: Value[]): Promise { @@ -102,10 +98,6 @@ export class Interpreter { * Almost same as execFn but when error occurs this always throws and never calls callback. * * @remarks This is the same function as that passed to AiScript NATIVE functions as opts.call. - * - * @param fn - the function - * @param args - arguments for the function - * @returns Return value of the function. */ @autobind public execFnSimple(fn: VFn, args: Value[]): Promise { @@ -199,7 +191,7 @@ export class Interpreter { switch (node.type) { case 'def': { if (node.mut) { - throw new Error('Namespaces cannot include mutable variable: ' + node.name); + throw new AiScriptNamespaceError('No "var" in namespace declaration: ' + node.name, node.loc.start); } const variable: Variable = { @@ -216,7 +208,10 @@ export class Interpreter { } default: { - throw new Error('invalid ns member type: ' + (node as Ast.Node).type); + // exhaustiveness check + const n: never = node; + const nd = n as Ast.Node; + throw new AiScriptNamespaceError('invalid ns member type: ' + nd.type, nd.loc.start); } } } @@ -235,10 +230,12 @@ export class Interpreter { return result ?? NULL; } else { const _args = new Map(); - for (let i = 0; i < (fn.args ?? []).length; i++) { - _args.set(fn.args![i]!, { + for (const i of fn.args.keys()) { + const argdef = fn.args[i]!; + if (!argdef.default) expectAny(args[i]); + _args.set(argdef.name, { isMutable: true, - value: args[i] ?? NULL, + value: args[i] ?? argdef.default!, }); } const fnScope = fn.scope!.createChildScope(_args); @@ -247,7 +244,20 @@ export class Interpreter { } @autobind - private async _eval(node: Ast.Node, scope: Scope): Promise { + private _eval(node: Ast.Node, scope: Scope): Promise { + return this.__eval(node, scope).catch(e => { + if (e.pos) throw e; + else { + const e2 = (e instanceof AiScriptError) ? e : new NonAiScriptError(e); + e2.pos = node.loc.start; + e2.message = `${e2.message} (Line ${e2.pos.line}, Column ${e2.pos.column})`; + throw e2; + } + }); + } + + @autobind + private async __eval(node: Ast.Node, scope: Scope): Promise { if (this.stop) return NULL; if (this.stepCount % IRQ_RATE === IRQ_AT) await new Promise(resolve => setTimeout(resolve, 5)); this.stepCount++; @@ -478,7 +488,17 @@ export class Interpreter { } case 'fn': { - return FN(node.args.map(arg => arg.name), node.children, scope); + return FN( + await Promise.all(node.args.map(async (arg) => { + return { + name: arg.name, + default: arg.default ? await this._eval(arg.default, scope) : arg.optional ? NULL : undefined, + // type: (TODO) + }; + })), + node.children, + scope, + ); } case 'block': { diff --git a/src/interpreter/lib/std.ts b/src/interpreter/lib/std.ts index b451d7e6..f3fbbc86 100644 --- a/src/interpreter/lib/std.ts +++ b/src/interpreter/lib/std.ts @@ -472,11 +472,6 @@ export const std: Record = { //#endregion //#region Num - 'Num:to_hex': FN_NATIVE(([v]) => { - assertNumber(v); - return STR(v.value.toString(16)); - }), - 'Num:from_hex': FN_NATIVE(([v]) => { assertString(v); return NUM(parseInt(v.value, 16)); diff --git a/src/interpreter/primitive-props.ts b/src/interpreter/primitive-props.ts index c89a4cb2..bc3541b0 100644 --- a/src/interpreter/primitive-props.ts +++ b/src/interpreter/primitive-props.ts @@ -15,6 +15,10 @@ const PRIMITIVE_PROPS: { to_str: (target: VNum): VFn => FN_NATIVE(async (_, _opts) => { return STR(target.value.toString()); }), + + to_hex: (target: VNum): VFn => FN_NATIVE(async (_, _opts) => { + return STR(target.value.toString(16)); + }), }, str: { @@ -291,7 +295,7 @@ const PRIMITIVE_PROPS: { const r = right[rightIndex]!; const compValue = await opts.call(comp, [l, r]); assertNumber(compValue); - if (compValue.value < 0) { + if (compValue.value <= 0) { result.push(left[leftIndex]!); leftIndex++; } else { diff --git a/src/interpreter/util.ts b/src/interpreter/util.ts index 1d60c0f3..f2ba2ce6 100644 --- a/src/interpreter/util.ts +++ b/src/interpreter/util.ts @@ -87,6 +87,7 @@ export function isArray(val: Value): val is VArr { } export function eq(a: Value, b: Value): boolean { + if (a.type === 'fn' && b.type === 'fn') return a.native && b.native ? a.native === b.native : a === b; if (a.type === 'fn' || b.type === 'fn') return false; if (a.type === 'null' && b.type === 'null') return true; if (a.type === 'null' || b.type === 'null') return false; @@ -188,7 +189,12 @@ export function reprValue(value: Value, literalLike = false, processedObjects = if (value.type === 'bool') return value.value.toString(); if (value.type === 'null') return 'null'; if (value.type === 'fn') { - return `@( ${(value.args ?? []).join(', ')} ) { ... }`; + if (value.native) { + // そのうちネイティブ関数の引数も表示できるようにしたいが、ホスト向けの破壊的変更を伴うと思われる + return '@( ?? ) { native code }'; + } else { + return `@( ${(value.args.map(v => v.name)).join(', ')} ) { ... }`; + } } return '?'; diff --git a/src/interpreter/value.ts b/src/interpreter/value.ts index 0933bc18..3250afd7 100644 --- a/src/interpreter/value.ts +++ b/src/interpreter/value.ts @@ -1,4 +1,5 @@ import type { Node } from '../node.js'; +import type { Type } from '../type.js'; import type { Scope } from './scope.js'; export type VNull = { @@ -30,20 +31,31 @@ export type VObj = { value: Map; }; +export type VFn = VUserFn | VNativeFn; +type VFnBase = { + type: 'fn'; +}; +export type VUserFn = VFnBase & { + native?: undefined; // if (vfn.native) で型アサーション出来るように + args: VFnArg[]; + statements: Node[]; + scope: Scope; +}; +export type VFnArg = { + name: string; + type?: Type; + default?: Value; +} /** * When your AiScript NATIVE function passes VFn.call to other caller(s) whose error thrown outside the scope, use VFn.topCall instead to keep it under AiScript error control system. */ -export type VFn = { - type: 'fn'; - args?: string[]; - statements?: Node[]; - native?: (args: (Value | undefined)[], opts: { +export type VNativeFn = VFnBase & { + native: (args: (Value | undefined)[], opts: { call: (fn: VFn, args: Value[]) => Promise; topCall: (fn: VFn, args: Value[]) => Promise; registerAbortHandler: (handler: () => void) => void; unregisterAbortHandler: (handler: () => void) => void; }) => Value | Promise | void; - scope?: Scope; }; export type VReturn = { @@ -115,14 +127,14 @@ export const ARR = (arr: VArr['value']): VArr => ({ value: arr, }); -export const FN = (args: VFn['args'], statements: VFn['statements'], scope: VFn['scope']): VFn => ({ +export const FN = (args: VUserFn['args'], statements: VUserFn['statements'], scope: VUserFn['scope']): VUserFn => ({ type: 'fn' as const, args: args, statements: statements, scope: scope, }); -export const FN_NATIVE = (fn: VFn['native']): VFn => ({ +export const FN_NATIVE = (fn: VNativeFn['native']): VNativeFn => ({ type: 'fn' as const, native: fn, }); diff --git a/src/node.ts b/src/node.ts index aebff7de..ebc4ad62 100644 --- a/src/node.ts +++ b/src/node.ts @@ -1,15 +1,36 @@ /** * ASTノード - * - * ASTノードはCSTノードをインタプリタ等から操作しやすい構造に変形したものです。 */ +export type Pos = { + line: number; + column: number; +}; + export type Loc = { - start: number; - end: number; + start: Pos; + end: Pos; +}; + +export type Node = Namespace | Meta | Statement | Expression | TypeSource | Attribute; + +type NodeBase = { + loc: Loc; // コード位置 }; -export type Node = Namespace | Meta | Statement | Expression | TypeSource; +export type Namespace = NodeBase & { + type: 'ns'; // 名前空間 + name: string; // 空間名 + members: (Definition | Namespace)[]; // メンバー +}; + +export type Meta = NodeBase & { + type: 'meta'; // メタデータ定義 + name: string | null; // 名 + value: Expression; // 値 +}; + +// statement export type Statement = Definition | @@ -30,53 +51,6 @@ export function isStatement(x: Node): x is Statement { return statementTypes.includes(x.type); } -export type Expression = - If | - Fn | - Match | - Block | - Exists | - Tmpl | - Str | - Num | - Bool | - Null | - Obj | - Arr | - Not | - And | - Or | - Identifier | - Call | - Index | - Prop; - -const expressionTypes = [ - 'if', 'fn', 'match', 'block', 'exists', 'tmpl', 'str', 'num', 'bool', 'null', 'obj', 'arr', 'identifier', 'call', 'index', 'prop', -]; -export function isExpression(x: Node): x is Expression { - return expressionTypes.includes(x.type); -} - -type NodeBase = { - loc?: { // コード位置 - start: number; - end: number; - }; -}; - -export type Namespace = NodeBase & { - type: 'ns'; // 名前空間 - name: string; // 空間名 - members: (Definition | Namespace)[]; // メンバー -}; - -export type Meta = NodeBase & { - type: 'meta'; // メタデータ定義 - name: string | null; // 名 - value: Expression; // 値 -}; - export type Definition = NodeBase & { type: 'def'; // 変数宣言文 name: string; // 変数名 @@ -144,6 +118,36 @@ export type Assign = NodeBase & { expr: Expression; // 式 }; +// expressions + +export type Expression = + If | + Fn | + Match | + Block | + Exists | + Tmpl | + Str | + Num | + Bool | + Null | + Obj | + Arr | + Not | + And | + Or | + Identifier | + Call | + Index | + Prop; + +const expressionTypes = [ + 'if', 'fn', 'match', 'block', 'exists', 'tmpl', 'str', 'num', 'bool', 'null', 'obj', 'arr', 'not', 'and', 'or', 'identifier', 'call', 'index', 'prop', +]; +export function isExpression(x: Node): x is Expression { + return expressionTypes.includes(x.type); +} + export type Not = NodeBase & { type: 'not'; // 否定 expr: Expression; // 式 @@ -178,6 +182,8 @@ export type Fn = NodeBase & { type: 'fn'; // 関数 args: { name: string; // 引数名 + optional: boolean; + default?: Expression; // 引数の初期値 argType?: TypeSource; // 引数の型 }[]; retType?: TypeSource; // 戻り値の型 @@ -243,14 +249,6 @@ export type Identifier = NodeBase & { name: string; // 変数名 }; -// chain node example: -// call > fn -// call > var(fn) -// index > arr -// index > var(arr) -// prop > prop(obj) > var(obj) -// call > prop(fn) > obj - export type Call = NodeBase & { type: 'call'; // 関数呼び出し target: Expression; // 対象 diff --git a/src/parser/index.ts b/src/parser/index.ts index 0c34f647..07092dbb 100644 --- a/src/parser/index.ts +++ b/src/parser/index.ts @@ -1,15 +1,12 @@ -import { AiScriptSyntaxError } from '../error.js'; -import * as parser from './parser.js'; +import { Scanner } from './scanner.js'; +import { parseTopLevel } from './syntaxes/toplevel.js'; import { validateKeyword } from './plugins/validate-keyword.js'; import { validateType } from './plugins/validate-type.js'; -import { setAttribute } from './plugins/set-attribute.js'; -import { transformChain } from './plugins/transform-chain.js'; -import { infixToFnCall } from './plugins/infix-to-fncall.js'; -import type * as Cst from './node.js'; + import type * as Ast from '../node.js'; -export type ParserPlugin = (nodes: Cst.Node[]) => Cst.Node[]; +export type ParserPlugin = (nodes: Ast.Node[]) => Ast.Node[]; export type PluginType = 'validate' | 'transform'; export class Parser { @@ -26,9 +23,6 @@ export class Parser { validateType, ], transform: [ - setAttribute, - transformChain, - infixToFnCall, ], }; } @@ -54,24 +48,10 @@ export class Parser { } public parse(input: string): Ast.Node[] { - let nodes: Cst.Node[]; + let nodes: Ast.Node[]; - // generate a node tree - try { - // apply preprocessor - const code = parser.parse(input, { startRule: 'Preprocess' }); - // apply main parser - nodes = parser.parse(code, { startRule: 'Main' }); - } catch (e) { - if (e.location) { - if (e.expected) { - throw new AiScriptSyntaxError(`Parsing error. (Line ${e.location.start.line}:${e.location.start.column})`, e); - } else { - throw new AiScriptSyntaxError(`${e.message} (Line ${e.location.start.line}:${e.location.start.column})`, e); - } - } - throw e; - } + const scanner = new Scanner(input); + nodes = parseTopLevel(scanner); // validate the node tree for (const plugin of this.plugins.validate) { @@ -83,6 +63,6 @@ export class Parser { nodes = plugin(nodes); } - return nodes as Ast.Node[]; + return nodes; } } diff --git a/src/parser/node.ts b/src/parser/node.ts deleted file mode 100644 index a165a08d..00000000 --- a/src/parser/node.ts +++ /dev/null @@ -1,331 +0,0 @@ -/** - * CSTノード - * - * パーサーが生成する直接的な処理結果です。 - * パーサーが生成しやすい形式になっているため、インタプリタ等では操作しにくい構造になっていることがあります。 - * この処理結果がプラグインによって処理されるとASTノードとなります。 -*/ - -import type { Loc } from '../node.js'; - -export type Node = Namespace | Meta | Statement | Expression | ChainMember | TypeSource; - -export type Statement = - Definition | - Return | - Attribute | // AST - Each | - For | - Loop | - Break | - Continue | - Assign | - AddAssign | - SubAssign; - -const statementTypes = [ - 'def', 'return', 'attr', 'each', 'for', 'loop', 'break', 'continue', 'assign', 'addAssign', 'subAssign', -]; -export function isStatement(x: Node): x is Statement { - return statementTypes.includes(x.type); -} - -export type Expression = - Infix | - Not | - And | - Or | - If | - Fn | - Match | - Block | - Exists | - Tmpl | - Str | - Num | - Bool | - Null | - Obj | - Arr | - Identifier | - Call | // IR - Index | // IR - Prop; // IR - -const expressionTypes = [ - 'infix', 'if', 'fn', 'match', 'block', 'exists', 'tmpl', 'str', 'num', 'bool', 'null', 'obj', 'arr', 'identifier', 'call', 'index', 'prop', -]; -export function isExpression(x: Node): x is Expression { - return expressionTypes.includes(x.type); -} - -type NodeBase = { - __AST_NODE: never; // phantom type - loc?: Loc; -}; - -export type Namespace = NodeBase & { - type: 'ns'; - name: string; - members: (Definition | Namespace)[]; -}; - -export type Meta = NodeBase & { - type: 'meta'; - name: string | null; - value: Expression; -}; - -export type Definition = NodeBase & { - type: 'def'; - name: string; - varType?: TypeSource; - expr: Expression; - mut: boolean; - attr?: Attribute[]; // IR -}; - -export type Attribute = NodeBase & { - type: 'attr'; - name: string; - value: Expression; -}; - -export type Return = NodeBase & { - type: 'return'; - expr: Expression; -}; - -export type Each = NodeBase & { - type: 'each'; - var: string; - items: Expression; - for: Statement | Expression; -}; - -export type For = NodeBase & { - type: 'for'; - var?: string; - from?: Expression; - to?: Expression; - times?: Expression; - for: Statement | Expression; -}; - -export type Loop = NodeBase & { - type: 'loop'; - statements: (Statement | Expression)[]; -}; - -export type Break = NodeBase & { - type: 'break'; -}; - -export type Continue = NodeBase & { - type: 'continue'; -}; - -export type AddAssign = NodeBase & { - type: 'addAssign'; - dest: Expression; - expr: Expression; -}; - -export type SubAssign = NodeBase & { - type: 'subAssign'; - dest: Expression; - expr: Expression; -}; - -export type Assign = NodeBase & { - type: 'assign'; - dest: Expression; - expr: Expression; -}; - -export type InfixOperator = '||' | '&&' | '==' | '!=' | '<=' | '>=' | '<' | '>' | '+' | '-' | '*' | '^' | '/' | '%'; - -export type Infix = NodeBase & { - type: 'infix'; - operands: Expression[]; - operators: InfixOperator[]; - operatorLocs: Loc[]; -}; - -export type Not = NodeBase & { - type: 'not'; - expr: Expression; -}; - -export type And = NodeBase & { - type: 'and'; - left: Expression; - right: Expression; - operatorLoc: Loc; -} - -export type Or = NodeBase & { - type: 'or'; - left: Expression; - right: Expression; - operatorLoc: Loc; -} - -export type If = NodeBase & { - type: 'if'; - cond: Expression; - then: Statement | Expression; - elseif: { - cond: Expression; - then: Statement | Expression; - }[]; - else?: Statement | Expression; -}; - -export type Fn = NodeBase & ChainProp & { - type: 'fn'; - args: { - name: string; - argType?: TypeSource; - }[]; - retType?: TypeSource; - children: (Statement | Expression)[]; -}; - -export type Match = NodeBase & ChainProp & { - type: 'match'; - about: Expression; - qs: { - q: Expression; - a: Statement | Expression; - }[]; - default?: Statement | Expression; -}; - -export type Block = NodeBase & ChainProp & { - type: 'block'; - statements: (Statement | Expression)[]; -}; - -export type Exists = NodeBase & ChainProp & { - type: 'exists'; - identifier: Identifier; -}; - -export type Tmpl = NodeBase & ChainProp & { - type: 'tmpl'; - tmpl: (string | Expression)[]; -}; - -export type Str = NodeBase & ChainProp & { - type: 'str'; - value: string; -}; - -export type Num = NodeBase & ChainProp & { - type: 'num'; - value: number; -}; - -export type Bool = NodeBase & ChainProp & { - type: 'bool'; - value: boolean; -}; - -export type Null = NodeBase & ChainProp & { - type: 'null'; -}; - -export type Obj = NodeBase & ChainProp & { - type: 'obj'; - value: Map; -}; - -export type Arr = NodeBase & ChainProp & { - type: 'arr'; - value: Expression[]; -}; - -export type Identifier = NodeBase & ChainProp & { - type: 'identifier'; - name: string; -}; - -// AST -type ChainProp = { - chain?: ChainMember[]; -}; - -// AST -export function hasChainProp(x: T): x is T & ChainProp { - return 'chain' in x && x.chain !== null; -} - -// AST -export type ChainMember = CallChain | IndexChain | PropChain; - -// AST -export type CallChain = NodeBase & { - type: 'callChain'; - args: Expression[]; -}; - -// AST -export type IndexChain = NodeBase & { - type: 'indexChain'; - index: Expression; -}; - -// AST -export type PropChain = NodeBase & { - type: 'propChain'; - name: string; -}; - -// IR -export type Call = NodeBase & { - type: 'call'; - target: Expression; - args: Expression[]; -}; -export function CALL(target: Call['target'], args: Call['args'], loc?: { start: number, end: number }): Call { - return { type: 'call', target, args, loc } as Call; -} - -// IR -export type Index = NodeBase & { - type: 'index'; - target: Expression; - index: Expression; -}; - -export function INDEX(target: Index['target'], index: Index['index'], loc?: { start: number, end: number }): Index { - return { type: 'index', target, index, loc } as Index; -} - -// IR -export type Prop = NodeBase & { - type: 'prop'; - target: Expression; - name: string; -}; - -export function PROP(target: Prop['target'], name: Prop['name'], loc?: { start: number, end: number }): Prop { - return { type: 'prop', target, name, loc } as Prop; -} - -// Type source - -export type TypeSource = NamedTypeSource | FnTypeSource; - -export type NamedTypeSource = NodeBase & { - type: 'namedTypeSource'; - name: string; - inner?: TypeSource; -}; - -export type FnTypeSource = NodeBase & { - type: 'fnTypeSource'; - args: TypeSource[]; - result: TypeSource; -}; diff --git a/src/parser/parser.peggy b/src/parser/parser.peggy deleted file mode 100644 index 0a9b27b2..00000000 --- a/src/parser/parser.peggy +++ /dev/null @@ -1,608 +0,0 @@ -{ - function createNode(type, params, children) { - const node = { type }; - params.children = children; - for (const key of Object.keys(params)) { - if (params[key] !== undefined) { - node[key] = params[key]; - } - } - const loc = location(); - node.loc = { start: loc.start.offset, end: loc.end.offset - 1 }; - return node; - } -} - -// -// preprocessor -// - -Preprocess - = s:PreprocessPart* -{ return s.join(''); } - -PreprocessPart - = Tmpl { return text(); } - / Str { return text(); } - / Comment - / . - -Comment - = "//" (!EOL .)* { return ' '.repeat(text().length); } - / "/*" (!"*/" .)* "*/" { return text().replace(/[^\n]/g, ' '); } - -// -// main parser -// - -Main - = _* content:GlobalStatements? _* -{ return content ?? []; } - -GlobalStatements - = head:GlobalStatement tails:(__* LF _* s:GlobalStatement { return s; })* -{ return [head, ...tails]; } - -NamespaceStatements - = head:NamespaceStatement tails:(__* LF _* s:NamespaceStatement { return s; })* -{ return [head, ...tails]; } - -Statements - = head:Statement tails:(__* LF _* s:Statement { return s; })* -{ return [head, ...tails]; } - -// list of global statements - -GlobalStatement - = Namespace // "::" - / Meta // "###" - / Statement - -// list of namespace statement - -NamespaceStatement - = VarDef - / FnDef - / Namespace - -// list of statement - -Statement - = VarDef // "let" NAME | "var" NAME - / FnDef // "@" - / Out // "<:" - / Return // "return" - / Attr // "+" - / Each // "each" - / For // "for" - / Loop // "loop" - / Break // "break" - / Continue // "continue" - / Assign // Expr "=" | Expr "+=" | Expr "-=" - / Expr - -// list of expression - -Expr - = Infix - / Expr2 - -Expr2 - = If // "if" - / Fn // "@(" - / Chain // Expr3 "(" | Expr3 "[" | Expr3 "." - / Expr3 - -Expr3 - = Match // "match" - / Eval // "eval" - / Exists // "exists" - / Tmpl // "`" - / Str // "\"" - / Num // "+" | "-" | "1"~"9" - / Bool // "true" | "false" - / Null // "null" - / Obj // "{" - / Arr // "[" - / Not // "!" - / Identifier // NAME_WITH_NAMESPACE - / "(" _* e:Expr _* ")" { return e; } - -// list of static literal - -StaticLiteral - = Num // "+" "1"~"9" | "-" "1"~"9" | "1"~"9" - / Str // "\"" - / Bool // "true" | "false" - / StaticArr // "[" - / StaticObj // "{" - / Null // "null" - - - -// -// global statements --------------------------------------------------------------------- -// - -// namespace statement - -Namespace - = "::" _+ name:NAME _+ "{" _* members:NamespaceStatements? _* "}" -{ return createNode('ns', { name, members }); } - -// meta statement - -Meta - = "###" __* name:NAME _* value:StaticLiteral -{ return createNode('meta', { name, value }); } - / "###" __* value:StaticLiteral -{ return createNode('meta', { name: null, value }); } - - - -// -// statements ---------------------------------------------------------------------------- -// - -// define statement - -VarDef - = "let" _+ name:NAME type:(_* ":" _* @Type)? _* "=" _* expr:Expr -{ return createNode('def', { name, varType: type, expr, mut: false, attr: [] }); } - / "var" _+ name:NAME type:(_* ":" _* @Type)? _* "=" _* expr:Expr -{ return createNode('def', { name, varType: type, expr, mut: true, attr: [] }); } - -// output statement - -// NOTE: out is syntax sugar for print(expr) -Out - = "<:" _* expr:Expr -{ - return createNode('identifier', { - name: 'print', - chain: [createNode('callChain', { args: [expr] })], - }); -} - -// attribute statement - -// Note: Attribute will be combined with def node when parsing is complete. -Attr - = "#[" _* name:NAME value:(_* @StaticLiteral)? _* "]" -{ - return createNode('attr', { - name: name, - value: value ?? createNode('bool', { value: true }) - }); -} - -// each statement - -Each - = "each" _* "(" "let" _+ varn:NAME _* ","? _* items:Expr ")" _* x:BlockOrStatement -{ - return createNode('each', { - var: varn, - items: items, - for: x, - }); -} - / "each" _+ "let" _+ varn:NAME _* ","? _* items:Expr _+ x:BlockOrStatement -{ - return createNode('each', { - var: varn, - items: items, - for: x, - }); -} - -// for statement - -For - = "for" _* "(" "let" _+ varn:NAME _* from_:("=" _* v:Expr { return v; })? ","? _* to:Expr ")" _* x:BlockOrStatement -{ - return createNode('for', { - var: varn, - from: from_ ?? createNode('num', { value: 0 }), - to: to, - for: x, - }); -} - / "for" _+ "let" _+ varn:NAME _* from_:("=" _* v:Expr { return v; })? ","? _* to:Expr _+ x:BlockOrStatement -{ - return createNode('for', { - var: varn, - from: from_ ?? createNode('num', { value: 0 }), - to: to, - for: x, - }); -} - / "for" _* "(" times:Expr ")" _* x:BlockOrStatement -{ - return createNode('for', { - times: times, - for: x, - }); -} - / "for" _+ times:Expr _+ x:BlockOrStatement -{ - return createNode('for', { - times: times, - for: x, - }); -} - -// return statement - -Return - = "return" ![A-Z0-9_:]i _* expr:Expr -{ return createNode('return', { expr }); } - -// loop statement - -Loop - = "loop" _* "{" _* s:Statements _* "}" -{ return createNode('loop', { statements: s }); } - -// break statement - -Break - = "break" ![A-Z0-9_:]i -{ return createNode('break', {}); } - -// continue statement - -Continue - = "continue" ![A-Z0-9_:]i -{ return createNode('continue', {}); } - -// assign statement - -Assign - = dest:Expr _* op:("+=" / "-=" / "=") _* expr:Expr -{ - if (op === '+=') - return createNode('addAssign', { dest, expr }); - else if (op === '-=') - return createNode('subAssign', { dest, expr }); - else - return createNode('assign', { dest, expr }); -} - - - -// -// expressions -------------------------------------------------------------------- -// - -// infix expression - -Infix - = head:Expr2 tail:(InfixSp* op:Op InfixSp* term:Expr2 { return {op, term}; })+ -{ - return createNode('infix', { - operands: [head, ...tail.map(i => i.term)], - operators: tail.map(i => i.op.value), - operatorLocs: tail.map(i => i.op.loc), - }); -} - -InfixSp - = "\\" LF - / __ - -Op - = ("||" / "&&" / "==" / "!=" / "<=" / ">=" / "<" / ">" / "+" / "-" / "*" / "^" / "/" / "%") -{ - const loc = location(); - return { - value: text(), - loc: { start: loc.start.offset, end: loc.end.offset - 1 }, - }; -} - -Not - = "!" expr:Expr -{ - return createNode('not', { - expr: expr, - }); -} - - -// chain - -Chain - = e:Expr3 chain:(CallChain / IndexChain / PropChain)+ -{ - if (e.chain) { - return { ...e, chain: [...e.chain, ...chain] }; - } else { - return { ...e, chain }; - } -} - -CallChain - = "(" _* args:CallArgs? _* ")" -{ return createNode('callChain', { args: args ?? [] }); } - -CallArgs - = head:Expr tails:(SEP expr:Expr { return expr; })* -{ return [head, ...tails]; } - -IndexChain - = "[" _* index:Expr _* "]" -{ return createNode('indexChain', { index }); } - -PropChain - = "." name:NAME -{ return createNode('propChain', { name }); } - -// if statement - -If - = "if" _+ cond:Expr _+ then:BlockOrStatement elseif:(_+ @ElseifBlocks)? elseBlock:(_+ @ElseBlock)? -{ - return createNode('if', { - cond: cond, - then: then, - elseif: elseif ?? [], - else: elseBlock - }); -} - -ElseifBlocks - = head:ElseifBlock tails:(_* @ElseifBlock)* -{ return [head, ...tails]; } - -ElseifBlock - = "elif" ![A-Z0-9_:]i _* cond:Expr _* then:BlockOrStatement -{ return { cond, then }; } - -ElseBlock - = "else" ![A-Z0-9_:]i _* then:BlockOrStatement -{ return then; } - -// match expression - -Match - = "match" ![A-Z0-9_:]i _* about:Expr _* "{" _* qs:(q:Expr _* "=>" _* a:BlockOrStatement _* { return { q, a }; })+ x:("*" _* "=>" _* @BlockOrStatement _*)? _* "}" -{ - return createNode('match', { - about: about, - qs: qs ?? [], - default: x - }); -} - -// eval expression - -Eval - = "eval" _* "{" _* s:Statements _* "}" -{ return createNode('block', { statements: s }); } - -// exists expression - -Exists - = "exists" _+ i:Identifier -{ return createNode('exists', { identifier: i }); } - -// variable reference expression - -Identifier - = name:NAME_WITH_NAMESPACE -{ return createNode('identifier', { name }); } - - - -// -// literals ------------------------------------------------------------------------------ -// - -// template literal - -Tmpl - = "`" items:(!"`" @TmplEmbed)* "`" -{ return createNode('tmpl', { tmpl: items }); } - -TmplEmbed - = "{" __* @expr:Expr __* "}" - / str:TmplAtom+ {return str.join("")} - -TmplAtom - = TmplEsc - / [^`{] - -TmplEsc - = "\\" @[{}`] - -// string literal - -Str - = "\"" value:(!"\"" c:(StrDoubleQuoteEsc / .) { return c; })* "\"" -{ return createNode('str', { value: value.join('') }); } - / "'" value:(!"'" c:(StrSingleQuoteEsc / .) { return c; })* "'" -{ return createNode('str', { value: value.join('') }); } - -StrDoubleQuoteEsc - = "\\\"" -{ return '"'; } - -StrSingleQuoteEsc - = "\\\'" -{ return '\''; } - -// number literal -Num - = Float - / Int - -Float - = [+-]? [1-9] [0-9]+ "." [0-9]+ - { return createNode('num', { value: parseFloat(text())}); } - / [+-]? [0-9] "." [0-9]+ - { return createNode('num', { value: parseFloat(text())}); } - -Int - = [+-]? [1-9] [0-9]+ -{ return createNode('num', { value: parseInt(text(), 10) }); } - / [+-]? [0-9] -{ return createNode('num', { value: parseInt(text(), 10) }); } - -// boolean literal - -Bool - = True - / False - -True - = "true" ![A-Z0-9_:]i -{ return createNode('bool', { value: true }); } - -False - = "false" ![A-Z0-9_:]i -{ return createNode('bool', { value: false }); } - -// null literal - -Null - = "null" ![A-Z0-9_:]i -{ return createNode('null', {}); } - -// object literal - -Obj - = "{" _* kvs:(k:NAME _* ":" _+ v:Expr _* ("," / ";")? _* { return { k, v }; })* "}" -{ - const obj = new Map(); - for (const kv of kvs) { - obj.set(kv.k, kv.v); - } - return createNode('obj', { value: obj }); -} - -// array literal - -Arr - = "[" _* items:(item:Expr _* ","? _* { return item; })* _* "]" -{ return createNode('arr', { value: items }); } - - - -// -// function ------------------------------------------------------------------------------ -// - -Arg - = name:NAME type:(_* ":" _* @Type)? -{ return { name, argType: type }; } - -Args - = head:Arg tails:(SEP @Arg)* -{ return [head, ...tails]; } - -// define function statement - -FnDef - = "@" s1:__* name:NAME s2:__* "(" _* args:Args? _* ")" ret:(_* ":" _* @Type)? _* "{" _* content:Statements? _* "}" -{ - if (s1.length > 0 || s2.length > 0) { - error('Cannot use spaces before or after the function name.'); - } - return createNode('def', { - name: name, - expr: createNode('fn', { args: args ?? [], retType: ret }, content ?? []), - mut: false, - attr: [] - }); -} - -// function expression - -Fn = "@(" _* args:Args? _* ")" ret:(_* ":" _* @Type)? _* "{" _* content:Statements? _* "}" -{ return createNode('fn', { args: args ?? [], retType: ret }, content ?? []); } - - - -// -// static literal ------------------------------------------------------------------------ -// - -// array literal (static) - -StaticArr - = "[" _* items:(item:StaticLiteral _* ","? _* { return item; })* _* "]" -{ return createNode('arr', { value: items }); } - -// object literal (static) - -StaticObj - = "{" _* kvs:(k:NAME _* ":" _+ v:StaticLiteral _* ("," / ";")? _* { return { k, v }; })* "}" -{ - const obj = new Map(); - for (const kv of kvs) { - obj.set(kv.k, kv.v); - } - return createNode('obj', { value: obj }); -} - - - -// -// type ---------------------------------------------------------------------------------- -// - -Type - = FnType - / NamedType - -FnType - = "@(" _* args:ArgTypes? _* ")" _* "=>" _* result:Type -{ return createNode('fnTypeSource', { args: args ?? [], result }); } - -ArgTypes - = head:Type tails:(SEP @Type)* -{ return [head, ...tails]; } - -NamedType - = name:NAME __* "<" __* inner:Type __* ">" -{ return createNode('namedTypeSource', { name, inner }); } - / name:NAME -{ return createNode('namedTypeSource', { name, inner: null }); } - - - -// -// general ------------------------------------------------------------------------------- -// - -NAME - = [A-Z_]i [A-Z0-9_]i* -{ return text(); } - -NAME_WITH_NAMESPACE - = NAME (":" NAME)* -{ return text(); } - -SEP - = _* "," _* - / _+ - -BlockOrStatement - = "{" _* s:Statements? _* "}" -{ return createNode('block', { statements: (s ?? []) }); } - / Statement - -LF - = "\r\n" / [\r\n] - -EOL - = !. / LF - -// spacing -_ - = [ \t\r\n] - -// spacing (no linebreaks) -__ - = [ \t] diff --git a/src/parser/plugins/infix-to-fncall.ts b/src/parser/plugins/infix-to-fncall.ts deleted file mode 100644 index d75dd4d2..00000000 --- a/src/parser/plugins/infix-to-fncall.ts +++ /dev/null @@ -1,163 +0,0 @@ -import { visitNode } from '../visit.js'; -import { AiScriptSyntaxError } from '../../error.js'; -import type * as Cst from '../node.js'; -import type { Loc } from '../../node.js'; - -/** - * 中置演算子式を表す木 - * 1 + 3 ならば次のようなイメージ - * ``` - * (+) - * (1) (3) - * ``` - */ -type InfixTree = { - type: 'infixTree'; - left: InfixTree | Cst.Node; - right: InfixTree | Cst.Node; - info: { - opLoc: Loc; - priority: number; // 優先度(高いほど優先して計算される値) - } & ({ - func: string; // 対応する関数名 - mapFn?: undefined; - } | { - func?: undefined; - mapFn: ((infix: InfixTree) => Cst.Node); //Nodeへ変換する関数 - }) -}; - -function INFIX_TREE(left: InfixTree | Cst.Node, right: InfixTree | Cst.Node, info: InfixTree['info']): InfixTree { - return { type: 'infixTree', left, right, info }; -} - -/** - * 現在の中置演算子式を表す木に新たな演算子と項を追加した木を構築する - * - * - 新しい演算子の優先度が現在見ている木の演算子の優先度 **以下** である場合は、現在見ている木は新しい演算子の左側の子になる。 - * 1 + 3 - 4 = (1 + 3) - 4 ならば - * ``` - * (-) - * (+) (4) - * (1) (3) - * ``` - * - * - 新しい演算子の優先度が現在見ている木の演算子の優先度 **より大きい** 場合は、右側の子と結合する。 - * 1 + 3 * 4 = 1 + (3 * 4) ならば - * ``` - * (+) - * (1) (*) - * (3) (4) - * ``` - * - * - TODO: 左結合性の場合しか考えていない(結合性によって優先度が同じ場合の振る舞いが変わりそう) - * - NOTE: 右結合性の演算子としては代入演算子などが挙げられる - * - NOTE: 比較の演算子などは非結合性とされる - */ -function insertTree(currTree: InfixTree | Cst.Node, nextTree: InfixTree | Cst.Node, nextOpInfo: InfixTree['info']): InfixTree { - if (currTree.type !== 'infixTree') { - return INFIX_TREE(currTree, nextTree, nextOpInfo); - } - - if (nextOpInfo.priority <= currTree.info.priority) { - return INFIX_TREE(currTree, nextTree, nextOpInfo); - } else { - const { left, right, info: currInfo } = currTree; - return INFIX_TREE(left, insertTree(right, nextTree, nextOpInfo), currInfo); - } -} - -/** - * 中置演算子式を表す木を対応する関数呼び出しの構造体に変換する - */ -function treeToNode(tree: InfixTree | Cst.Node): Cst.Node { - if (tree.type !== 'infixTree') { - return tree; - } - - if (tree.info.mapFn) { - return tree.info.mapFn(tree); - } else { - const left = treeToNode(tree.left); - const right = treeToNode(tree.right); - return { - type: 'call', - target: { type: 'identifier', name: tree.info.func, loc: tree.info.opLoc }, - args: [left, right], - loc: { start: left.loc!.start,end: right.loc!.end }, - } as Cst.Call; - } -} - -const infoTable: Record> = { - '*': { func: 'Core:mul', priority: 7 }, - '^': { func: 'Core:pow', priority: 7 }, - '/': { func: 'Core:div', priority: 7 }, - '%': { func: 'Core:mod', priority: 7 }, - '+': { func: 'Core:add', priority: 6 }, - '-': { func: 'Core:sub', priority: 6 }, - '==': { func: 'Core:eq', priority: 4 }, - '!=': { func: 'Core:neq', priority: 4 }, - '<': { func: 'Core:lt', priority: 4 }, - '>': { func: 'Core:gt', priority: 4 }, - '<=': { func: 'Core:lteq', priority: 4 }, - '>=': { func: 'Core:gteq', priority: 4 }, - '&&': { - mapFn: infix => { - const left = treeToNode(infix.left); - const right = treeToNode(infix.right); - return { - type: 'and', - left, - right, - loc: { start: left.loc!.start, end: right.loc!.end }, - operatorLoc: infix.info.opLoc, - } as Cst.And; - }, - priority: 3, - }, - '||': { - mapFn: infix => { - const left = treeToNode(infix.left); - const right = treeToNode(infix.right); - return { - type: 'or', - left, - right, - loc: { start: left.loc!.start, end: right.loc!.end }, - operatorLoc: infix.info.opLoc, - } as Cst.Or; - }, - priority: 3, - }, -}; - -/** - * NInfix を関数呼び出し形式に変換する - */ -function transform(node: Cst.Infix): Cst.Node { - const infos = node.operators.map((op, i) => { - const info = infoTable[op]; - if (info == null) { - throw new AiScriptSyntaxError(`No such operator: ${op}.`); - } - return { ...info, opLoc: node.operatorLocs[i] } as InfixTree['info']; - }); - let currTree = INFIX_TREE(node.operands[0]!, node.operands[1]!, infos[0]!); - for (let i = 0; i < infos.length - 1; i++) { - currTree = insertTree(currTree, node.operands[2 + i]!, infos[1 + i]!); - } - return treeToNode(currTree); -} - -export function infixToFnCall(nodes: Cst.Node[]): Cst.Node[] { - for (let i = 0; i < nodes.length; i++) { - nodes[i] = visitNode(nodes[i]!, (node) => { - if (node.type === 'infix') { - return transform(node); - } - return node; - }); - } - return nodes; -} diff --git a/src/parser/plugins/set-attribute.ts b/src/parser/plugins/set-attribute.ts deleted file mode 100644 index a19e754e..00000000 --- a/src/parser/plugins/set-attribute.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { AiScriptSyntaxError } from '../../error.js'; -import type * as Cst from '../node.js'; - -export function setAttribute(node: Cst.Expression[]): Cst.Expression[] -export function setAttribute(node: Cst.Statement[]): Cst.Statement[] -export function setAttribute(node: (Cst.Statement | Cst.Expression)[]): (Cst.Statement | Cst.Expression)[] -export function setAttribute(node: Cst.Node[]): Cst.Node[] -export function setAttribute(nodes: Cst.Node[]): Cst.Node[] { - const result: Cst.Node[] = []; - const stockedAttrs: Cst.Attribute[] = []; - - for (const node of nodes) { - if (node.type === 'attr') { - stockedAttrs.push(node); - } else if (node.type === 'def') { - if (node.attr == null) { - node.attr = []; - } - node.attr.push(...stockedAttrs); - // clear all - stockedAttrs.splice(0, stockedAttrs.length); - if (node.expr.type === 'fn') { - node.expr.children = setAttribute(node.expr.children); - } - result.push(node); - } else { - if (stockedAttrs.length > 0) { - throw new AiScriptSyntaxError('invalid attribute.'); - } - switch (node.type) { - case 'fn': { - node.children = setAttribute(node.children); - break; - } - case 'block': { - node.statements = setAttribute(node.statements); - break; - } - } - result.push(node); - } - } - if (stockedAttrs.length > 0) { - throw new AiScriptSyntaxError('invalid attribute.'); - } - - return result; -} diff --git a/src/parser/plugins/transform-chain.ts b/src/parser/plugins/transform-chain.ts deleted file mode 100644 index 528b9258..00000000 --- a/src/parser/plugins/transform-chain.ts +++ /dev/null @@ -1,39 +0,0 @@ -import * as Cst from '../node.js'; -import { visitNode } from '../visit.js'; - -function transformNode(node: Cst.Node): Cst.Node { - // chain - if (Cst.isExpression(node) && Cst.hasChainProp(node) && node.chain != null) { - const { chain, ...hostNode } = node; - let parent: Cst.Expression = hostNode; - for (const item of chain) { - switch (item.type) { - case 'callChain': { - parent = Cst.CALL(parent, item.args, item.loc); - break; - } - case 'indexChain': { - parent = Cst.INDEX(parent, item.index, item.loc); - break; - } - case 'propChain': { - parent = Cst.PROP(parent, item.name, item.loc); - break; - } - default: { - break; - } - } - } - return parent; - } - - return node; -} - -export function transformChain(nodes: Cst.Node[]): Cst.Node[] { - for (let i = 0; i < nodes.length; i++) { - nodes[i] = visitNode(nodes[i]!, transformNode); - } - return nodes; -} diff --git a/src/parser/plugins/validate-keyword.ts b/src/parser/plugins/validate-keyword.ts index 3e9af586..d44da8f9 100644 --- a/src/parser/plugins/validate-keyword.ts +++ b/src/parser/plugins/validate-keyword.ts @@ -1,73 +1,103 @@ import { AiScriptSyntaxError } from '../../error.js'; import { visitNode } from '../visit.js'; -import type * as Cst from '../node.js'; +import type * as Ast from '../../node.js'; -const reservedWord = [ - 'null', - 'true', - 'false', - 'each', - 'for', - 'loop', - 'break', - 'continue', - 'match', - 'if', - 'elif', - 'else', - 'return', - 'eval', - 'var', - 'let', - 'exists', +// 予約語となっている識別子があるかを確認する。 +// - キーワードは字句解析の段階でそれぞれのKeywordトークンとなるため除外 +// - 文脈キーワードは識別子に利用できるため除外 - // future - 'fn', - 'namespace', - 'meta', +const reservedWord = [ + 'as', + 'async', 'attr', 'attribute', - 'static', + 'await', + 'catch', 'class', - 'struct', - 'module', - 'while', - 'import', - 'export', // 'const', + 'component', + 'constructor', // 'def', + 'dictionary', + 'enum', + 'export', + 'finally', + 'fn', // 'func', // 'function', - // 'ref', - // 'out', + 'hash', + 'in', + 'interface', + 'out', + 'private', + 'public', + 'ref', + 'static', + 'struct', + 'table', + 'this', + 'throw', + 'trait', + 'try', + 'undefined', + 'use', + 'using', + 'when', + 'yield', + 'import', + 'is', + 'meta', + 'module', + 'namespace', + 'new', ]; -function throwReservedWordError(name: string): void { - throw new AiScriptSyntaxError(`Reserved word "${name}" cannot be used as variable name.`); +function throwReservedWordError(name: string, loc: Ast.Loc): void { + throw new AiScriptSyntaxError(`Reserved word "${name}" cannot be used as variable name.`, loc.start); } -function validateNode(node: Cst.Node): Cst.Node { +function validateNode(node: Ast.Node): Ast.Node { switch (node.type) { + case 'ns': case 'def': case 'attr': - case 'ns': case 'identifier': - case 'propChain': { + case 'prop': { if (reservedWord.includes(node.name)) { - throwReservedWordError(node.name); + throwReservedWordError(node.name, node.loc); } break; } case 'meta': { if (node.name != null && reservedWord.includes(node.name)) { - throwReservedWordError(node.name); + throwReservedWordError(node.name, node.loc); + } + break; + } + case 'each': { + if (reservedWord.includes(node.var)) { + throwReservedWordError(node.var, node.loc); + } + break; + } + case 'for': { + if (node.var != null && reservedWord.includes(node.var)) { + throwReservedWordError(node.var, node.loc); } break; } case 'fn': { for (const arg of node.args) { if (reservedWord.includes(arg.name)) { - throwReservedWordError(arg.name); + throwReservedWordError(arg.name, node.loc); + } + } + break; + } + case 'obj': { + for (const name of node.value.keys()) { + if (reservedWord.includes(name)) { + throwReservedWordError(name, node.loc); } } break; @@ -77,7 +107,7 @@ function validateNode(node: Cst.Node): Cst.Node { return node; } -export function validateKeyword(nodes: Cst.Node[]): Cst.Node[] { +export function validateKeyword(nodes: Ast.Node[]): Ast.Node[] { for (const inner of nodes) { visitNode(inner, validateNode); } diff --git a/src/parser/plugins/validate-type.ts b/src/parser/plugins/validate-type.ts index 08d5addf..3dbc0cf3 100644 --- a/src/parser/plugins/validate-type.ts +++ b/src/parser/plugins/validate-type.ts @@ -1,8 +1,8 @@ import { getTypeBySource } from '../../type.js'; import { visitNode } from '../visit.js'; -import type * as Cst from '../node.js'; +import type * as Ast from '../../node.js'; -function validateNode(node: Cst.Node): Cst.Node { +function validateNode(node: Ast.Node): Ast.Node { switch (node.type) { case 'def': { if (node.varType != null) { @@ -26,7 +26,7 @@ function validateNode(node: Cst.Node): Cst.Node { return node; } -export function validateType(nodes: Cst.Node[]): Cst.Node[] { +export function validateType(nodes: Ast.Node[]): Ast.Node[] { for (const node of nodes) { visitNode(node, validateNode); } diff --git a/src/parser/scanner.ts b/src/parser/scanner.ts new file mode 100644 index 00000000..4feb5ee7 --- /dev/null +++ b/src/parser/scanner.ts @@ -0,0 +1,627 @@ +import { AiScriptSyntaxError } from '../error.js'; +import { CharStream } from './streams/char-stream.js'; +import { TOKEN, TokenKind } from './token.js'; + +import type { ITokenStream } from './streams/token-stream.js'; +import type { Token, TokenPosition } from './token.js'; + +const spaceChars = [' ', '\t']; +const lineBreakChars = ['\r', '\n']; +const digit = /^[0-9]$/; +const wordChar = /^[A-Za-z0-9_]$/; + +/** + * 入力文字列からトークンを読み取るクラス +*/ +export class Scanner implements ITokenStream { + private stream: CharStream; + private _tokens: Token[] = []; + + constructor(source: string) + constructor(stream: CharStream) + constructor(x: string | CharStream) { + if (typeof x === 'string') { + this.stream = new CharStream(x); + } else { + this.stream = x; + } + this._tokens.push(this.readToken()); + } + + /** + * カーソル位置にあるトークンを取得します。 + */ + public get token(): Token { + return this._tokens[0]!; + } + + /** + * カーソル位置にあるトークンの種類を取得します。 + */ + public getKind(): TokenKind { + return this.token.kind; + } + + /** + * カーソル位置にあるトークンの位置情報を取得します。 + */ + public getPos(): TokenPosition { + return this.token.pos; + } + + /** + * カーソル位置を次のトークンへ進めます。 + */ + public next(): void { + // 現在のトークンがEOFだったら次のトークンに進まない + if (this._tokens[0]!.kind === TokenKind.EOF) { + return; + } + + this._tokens.shift(); + + if (this._tokens.length === 0) { + this._tokens.push(this.readToken()); + } + } + + /** + * トークンの先読みを行います。カーソル位置は移動されません。 + */ + public lookahead(offset: number): Token { + while (this._tokens.length <= offset) { + this._tokens.push(this.readToken()); + } + + return this._tokens[offset]!; + } + + /** + * カーソル位置にあるトークンが指定したトークンの種類と一致するかを確認します。 + * 一致しなかった場合には文法エラーを発生させます。 + */ + public expect(kind: TokenKind): void { + if (this.getKind() !== kind) { + throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[this.getKind()]}`, this.getPos()); + } + } + + /** + * カーソル位置にあるトークンが指定したトークンの種類と一致することを確認し、 + * カーソル位置を次のトークンへ進めます。 + */ + public nextWith(kind: TokenKind): void { + this.expect(kind); + this.next(); + } + + private readToken(): Token { + let token; + let hasLeftSpacing = false; + + while (true) { + if (this.stream.eof) { + token = TOKEN(TokenKind.EOF, this.stream.getPos(), { hasLeftSpacing }); + break; + } + // skip spasing + if (spaceChars.includes(this.stream.char)) { + this.stream.next(); + hasLeftSpacing = true; + continue; + } + + // トークン位置を記憶 + const pos = this.stream.getPos(); + + if (lineBreakChars.includes(this.stream.char)) { + this.stream.next(); + token = TOKEN(TokenKind.NewLine, pos, { hasLeftSpacing }); + return token; + } + switch (this.stream.char) { + case '!': { + this.stream.next(); + if (!this.stream.eof && (this.stream.char as string) === '=') { + this.stream.next(); + token = TOKEN(TokenKind.NotEq, pos, { hasLeftSpacing }); + } else { + token = TOKEN(TokenKind.Not, pos, { hasLeftSpacing }); + } + break; + } + case '"': + case '\'': { + token = this.readStringLiteral(hasLeftSpacing); + break; + } + case '#': { + this.stream.next(); + if (!this.stream.eof && (this.stream.char as string) === '#') { + this.stream.next(); + if (!this.stream.eof && (this.stream.char as string) === '#') { + this.stream.next(); + token = TOKEN(TokenKind.Sharp3, pos, { hasLeftSpacing }); + } + } else if (!this.stream.eof && (this.stream.char as string) === '[') { + this.stream.next(); + token = TOKEN(TokenKind.OpenSharpBracket, pos, { hasLeftSpacing }); + } else { + throw new AiScriptSyntaxError('invalid character: "#"', pos); + } + break; + } + case '%': { + this.stream.next(); + token = TOKEN(TokenKind.Percent, pos, { hasLeftSpacing }); + break; + } + case '&': { + this.stream.next(); + if (!this.stream.eof && (this.stream.char as string) === '&') { + this.stream.next(); + token = TOKEN(TokenKind.And2, pos, { hasLeftSpacing }); + } + break; + } + case '(': { + this.stream.next(); + token = TOKEN(TokenKind.OpenParen, pos, { hasLeftSpacing }); + break; + } + case ')': { + this.stream.next(); + token = TOKEN(TokenKind.CloseParen, pos, { hasLeftSpacing }); + break; + } + case '*': { + this.stream.next(); + token = TOKEN(TokenKind.Asterisk, pos, { hasLeftSpacing }); + break; + } + case '+': { + this.stream.next(); + if (!this.stream.eof && (this.stream.char as string) === '=') { + this.stream.next(); + token = TOKEN(TokenKind.PlusEq, pos, { hasLeftSpacing }); + } else { + token = TOKEN(TokenKind.Plus, pos, { hasLeftSpacing }); + } + break; + } + case ',': { + this.stream.next(); + token = TOKEN(TokenKind.Comma, pos, { hasLeftSpacing }); + break; + } + case '-': { + this.stream.next(); + if (!this.stream.eof && (this.stream.char as string) === '=') { + this.stream.next(); + token = TOKEN(TokenKind.MinusEq, pos, { hasLeftSpacing }); + } else { + token = TOKEN(TokenKind.Minus, pos, { hasLeftSpacing }); + } + break; + } + case '.': { + this.stream.next(); + token = TOKEN(TokenKind.Dot, pos, { hasLeftSpacing }); + break; + } + case '/': { + this.stream.next(); + if (!this.stream.eof && (this.stream.char as string) === '*') { + this.stream.next(); + this.skipCommentRange(); + continue; + } else if (!this.stream.eof && (this.stream.char as string) === '/') { + this.stream.next(); + this.skipCommentLine(); + continue; + } else { + token = TOKEN(TokenKind.Slash, pos, { hasLeftSpacing }); + } + break; + } + case ':': { + this.stream.next(); + if (!this.stream.eof && (this.stream.char as string) === ':') { + this.stream.next(); + token = TOKEN(TokenKind.Colon2, pos, { hasLeftSpacing }); + } else { + token = TOKEN(TokenKind.Colon, pos, { hasLeftSpacing }); + } + break; + } + case ';': { + this.stream.next(); + token = TOKEN(TokenKind.SemiColon, pos, { hasLeftSpacing }); + break; + } + case '<': { + this.stream.next(); + if (!this.stream.eof && (this.stream.char as string) === '=') { + this.stream.next(); + token = TOKEN(TokenKind.LtEq, pos, { hasLeftSpacing }); + } else if (!this.stream.eof && (this.stream.char as string) === ':') { + this.stream.next(); + token = TOKEN(TokenKind.Out, pos, { hasLeftSpacing }); + } else { + token = TOKEN(TokenKind.Lt, pos, { hasLeftSpacing }); + } + break; + } + case '=': { + this.stream.next(); + if (!this.stream.eof && (this.stream.char as string) === '=') { + this.stream.next(); + token = TOKEN(TokenKind.Eq2, pos, { hasLeftSpacing }); + } else if (!this.stream.eof && (this.stream.char as string) === '>') { + this.stream.next(); + token = TOKEN(TokenKind.Arrow, pos, { hasLeftSpacing }); + } else { + token = TOKEN(TokenKind.Eq, pos, { hasLeftSpacing }); + } + break; + } + case '>': { + this.stream.next(); + if (!this.stream.eof && (this.stream.char as string) === '=') { + this.stream.next(); + token = TOKEN(TokenKind.GtEq, pos, { hasLeftSpacing }); + } else { + token = TOKEN(TokenKind.Gt, pos, { hasLeftSpacing }); + } + break; + } + case '?': { + this.stream.next(); + token = TOKEN(TokenKind.Question, pos, { hasLeftSpacing }); + break; + } + case '@': { + this.stream.next(); + token = TOKEN(TokenKind.At, pos, { hasLeftSpacing }); + break; + } + case '[': { + this.stream.next(); + token = TOKEN(TokenKind.OpenBracket, pos, { hasLeftSpacing }); + break; + } + case '\\': { + this.stream.next(); + token = TOKEN(TokenKind.BackSlash, pos, { hasLeftSpacing }); + break; + } + case ']': { + this.stream.next(); + token = TOKEN(TokenKind.CloseBracket, pos, { hasLeftSpacing }); + break; + } + case '^': { + this.stream.next(); + token = TOKEN(TokenKind.Hat, pos, { hasLeftSpacing }); + break; + } + case '`': { + token = this.readTemplate(hasLeftSpacing); + break; + } + case '{': { + this.stream.next(); + token = TOKEN(TokenKind.OpenBrace, pos, { hasLeftSpacing }); + break; + } + case '|': { + this.stream.next(); + if (!this.stream.eof && (this.stream.char as string) === '|') { + this.stream.next(); + token = TOKEN(TokenKind.Or2, pos, { hasLeftSpacing }); + } + break; + } + case '}': { + this.stream.next(); + token = TOKEN(TokenKind.CloseBrace, pos, { hasLeftSpacing }); + break; + } + } + if (token == null) { + const digitToken = this.tryReadDigits(hasLeftSpacing); + if (digitToken) { + token = digitToken; + break; + } + const wordToken = this.tryReadWord(hasLeftSpacing); + if (wordToken) { + token = wordToken; + break; + } + throw new AiScriptSyntaxError(`invalid character: "${this.stream.char}"`, pos); + } + break; + } + return token; + } + + private tryReadWord(hasLeftSpacing: boolean): Token | undefined { + // read a word + let value = ''; + + const pos = this.stream.getPos(); + + while (!this.stream.eof && wordChar.test(this.stream.char)) { + value += this.stream.char; + this.stream.next(); + } + if (value.length === 0) { + return; + } + // check word kind + switch (value) { + case 'null': { + return TOKEN(TokenKind.NullKeyword, pos, { hasLeftSpacing }); + } + case 'true': { + return TOKEN(TokenKind.TrueKeyword, pos, { hasLeftSpacing }); + } + case 'false': { + return TOKEN(TokenKind.FalseKeyword, pos, { hasLeftSpacing }); + } + case 'each': { + return TOKEN(TokenKind.EachKeyword, pos, { hasLeftSpacing }); + } + case 'for': { + return TOKEN(TokenKind.ForKeyword, pos, { hasLeftSpacing }); + } + case 'loop': { + return TOKEN(TokenKind.LoopKeyword, pos, { hasLeftSpacing }); + } + case 'do': { + return TOKEN(TokenKind.DoKeyword, pos, { hasLeftSpacing }); + } + case 'while': { + return TOKEN(TokenKind.WhileKeyword, pos, { hasLeftSpacing }); + } + case 'break': { + return TOKEN(TokenKind.BreakKeyword, pos, { hasLeftSpacing }); + } + case 'continue': { + return TOKEN(TokenKind.ContinueKeyword, pos, { hasLeftSpacing }); + } + case 'match': { + return TOKEN(TokenKind.MatchKeyword, pos, { hasLeftSpacing }); + } + case 'case': { + return TOKEN(TokenKind.CaseKeyword, pos, { hasLeftSpacing }); + } + case 'default': { + return TOKEN(TokenKind.DefaultKeyword, pos, { hasLeftSpacing }); + } + case 'if': { + return TOKEN(TokenKind.IfKeyword, pos, { hasLeftSpacing }); + } + case 'elif': { + return TOKEN(TokenKind.ElifKeyword, pos, { hasLeftSpacing }); + } + case 'else': { + return TOKEN(TokenKind.ElseKeyword, pos, { hasLeftSpacing }); + } + case 'return': { + return TOKEN(TokenKind.ReturnKeyword, pos, { hasLeftSpacing }); + } + case 'eval': { + return TOKEN(TokenKind.EvalKeyword, pos, { hasLeftSpacing }); + } + case 'var': { + return TOKEN(TokenKind.VarKeyword, pos, { hasLeftSpacing }); + } + case 'let': { + return TOKEN(TokenKind.LetKeyword, pos, { hasLeftSpacing }); + } + case 'exists': { + return TOKEN(TokenKind.ExistsKeyword, pos, { hasLeftSpacing }); + } + default: { + return TOKEN(TokenKind.Identifier, pos, { hasLeftSpacing, value }); + } + } + } + + private tryReadDigits(hasLeftSpacing: boolean): Token | undefined { + let wholeNumber = ''; + let fractional = ''; + + const pos = this.stream.getPos(); + + while (!this.stream.eof && digit.test(this.stream.char)) { + wholeNumber += this.stream.char; + this.stream.next(); + } + if (wholeNumber.length === 0) { + return; + } + if (!this.stream.eof && this.stream.char === '.') { + this.stream.next(); + while (!this.stream.eof as boolean && digit.test(this.stream.char as string)) { + fractional += this.stream.char; + this.stream.next(); + } + if (fractional.length === 0) { + throw new AiScriptSyntaxError('digit expected', pos); + } + } + let value; + if (fractional.length > 0) { + value = wholeNumber + '.' + fractional; + } else { + value = wholeNumber; + } + return TOKEN(TokenKind.NumberLiteral, pos, { hasLeftSpacing, value }); + } + + private readStringLiteral(hasLeftSpacing: boolean): Token { + let value = ''; + const literalMark = this.stream.char; + let state: 'string' | 'escape' | 'finish' = 'string'; + + const pos = this.stream.getPos(); + this.stream.next(); + + while (state !== 'finish') { + switch (state) { + case 'string': { + if (this.stream.eof) { + throw new AiScriptSyntaxError('unexpected EOF', pos); + } + if (this.stream.char === '\\') { + this.stream.next(); + state = 'escape'; + break; + } + if (this.stream.char === literalMark) { + this.stream.next(); + state = 'finish'; + break; + } + value += this.stream.char; + this.stream.next(); + break; + } + case 'escape': { + if (this.stream.eof) { + throw new AiScriptSyntaxError('unexpected EOF', pos); + } + value += this.stream.char; + this.stream.next(); + state = 'string'; + break; + } + } + } + return TOKEN(TokenKind.StringLiteral, pos, { hasLeftSpacing, value }); + } + + private readTemplate(hasLeftSpacing: boolean): Token { + const elements: Token[] = []; + let buf = ''; + let tokenBuf: Token[] = []; + let state: 'string' | 'escape' | 'expr' | 'finish' = 'string'; + + const pos = this.stream.getPos(); + let elementPos = pos; + this.stream.next(); + + while (state !== 'finish') { + switch (state) { + case 'string': { + // テンプレートの終了が無いままEOFに達した + if (this.stream.eof) { + throw new AiScriptSyntaxError('unexpected EOF', pos); + } + // エスケープ + if (this.stream.char === '\\') { + this.stream.next(); + state = 'escape'; + break; + } + // テンプレートの終了 + if (this.stream.char === '`') { + this.stream.next(); + if (buf.length > 0) { + elements.push(TOKEN(TokenKind.TemplateStringElement, elementPos, { hasLeftSpacing, value: buf })); + } + state = 'finish'; + break; + } + // 埋め込み式の開始 + if (this.stream.char === '{') { + this.stream.next(); + if (buf.length > 0) { + elements.push(TOKEN(TokenKind.TemplateStringElement, elementPos, { hasLeftSpacing, value: buf })); + buf = ''; + } + // ここから式エレメントになるので位置を更新 + elementPos = this.stream.getPos(); + state = 'expr'; + break; + } + buf += this.stream.char; + this.stream.next(); + break; + } + case 'escape': { + // エスケープ対象の文字が無いままEOFに達した + if (this.stream.eof) { + throw new AiScriptSyntaxError('unexpected EOF', pos); + } + // 普通の文字として取り込み + buf += this.stream.char; + this.stream.next(); + // 通常の文字列に戻る + state = 'string'; + break; + } + case 'expr': { + // 埋め込み式の終端記号が無いままEOFに達した + if (this.stream.eof) { + throw new AiScriptSyntaxError('unexpected EOF', pos); + } + // skip spasing + if (spaceChars.includes(this.stream.char)) { + this.stream.next(); + continue; + } + // 埋め込み式の終了 + if ((this.stream.char as string) === '}') { + elements.push(TOKEN(TokenKind.TemplateExprElement, elementPos, { hasLeftSpacing, children: tokenBuf })); + // ここから文字列エレメントになるので位置を更新 + elementPos = this.stream.getPos(); + // TemplateExprElementトークンの終了位置をTokenStreamが取得するためのEOFトークンを追加 + tokenBuf.push(TOKEN(TokenKind.EOF, elementPos)); + tokenBuf = []; + state = 'string'; + this.stream.next(); + break; + } + const token = this.readToken(); + tokenBuf.push(token); + break; + } + } + } + + return TOKEN(TokenKind.Template, pos, { hasLeftSpacing, children: elements }); + } + + private skipCommentLine(): void { + while (true) { + if (this.stream.eof) { + break; + } + if (this.stream.char === '\n') { + break; + } + this.stream.next(); + } + } + + private skipCommentRange(): void { + while (true) { + if (this.stream.eof) { + break; + } + if (this.stream.char === '*') { + this.stream.next(); + if ((this.stream.char as string) === '/') { + this.stream.next(); + break; + } + continue; + } + this.stream.next(); + } + } +} diff --git a/src/parser/streams/char-stream.ts b/src/parser/streams/char-stream.ts new file mode 100644 index 00000000..58b36793 --- /dev/null +++ b/src/parser/streams/char-stream.ts @@ -0,0 +1,139 @@ +/** + * 入力文字列から文字を読み取るクラス +*/ +export class CharStream { + private pages: Map; + private firstPageIndex: number; + private lastPageIndex: number; + private pageIndex: number; + private address: number; + private _char?: string; + /** zero-based number */ + private line: number; + /** zero-based number */ + private column: number; + + constructor(source: string, opts?: { line?: number, column?: number }) { + this.pages = new Map(); + this.pages.set(0, source); + this.firstPageIndex = 0; + this.lastPageIndex = 0; + this.pageIndex = 0; + this.address = 0; + this.line = opts?.line ?? 0; + this.column = opts?.column ?? 0; + this.moveNext(); + } + + /** + * ストリームの終わりに達しているかどうかを取得します。 + */ + public get eof(): boolean { + return this.endOfPage && this.isLastPage; + } + + /** + * カーソル位置にある文字を取得します。 + */ + public get char(): string { + if (this.eof) { + throw new Error('end of stream'); + } + return this._char!; + } + + /** + * カーソル位置に対応するソースコード上の行番号と列番号を取得します。 + */ + public getPos(): { line: number, column: number } { + return { + line: (this.line + 1), + column: (this.column + 1), + }; + } + + /** + * カーソル位置を次の文字へ進めます。 + */ + public next(): void { + if (!this.eof && this._char === '\n') { + this.line++; + this.column = 0; + } else { + this.column++; + } + this.incAddr(); + this.moveNext(); + } + + /** + * カーソル位置を前の文字へ戻します。 + */ + public prev(): void { + this.decAddr(); + this.movePrev(); + } + + private get isFirstPage(): boolean { + return (this.pageIndex <= this.firstPageIndex); + } + + private get isLastPage(): boolean { + return (this.pageIndex >= this.lastPageIndex); + } + + private get endOfPage(): boolean { + const page = this.pages.get(this.pageIndex)!; + return (this.address >= page.length); + } + + private moveNext(): void { + this.loadChar(); + while (true) { + if (!this.eof && this._char === '\r') { + this.incAddr(); + this.loadChar(); + continue; + } + break; + } + } + + private incAddr(): void { + if (!this.endOfPage) { + this.address++; + } else if (!this.isLastPage) { + this.pageIndex++; + this.address = 0; + } + } + + private movePrev(): void { + this.loadChar(); + while (true) { + if (!this.eof && this._char === '\r') { + this.decAddr(); + this.loadChar(); + continue; + } + break; + } + } + + private decAddr(): void { + if (this.address > 0) { + this.address--; + } else if (!this.isFirstPage) { + this.pageIndex--; + this.address = this.pages.get(this.pageIndex)!.length - 1; + } + } + + private loadChar(): void { + if (this.eof) { + this._char = undefined; + } else { + this._char = this.pages.get(this.pageIndex)![this.address]!; + } + } +} diff --git a/src/parser/streams/token-stream.ts b/src/parser/streams/token-stream.ts new file mode 100644 index 00000000..d6d3f68b --- /dev/null +++ b/src/parser/streams/token-stream.ts @@ -0,0 +1,136 @@ +import { AiScriptSyntaxError } from '../../error.js'; +import { TOKEN, TokenKind } from '../token.js'; +import type { Token, TokenPosition } from '../token.js'; + +/** + * トークンの読み取りに関するインターフェース +*/ +export interface ITokenStream { + /** + * カーソル位置にあるトークンを取得します。 + */ + get token(): Token; + + /** + * カーソル位置にあるトークンの種類を取得します。 + */ + getKind(): TokenKind; + + /** + * カーソル位置にあるトークンの位置情報を取得します。 + */ + getPos(): TokenPosition; + + /** + * カーソル位置を次のトークンへ進めます。 + */ + next(): void; + + /** + * トークンの先読みを行います。カーソル位置は移動されません。 + */ + lookahead(offset: number): Token; + + /** + * カーソル位置にあるトークンが指定したトークンの種類と一致するかを確認します。 + * 一致しなかった場合には文法エラーを発生させます。 + */ + expect(kind: TokenKind): void; + + /** + * カーソル位置にあるトークンが指定したトークンの種類と一致することを確認し、 + * カーソル位置を次のトークンへ進めます。 + */ + nextWith(kind: TokenKind): void; +} + +/** + * トークン列からトークンを読み取るクラス +*/ +export class TokenStream implements ITokenStream { + private source: Token[]; + private index: number; + private _token: Token; + + constructor(source: TokenStream['source']) { + this.source = source; + this.index = 0; + this.load(); + } + + private get eof(): boolean { + return (this.index >= this.source.length); + } + + /** + * カーソル位置にあるトークンを取得します。 + */ + public get token(): Token { + if (this.eof) { + return TOKEN(TokenKind.EOF, { line: -1, column: -1 }); + } + return this._token; + } + + /** + * カーソル位置にあるトークンの種類を取得します。 + */ + public getKind(): TokenKind { + return this.token.kind; + } + + /** + * カーソル位置にあるトークンの位置情報を取得します。 + */ + public getPos(): TokenPosition { + return this.token.pos; + } + + /** + * カーソル位置を次のトークンへ進めます。 + */ + public next(): void { + if (!this.eof) { + this.index++; + } + this.load(); + } + + /** + * トークンの先読みを行います。カーソル位置は移動されません。 + */ + public lookahead(offset: number): Token { + if (this.index + offset < this.source.length) { + return this.source[this.index + offset]!; + } else { + return TOKEN(TokenKind.EOF, { line: -1, column: -1 }); + } + } + + /** + * カーソル位置にあるトークンが指定したトークンの種類と一致するかを確認します。 + * 一致しなかった場合には文法エラーを発生させます。 + */ + public expect(kind: TokenKind): void { + if (this.getKind() !== kind) { + throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[this.getKind()]}`, this.getPos()); + } + } + + /** + * カーソル位置にあるトークンが指定したトークンの種類と一致することを確認し、 + * カーソル位置を次のトークンへ進めます。 + */ + public nextWith(kind: TokenKind): void { + this.expect(kind); + this.next(); + } + + private load(): void { + if (this.eof) { + this._token = TOKEN(TokenKind.EOF, { line: -1, column: -1 }); + } else { + this._token = this.source[this.index]!; + } + } +} diff --git a/src/parser/syntaxes/common.ts b/src/parser/syntaxes/common.ts new file mode 100644 index 00000000..faf5cc5e --- /dev/null +++ b/src/parser/syntaxes/common.ts @@ -0,0 +1,182 @@ +import { TokenKind } from '../token.js'; +import { AiScriptSyntaxError } from '../../error.js'; +import { NODE } from '../utils.js'; +import { parseStatement } from './statements.js'; +import { parseExpr } from './expressions.js'; + +import type { ITokenStream } from '../streams/token-stream.js'; +import type * as Ast from '../../node.js'; + +/** + * ```abnf + * Params = "(" [IDENT [":" Type] *(SEP IDENT [":" Type])] ")" + * ``` +*/ +export function parseParams(s: ITokenStream): { name: string, argType?: Ast.Node }[] { + const items: { name: string, optional?: boolean, default?: Ast.Node, argType?: Ast.Node }[] = []; + + s.nextWith(TokenKind.OpenParen); + + if (s.getKind() === TokenKind.NewLine) { + s.next(); + } + + while (s.getKind() !== TokenKind.CloseParen) { + s.expect(TokenKind.Identifier); + const name = s.token.value!; + s.next(); + + let optional = false; + let defaultExpr; + if ((s.getKind() as TokenKind) === TokenKind.Question) { + s.next(); + optional = true; + } else if ((s.getKind() as TokenKind) === TokenKind.Eq) { + s.next(); + defaultExpr = parseExpr(s, false); + } + let type; + if (s.getKind() === TokenKind.Colon) { + s.next(); + type = parseType(s); + } + + items.push({ name, optional, default: defaultExpr, argType: type }); + + // separator + switch (s.getKind()) { + case TokenKind.NewLine: { + s.next(); + break; + } + case TokenKind.Comma: { + s.next(); + if (s.getKind() === TokenKind.NewLine) { + s.next(); + } + break; + } + case TokenKind.CloseParen: { + break; + } + default: { + throw new AiScriptSyntaxError('separator expected', s.getPos()); + } + } + } + + s.nextWith(TokenKind.CloseParen); + + return items; +} + +/** + * ```abnf + * Block = "{" *Statement "}" + * ``` +*/ +export function parseBlock(s: ITokenStream): Ast.Node[] { + s.nextWith(TokenKind.OpenBrace); + + while (s.getKind() === TokenKind.NewLine) { + s.next(); + } + + const steps: Ast.Node[] = []; + while (s.getKind() !== TokenKind.CloseBrace) { + steps.push(parseStatement(s)); + + // terminator + switch (s.getKind()) { + case TokenKind.NewLine: + case TokenKind.SemiColon: { + while ([TokenKind.NewLine, TokenKind.SemiColon].includes(s.getKind())) { + s.next(); + } + break; + } + case TokenKind.CloseBrace: { + break; + } + default: { + throw new AiScriptSyntaxError('Multiple statements cannot be placed on a single line.', s.getPos()); + } + } + } + + s.nextWith(TokenKind.CloseBrace); + + return steps; +} + +//#region Type + +export function parseType(s: ITokenStream): Ast.Node { + if (s.getKind() === TokenKind.At) { + return parseFnType(s); + } else { + return parseNamedType(s); + } +} + +/** + * ```abnf + * FnType = "@" "(" ParamTypes ")" "=>" Type + * ParamTypes = [Type *(SEP Type)] + * ``` +*/ +function parseFnType(s: ITokenStream): Ast.Node { + const startPos = s.getPos(); + + s.nextWith(TokenKind.At); + s.nextWith(TokenKind.OpenParen); + + const params: Ast.Node[] = []; + while (s.getKind() !== TokenKind.CloseParen) { + if (params.length > 0) { + switch (s.getKind()) { + case TokenKind.Comma: { + s.next(); + break; + } + default: { + throw new AiScriptSyntaxError('separator expected', s.getPos()); + } + } + } + const type = parseType(s); + params.push(type); + } + + s.nextWith(TokenKind.CloseParen); + s.nextWith(TokenKind.Arrow); + + const resultType = parseType(s); + + return NODE('fnTypeSource', { args: params, result: resultType }, startPos, s.getPos()); +} + +/** + * ```abnf + * NamedType = IDENT ["<" Type ">"] + * ``` +*/ +function parseNamedType(s: ITokenStream): Ast.Node { + const startPos = s.getPos(); + + s.expect(TokenKind.Identifier); + const name = s.token.value!; + s.next(); + + // inner type + let inner = null; + if (s.getKind() === TokenKind.Lt) { + s.next(); + inner = parseType(s); + s.nextWith(TokenKind.Gt); + } + + return NODE('namedTypeSource', { name, inner }, startPos, s.getPos()); +} + +//#endregion Type diff --git a/src/parser/syntaxes/expressions.ts b/src/parser/syntaxes/expressions.ts new file mode 100644 index 00000000..9fd0b893 --- /dev/null +++ b/src/parser/syntaxes/expressions.ts @@ -0,0 +1,691 @@ +import { AiScriptSyntaxError } from '../../error.js'; +import { CALL_NODE, NODE } from '../utils.js'; +import { TokenStream } from '../streams/token-stream.js'; +import { TokenKind } from '../token.js'; +import { parseBlock, parseParams, parseType } from './common.js'; +import { parseBlockOrStatement } from './statements.js'; + +import type * as Ast from '../../node.js'; +import type { ITokenStream } from '../streams/token-stream.js'; + +export function parseExpr(s: ITokenStream, isStatic: boolean): Ast.Node { + if (isStatic) { + return parseAtom(s, true); + } else { + return parsePratt(s, 0); + } +} + +// NOTE: infix(中置演算子)ではlbpを大きくすると右結合、rbpを大きくすると左結合の演算子になります。 +// この値は演算子が左と右に対してどのくらい結合力があるかを表わしています。詳細はpratt parsingの説明ページを参照してください。 + +const operators: OpInfo[] = [ + { opKind: 'postfix', kind: TokenKind.OpenParen, bp: 20 }, + { opKind: 'postfix', kind: TokenKind.OpenBracket, bp: 20 }, + + { opKind: 'infix', kind: TokenKind.Dot, lbp: 18, rbp: 19 }, + + { opKind: 'infix', kind: TokenKind.Hat, lbp: 17, rbp: 16 }, + + { opKind: 'prefix', kind: TokenKind.Plus, bp: 14 }, + { opKind: 'prefix', kind: TokenKind.Minus, bp: 14 }, + { opKind: 'prefix', kind: TokenKind.Not, bp: 14 }, + + { opKind: 'infix', kind: TokenKind.Asterisk, lbp: 12, rbp: 13 }, + { opKind: 'infix', kind: TokenKind.Slash, lbp: 12, rbp: 13 }, + { opKind: 'infix', kind: TokenKind.Percent, lbp: 12, rbp: 13 }, + + { opKind: 'infix', kind: TokenKind.Plus, lbp: 10, rbp: 11 }, + { opKind: 'infix', kind: TokenKind.Minus, lbp: 10, rbp: 11 }, + + { opKind: 'infix', kind: TokenKind.Lt, lbp: 8, rbp: 9 }, + { opKind: 'infix', kind: TokenKind.LtEq, lbp: 8, rbp: 9 }, + { opKind: 'infix', kind: TokenKind.Gt, lbp: 8, rbp: 9 }, + { opKind: 'infix', kind: TokenKind.GtEq, lbp: 8, rbp: 9 }, + + { opKind: 'infix', kind: TokenKind.Eq2, lbp: 6, rbp: 7 }, + { opKind: 'infix', kind: TokenKind.NotEq, lbp: 6, rbp: 7 }, + + { opKind: 'infix', kind: TokenKind.And2, lbp: 4, rbp: 5 }, + + { opKind: 'infix', kind: TokenKind.Or2, lbp: 2, rbp: 3 }, +]; + +function parsePrefix(s: ITokenStream, minBp: number): Ast.Node { + const startPos = s.getPos(); + const op = s.getKind(); + s.next(); + + // 改行のエスケープ + if (s.getKind() === TokenKind.BackSlash) { + s.next(); + s.nextWith(TokenKind.NewLine); + } + + const expr = parsePratt(s, minBp); + + const endPos = expr.loc.end; + + switch (op) { + case TokenKind.Plus: { + // 数値リテラル以外は非サポート + if (expr.type === 'num') { + return NODE('num', { value: expr.value }, startPos, endPos); + } else { + throw new AiScriptSyntaxError('currently, sign is only supported for number literal.', startPos); + } + // TODO: 将来的にサポートされる式を拡張 + // return NODE('plus', { expr }, startPos, endPos); + } + case TokenKind.Minus: { + // 数値リテラル以外は非サポート + if (expr.type === 'num') { + return NODE('num', { value: -1 * expr.value }, startPos, endPos); + } else { + throw new AiScriptSyntaxError('currently, sign is only supported for number literal.', startPos); + } + // TODO: 将来的にサポートされる式を拡張 + // return NODE('minus', { expr }, startPos, endPos); + } + case TokenKind.Not: { + return NODE('not', { expr }, startPos, endPos); + } + default: { + throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[op]}`, startPos); + } + } +} + +function parseInfix(s: ITokenStream, left: Ast.Node, minBp: number): Ast.Node { + const startPos = s.getPos(); + const op = s.getKind(); + s.next(); + + // 改行のエスケープ + if (s.getKind() === TokenKind.BackSlash) { + s.next(); + s.nextWith(TokenKind.NewLine); + } + + if (op === TokenKind.Dot) { + s.expect(TokenKind.Identifier); + const name = s.token.value!; + s.next(); + + return NODE('prop', { + target: left, + name, + }, startPos, s.getPos()); + } else { + const right = parsePratt(s, minBp); + const endPos = s.getPos(); + + switch (op) { + case TokenKind.Hat: { + return CALL_NODE('Core:pow', [left, right], startPos, endPos); + } + case TokenKind.Asterisk: { + return CALL_NODE('Core:mul', [left, right], startPos, endPos); + } + case TokenKind.Slash: { + return CALL_NODE('Core:div', [left, right], startPos, endPos); + } + case TokenKind.Percent: { + return CALL_NODE('Core:mod', [left, right], startPos, endPos); + } + case TokenKind.Plus: { + return CALL_NODE('Core:add', [left, right], startPos, endPos); + } + case TokenKind.Minus: { + return CALL_NODE('Core:sub', [left, right], startPos, endPos); + } + case TokenKind.Lt: { + return CALL_NODE('Core:lt', [left, right], startPos, endPos); + } + case TokenKind.LtEq: { + return CALL_NODE('Core:lteq', [left, right], startPos, endPos); + } + case TokenKind.Gt: { + return CALL_NODE('Core:gt', [left, right], startPos, endPos); + } + case TokenKind.GtEq: { + return CALL_NODE('Core:gteq', [left, right], startPos, endPos); + } + case TokenKind.Eq2: { + return CALL_NODE('Core:eq', [left, right], startPos, endPos); + } + case TokenKind.NotEq: { + return CALL_NODE('Core:neq', [left, right], startPos, endPos); + } + case TokenKind.And2: { + return NODE('and', { left, right }, startPos, endPos); + } + case TokenKind.Or2: { + return NODE('or', { left, right }, startPos, endPos); + } + default: { + throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[op]}`, startPos); + } + } + } +} + +function parsePostfix(s: ITokenStream, expr: Ast.Node): Ast.Node { + const startPos = s.getPos(); + const op = s.getKind(); + + switch (op) { + case TokenKind.OpenParen: { + return parseCall(s, expr); + } + case TokenKind.OpenBracket: { + s.next(); + const index = parseExpr(s, false); + s.nextWith(TokenKind.CloseBracket); + + return NODE('index', { + target: expr, + index, + }, startPos, s.getPos()); + } + default: { + throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[op]}`, startPos); + } + } +} + +function parseAtom(s: ITokenStream, isStatic: boolean): Ast.Node { + const startPos = s.getPos(); + + switch (s.getKind()) { + case TokenKind.IfKeyword: { + if (isStatic) break; + return parseIf(s); + } + case TokenKind.At: { + if (isStatic) break; + return parseFnExpr(s); + } + case TokenKind.MatchKeyword: { + if (isStatic) break; + return parseMatch(s); + } + case TokenKind.EvalKeyword: { + if (isStatic) break; + return parseEval(s); + } + case TokenKind.ExistsKeyword: { + if (isStatic) break; + return parseExists(s); + } + case TokenKind.Template: { + const values: (string | Ast.Node)[] = []; + + if (isStatic) break; + + for (const [i, element] of s.token.children!.entries()) { + switch (element.kind) { + case TokenKind.TemplateStringElement: { + // トークンの終了位置を取得するために先読み + const nextToken = s.token.children![i + 1] ?? s.lookahead(1); + values.push(NODE('str', { value: element.value! }, element.pos, nextToken.pos)); + break; + } + case TokenKind.TemplateExprElement: { + // スキャナで埋め込み式として事前に読み取っておいたトークン列をパースする + const exprStream = new TokenStream(element.children!); + const expr = parseExpr(exprStream, false); + if (exprStream.getKind() !== TokenKind.EOF) { + throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[exprStream.token.kind]}`, exprStream.token.pos); + } + values.push(expr); + break; + } + default: { + throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[element.kind]}`, element.pos); + } + } + } + + s.next(); + return NODE('tmpl', { tmpl: values }, startPos, s.getPos()); + } + case TokenKind.StringLiteral: { + const value = s.token.value!; + s.next(); + return NODE('str', { value }, startPos, s.getPos()); + } + case TokenKind.NumberLiteral: { + // TODO: validate number value + const value = Number(s.token.value!); + s.next(); + return NODE('num', { value }, startPos, s.getPos()); + } + case TokenKind.TrueKeyword: + case TokenKind.FalseKeyword: { + const value = (s.getKind() === TokenKind.TrueKeyword); + s.next(); + return NODE('bool', { value }, startPos, s.getPos()); + } + case TokenKind.NullKeyword: { + s.next(); + return NODE('null', {}, startPos, s.getPos()); + } + case TokenKind.OpenBrace: { + return parseObject(s, isStatic); + } + case TokenKind.OpenBracket: { + return parseArray(s, isStatic); + } + case TokenKind.Identifier: { + if (isStatic) break; + return parseReference(s); + } + case TokenKind.OpenParen: { + s.next(); + const expr = parseExpr(s, isStatic); + s.nextWith(TokenKind.CloseParen); + return expr; + } + } + throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[s.getKind()]}`, startPos); +} + +/** + * Call = "(" [Expr *(SEP Expr) [SEP]] ")" +*/ +function parseCall(s: ITokenStream, target: Ast.Node): Ast.Node { + const startPos = s.getPos(); + const items: Ast.Node[] = []; + + s.nextWith(TokenKind.OpenParen); + + if (s.getKind() === TokenKind.NewLine) { + s.next(); + } + + while (s.getKind() !== TokenKind.CloseParen) { + items.push(parseExpr(s, false)); + + // separator + switch (s.getKind()) { + case TokenKind.NewLine: { + s.next(); + break; + } + case TokenKind.Comma: { + s.next(); + if (s.getKind() === TokenKind.NewLine) { + s.next(); + } + break; + } + case TokenKind.CloseParen: { + break; + } + default: { + throw new AiScriptSyntaxError('separator expected', s.getPos()); + } + } + } + + s.nextWith(TokenKind.CloseParen); + + return NODE('call', { + target, + args: items, + }, startPos, s.getPos()); +} + +/** + * ```abnf + * If = "if" Expr BlockOrStatement *("elif" Expr BlockOrStatement) ["else" BlockOrStatement] + * ``` +*/ +function parseIf(s: ITokenStream): Ast.Node { + const startPos = s.getPos(); + + s.nextWith(TokenKind.IfKeyword); + const cond = parseExpr(s, false); + const then = parseBlockOrStatement(s); + + if (s.getKind() === TokenKind.NewLine && [TokenKind.ElifKeyword, TokenKind.ElseKeyword].includes(s.lookahead(1).kind)) { + s.next(); + } + + const elseif: { cond: Ast.Node, then: Ast.Node }[] = []; + while (s.getKind() === TokenKind.ElifKeyword) { + s.next(); + const elifCond = parseExpr(s, false); + const elifThen = parseBlockOrStatement(s); + if ((s.getKind()) === TokenKind.NewLine && [TokenKind.ElifKeyword, TokenKind.ElseKeyword].includes(s.lookahead(1).kind)) { + s.next(); + } + elseif.push({ cond: elifCond, then: elifThen }); + } + + let _else = undefined; + if (s.getKind() === TokenKind.ElseKeyword) { + s.next(); + _else = parseBlockOrStatement(s); + } + + return NODE('if', { cond, then, elseif, else: _else }, startPos, s.getPos()); +} + +/** + * ```abnf + * FnExpr = "@" Params [":" Type] Block + * ``` +*/ +function parseFnExpr(s: ITokenStream): Ast.Node { + const startPos = s.getPos(); + + s.nextWith(TokenKind.At); + + const params = parseParams(s); + + let type; + if ((s.getKind()) === TokenKind.Colon) { + s.next(); + type = parseType(s); + } + + const body = parseBlock(s); + + return NODE('fn', { args: params, retType: type, children: body }, startPos, s.getPos()); +} + +/** + * ```abnf + * Match = "match" Expr "{" [MatchCases] ["default" "=>" BlockOrStatement [SEP]] "}" + * MatchCases = "case" Expr "=>" BlockOrStatement *(SEP "case" Expr "=>" BlockOrStatement) [SEP] + * ``` +*/ +function parseMatch(s: ITokenStream): Ast.Node { + const startPos = s.getPos(); + + s.nextWith(TokenKind.MatchKeyword); + const about = parseExpr(s, false); + + s.nextWith(TokenKind.OpenBrace); + + if (s.getKind() === TokenKind.NewLine) { + s.next(); + } + + const qs: { q: Ast.Node, a: Ast.Node }[] = []; + while (s.getKind() !== TokenKind.DefaultKeyword && s.getKind() !== TokenKind.CloseBrace) { + s.nextWith(TokenKind.CaseKeyword); + const q = parseExpr(s, false); + s.nextWith(TokenKind.Arrow); + const a = parseBlockOrStatement(s); + qs.push({ q, a }); + + // separator + switch (s.getKind()) { + case TokenKind.NewLine: { + s.next(); + break; + } + case TokenKind.Comma: { + s.next(); + if (s.getKind() === TokenKind.NewLine) { + s.next(); + } + break; + } + case TokenKind.DefaultKeyword: + case TokenKind.CloseBrace: { + break; + } + default: { + throw new AiScriptSyntaxError('separator expected', s.getPos()); + } + } + } + + let x; + if (s.getKind() === TokenKind.DefaultKeyword) { + s.next(); + s.nextWith(TokenKind.Arrow); + x = parseBlockOrStatement(s); + + // separator + switch (s.getKind()) { + case TokenKind.NewLine: { + s.next(); + break; + } + case TokenKind.Comma: { + s.next(); + if ((s.getKind()) === TokenKind.NewLine) { + s.next(); + } + break; + } + case TokenKind.CloseBrace: { + break; + } + default: { + throw new AiScriptSyntaxError('separator expected', s.getPos()); + } + } + } + + s.nextWith(TokenKind.CloseBrace); + + return NODE('match', { about, qs, default: x }, startPos, s.getPos()); +} + +/** + * ```abnf + * Eval = "eval" Block + * ``` +*/ +function parseEval(s: ITokenStream): Ast.Node { + const startPos = s.getPos(); + + s.nextWith(TokenKind.EvalKeyword); + const statements = parseBlock(s); + + return NODE('block', { statements }, startPos, s.getPos()); +} + +/** + * ```abnf + * Exists = "exists" Reference + * ``` +*/ +function parseExists(s: ITokenStream): Ast.Node { + const startPos = s.getPos(); + + s.nextWith(TokenKind.ExistsKeyword); + const identifier = parseReference(s); + + return NODE('exists', { identifier }, startPos, s.getPos()); +} + +/** + * ```abnf + * Reference = IDENT *(":" IDENT) + * ``` +*/ +function parseReference(s: ITokenStream): Ast.Node { + const startPos = s.getPos(); + + const segs: string[] = []; + while (true) { + if (segs.length > 0) { + if (s.getKind() === TokenKind.Colon) { + if (s.token.hasLeftSpacing) { + throw new AiScriptSyntaxError('Cannot use spaces in a reference.', s.getPos()); + } + s.next(); + if (s.token.hasLeftSpacing) { + throw new AiScriptSyntaxError('Cannot use spaces in a reference.', s.getPos()); + } + } else { + break; + } + } + s.expect(TokenKind.Identifier); + segs.push(s.token.value!); + s.next(); + } + return NODE('identifier', { name: segs.join(':') }, startPos, s.getPos()); +} + +/** + * ```abnf + * Object = "{" [IDENT ":" Expr *(SEP IDENT ":" Expr) [SEP]] "}" + * ``` +*/ +function parseObject(s: ITokenStream, isStatic: boolean): Ast.Node { + const startPos = s.getPos(); + + s.nextWith(TokenKind.OpenBrace); + + while (s.getKind() === TokenKind.NewLine) { + s.next(); + } + + const map = new Map(); + while (s.getKind() !== TokenKind.CloseBrace) { + s.expect(TokenKind.Identifier); + const k = s.token.value!; + s.next(); + + s.nextWith(TokenKind.Colon); + + const v = parseExpr(s, isStatic); + + map.set(k, v); + + // separator + switch (s.getKind()) { + case TokenKind.NewLine: + case TokenKind.Comma: { + s.next(); + while (s.getKind() === TokenKind.NewLine) { + s.next(); + } + break; + } + case TokenKind.CloseBrace: { + break; + } + default: { + throw new AiScriptSyntaxError('separator expected', s.getPos()); + } + } + } + + s.nextWith(TokenKind.CloseBrace); + + return NODE('obj', { value: map }, startPos, s.getPos()); +} + +/** + * ```abnf + * Array = "[" [Expr *(SEP Expr) [SEP]] "]" + * ``` +*/ +function parseArray(s: ITokenStream, isStatic: boolean): Ast.Node { + const startPos = s.getPos(); + + s.nextWith(TokenKind.OpenBracket); + + while (s.getKind() === TokenKind.NewLine) { + s.next(); + } + + const value = []; + while (s.getKind() !== TokenKind.CloseBracket) { + value.push(parseExpr(s, isStatic)); + + // separator + switch (s.getKind()) { + case TokenKind.NewLine: + case TokenKind.Comma: { + s.next(); + while (s.getKind() === TokenKind.NewLine) { + s.next(); + } + break; + } + case TokenKind.CloseBracket: { + break; + } + default: { + throw new AiScriptSyntaxError('separator expected', s.getPos()); + } + } + } + + s.nextWith(TokenKind.CloseBracket); + + return NODE('arr', { value }, startPos, s.getPos()); +} + +//#region Pratt parsing + +type PrefixInfo = { opKind: 'prefix', kind: TokenKind, bp: number }; +type InfixInfo = { opKind: 'infix', kind: TokenKind, lbp: number, rbp: number }; +type PostfixInfo = { opKind: 'postfix', kind: TokenKind, bp: number }; +type OpInfo = PrefixInfo | InfixInfo | PostfixInfo; + +function parsePratt(s: ITokenStream, minBp: number): Ast.Node { + // pratt parsing + // https://matklad.github.io/2020/04/13/simple-but-powerful-pratt-parsing.html + + let left: Ast.Node; + + const tokenKind = s.getKind(); + const prefix = operators.find((x): x is PrefixInfo => x.opKind === 'prefix' && x.kind === tokenKind); + if (prefix != null) { + left = parsePrefix(s, prefix.bp); + } else { + left = parseAtom(s, false); + } + + while (true) { + // 改行のエスケープ + if (s.getKind() === TokenKind.BackSlash) { + s.next(); + s.nextWith(TokenKind.NewLine); + } + + const tokenKind = s.getKind(); + + const postfix = operators.find((x): x is PostfixInfo => x.opKind === 'postfix' && x.kind === tokenKind); + if (postfix != null) { + if (postfix.bp < minBp) { + break; + } + + if ([TokenKind.OpenBracket, TokenKind.OpenParen].includes(tokenKind) && s.token.hasLeftSpacing) { + // 前にスペースがある場合は後置演算子として処理しない + } else { + left = parsePostfix(s, left); + continue; + } + } + + const infix = operators.find((x): x is InfixInfo => x.opKind === 'infix' && x.kind === tokenKind); + if (infix != null) { + if (infix.lbp < minBp) { + break; + } + + left = parseInfix(s, left, infix.rbp); + continue; + } + + break; + } + + return left; +} + +//#endregion Pratt parsing diff --git a/src/parser/syntaxes/statements.ts b/src/parser/syntaxes/statements.ts new file mode 100644 index 00000000..2c50138e --- /dev/null +++ b/src/parser/syntaxes/statements.ts @@ -0,0 +1,469 @@ +import { AiScriptSyntaxError } from '../../error.js'; +import { CALL_NODE, NODE } from '../utils.js'; +import { TokenKind } from '../token.js'; +import { parseBlock, parseParams, parseType } from './common.js'; +import { parseExpr } from './expressions.js'; + +import type * as Ast from '../../node.js'; +import type { ITokenStream } from '../streams/token-stream.js'; + +/** + * ```abnf + * Statement = VarDef / FnDef / Out / Return / Attr / Each / For / Loop + * / Break / Continue / Assign / Expr + * ``` +*/ +export function parseStatement(s: ITokenStream): Ast.Node { + const startPos = s.getPos(); + + switch (s.getKind()) { + case TokenKind.VarKeyword: + case TokenKind.LetKeyword: { + return parseVarDef(s); + } + case TokenKind.At: { + if (s.lookahead(1).kind === TokenKind.Identifier) { + return parseFnDef(s); + } + break; + } + case TokenKind.Out: { + return parseOut(s); + } + case TokenKind.ReturnKeyword: { + return parseReturn(s); + } + case TokenKind.OpenSharpBracket: { + return parseStatementWithAttr(s); + } + case TokenKind.EachKeyword: { + return parseEach(s); + } + case TokenKind.ForKeyword: { + return parseFor(s); + } + case TokenKind.LoopKeyword: { + return parseLoop(s); + } + case TokenKind.DoKeyword: { + return parseDoWhile(s); + } + case TokenKind.WhileKeyword: { + return parseWhile(s); + } + case TokenKind.BreakKeyword: { + s.next(); + return NODE('break', {}, startPos, s.getPos()); + } + case TokenKind.ContinueKeyword: { + s.next(); + return NODE('continue', {}, startPos, s.getPos()); + } + } + const expr = parseExpr(s, false); + const assign = tryParseAssign(s, expr); + if (assign) { + return assign; + } + return expr; +} + +export function parseDefStatement(s: ITokenStream): Ast.Node { + switch (s.getKind()) { + case TokenKind.VarKeyword: + case TokenKind.LetKeyword: { + return parseVarDef(s); + } + case TokenKind.At: { + return parseFnDef(s); + } + default: { + throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[s.getKind()]}`, s.getPos()); + } + } +} + +/** + * ```abnf + * BlockOrStatement = Block / Statement + * ``` +*/ +export function parseBlockOrStatement(s: ITokenStream): Ast.Node { + if (s.getKind() === TokenKind.OpenBrace) { + const startPos = s.getPos(); + const statements = parseBlock(s); + return NODE('block', { statements }, startPos, s.getPos()); + } else { + return parseStatement(s); + } +} + +/** + * ```abnf + * VarDef = ("let" / "var") IDENT [":" Type] "=" Expr + * ``` +*/ +function parseVarDef(s: ITokenStream): Ast.Node { + const startPos = s.getPos(); + + let mut; + switch (s.getKind()) { + case TokenKind.LetKeyword: { + mut = false; + break; + } + case TokenKind.VarKeyword: { + mut = true; + break; + } + default: { + throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[s.getKind()]}`, s.getPos()); + } + } + s.next(); + + s.expect(TokenKind.Identifier); + const name = s.token.value!; + s.next(); + + let type; + if (s.getKind() === TokenKind.Colon) { + s.next(); + type = parseType(s); + } + + s.nextWith(TokenKind.Eq); + + if (s.getKind() === TokenKind.NewLine) { + s.next(); + } + + const expr = parseExpr(s, false); + + return NODE('def', { name, varType: type, expr, mut, attr: [] }, startPos, s.getPos()); +} + +/** + * ```abnf + * FnDef = "@" IDENT Params [":" Type] Block + * ``` +*/ +function parseFnDef(s: ITokenStream): Ast.Node { + const startPos = s.getPos(); + + s.nextWith(TokenKind.At); + + s.expect(TokenKind.Identifier); + const name = s.token.value; + s.next(); + + const params = parseParams(s); + + let type; + if (s.getKind() === TokenKind.Colon) { + s.next(); + type = parseType(s); + } + + const body = parseBlock(s); + + const endPos = s.getPos(); + + return NODE('def', { + name, + expr: NODE('fn', { + args: params, + retType: type, + children: body, + }, startPos, endPos), + mut: false, + attr: [], + }, startPos, endPos); +} + +/** + * ```abnf + * Out = "<:" Expr + * ``` +*/ +function parseOut(s: ITokenStream): Ast.Node { + const startPos = s.getPos(); + + s.nextWith(TokenKind.Out); + const expr = parseExpr(s, false); + + return CALL_NODE('print', [expr], startPos, s.getPos()); +} + +/** + * ```abnf + * Each = "each" "let" IDENT ("," / SPACE) Expr BlockOrStatement + * / "each" "(" "let" IDENT ("," / SPACE) Expr ")" BlockOrStatement + * ``` +*/ +function parseEach(s: ITokenStream): Ast.Node { + const startPos = s.getPos(); + let hasParen = false; + + s.nextWith(TokenKind.EachKeyword); + + if (s.getKind() === TokenKind.OpenParen) { + hasParen = true; + s.next(); + } + + s.nextWith(TokenKind.LetKeyword); + + s.expect(TokenKind.Identifier); + const name = s.token.value!; + s.next(); + + if (s.getKind() === TokenKind.Comma) { + s.next(); + } else { + throw new AiScriptSyntaxError('separator expected', s.getPos()); + } + + const items = parseExpr(s, false); + + if (hasParen) { + s.nextWith(TokenKind.CloseParen); + } + + const body = parseBlockOrStatement(s); + + return NODE('each', { + var: name, + items: items, + for: body, + }, startPos, s.getPos()); +} + +function parseFor(s: ITokenStream): Ast.Node { + const startPos = s.getPos(); + let hasParen = false; + + s.nextWith(TokenKind.ForKeyword); + + if (s.getKind() === TokenKind.OpenParen) { + hasParen = true; + s.next(); + } + + if (s.getKind() === TokenKind.LetKeyword) { + // range syntax + s.next(); + + const identPos = s.getPos(); + + s.expect(TokenKind.Identifier); + const name = s.token.value!; + s.next(); + + let _from; + if (s.getKind() === TokenKind.Eq) { + s.next(); + _from = parseExpr(s, false); + } else { + _from = NODE('num', { value: 0 }, identPos, identPos); + } + + if (s.getKind() === TokenKind.Comma) { + s.next(); + } else { + throw new AiScriptSyntaxError('separator expected', s.getPos()); + } + + const to = parseExpr(s, false); + + if (hasParen) { + s.nextWith(TokenKind.CloseParen); + } + + const body = parseBlockOrStatement(s); + + return NODE('for', { + var: name, + from: _from, + to, + for: body, + }, startPos, s.getPos()); + } else { + // times syntax + + const times = parseExpr(s, false); + + if (hasParen) { + s.nextWith(TokenKind.CloseParen); + } + + const body = parseBlockOrStatement(s); + + return NODE('for', { + times, + for: body, + }, startPos, s.getPos()); + } +} + +/** + * ```abnf + * Return = "return" Expr + * ``` +*/ +function parseReturn(s: ITokenStream): Ast.Node { + const startPos = s.getPos(); + + s.nextWith(TokenKind.ReturnKeyword); + const expr = parseExpr(s, false); + + return NODE('return', { expr }, startPos, s.getPos()); +} + +/** + * ```abnf + * StatementWithAttr = *Attr Statement + * ``` +*/ +function parseStatementWithAttr(s: ITokenStream): Ast.Node { + const attrs: Ast.Attribute[] = []; + while (s.getKind() === TokenKind.OpenSharpBracket) { + attrs.push(parseAttr(s) as Ast.Attribute); + s.nextWith(TokenKind.NewLine); + } + + const statement = parseStatement(s); + + if (statement.type !== 'def') { + throw new AiScriptSyntaxError('invalid attribute.', statement.loc.start); + } + if (statement.attr != null) { + statement.attr.push(...attrs); + } else { + statement.attr = attrs; + } + + return statement; +} + +/** + * ```abnf + * Attr = "#[" IDENT [StaticExpr] "]" + * ``` +*/ +function parseAttr(s: ITokenStream): Ast.Node { + const startPos = s.getPos(); + + s.nextWith(TokenKind.OpenSharpBracket); + + s.expect(TokenKind.Identifier); + const name = s.token.value!; + s.next(); + + let value; + if (s.getKind() !== TokenKind.CloseBracket) { + value = parseExpr(s, true); + } else { + const closePos = s.getPos(); + value = NODE('bool', { value: true }, closePos, closePos); + } + + s.nextWith(TokenKind.CloseBracket); + + return NODE('attr', { name, value }, startPos, s.getPos()); +} + +/** + * ```abnf + * Loop = "loop" Block + * ``` +*/ +function parseLoop(s: ITokenStream): Ast.Node { + const startPos = s.getPos(); + + s.nextWith(TokenKind.LoopKeyword); + const statements = parseBlock(s); + + return NODE('loop', { statements }, startPos, s.getPos()); +} + +/** + * ```abnf + * Loop = "do" BlockOrStatement "while" Expr + * ``` +*/ +function parseDoWhile(s: ITokenStream): Ast.Node { + const doStartPos = s.getPos(); + s.nextWith(TokenKind.DoKeyword); + const body = parseBlockOrStatement(s); + const whilePos = s.getPos(); + s.nextWith(TokenKind.WhileKeyword); + const cond = parseExpr(s, false); + const endPos = s.getPos(); + + return NODE('loop', { + statements: [ + body, + NODE('if', { + cond: NODE('not', { expr: cond }, whilePos, endPos), + then: NODE('break', {}, endPos, endPos), + elseif: [], + }, whilePos, endPos), + ], + }, doStartPos, endPos); +} + +/** + * ```abnf + * Loop = "while" Expr BlockOrStatement + * ``` +*/ +function parseWhile(s: ITokenStream): Ast.Node { + const startPos = s.getPos(); + s.nextWith(TokenKind.WhileKeyword); + const cond = parseExpr(s, false); + const condEndPos = s.getPos(); + const body = parseBlockOrStatement(s); + + return NODE('loop', { + statements: [ + NODE('if', { + cond: NODE('not', { expr: cond }, startPos, condEndPos), + then: NODE('break', {}, condEndPos, condEndPos), + elseif: [], + }, startPos, condEndPos), + body, + ], + }, startPos, s.getPos()); +} + +/** + * ```abnf + * Assign = Expr ("=" / "+=" / "-=") Expr + * ``` +*/ +function tryParseAssign(s: ITokenStream, dest: Ast.Node): Ast.Node | undefined { + const startPos = s.getPos(); + + // Assign + switch (s.getKind()) { + case TokenKind.Eq: { + s.next(); + const expr = parseExpr(s, false); + return NODE('assign', { dest, expr }, startPos, s.getPos()); + } + case TokenKind.PlusEq: { + s.next(); + const expr = parseExpr(s, false); + return NODE('addAssign', { dest, expr }, startPos, s.getPos()); + } + case TokenKind.MinusEq: { + s.next(); + const expr = parseExpr(s, false); + return NODE('subAssign', { dest, expr }, startPos, s.getPos()); + } + default: { + return; + } + } +} diff --git a/src/parser/syntaxes/toplevel.ts b/src/parser/syntaxes/toplevel.ts new file mode 100644 index 00000000..a1914f0f --- /dev/null +++ b/src/parser/syntaxes/toplevel.ts @@ -0,0 +1,135 @@ +import { NODE } from '../utils.js'; +import { TokenKind } from '../token.js'; +import { AiScriptSyntaxError } from '../../error.js'; +import { parseDefStatement, parseStatement } from './statements.js'; +import { parseExpr } from './expressions.js'; + +import type * as Ast from '../../node.js'; +import type { ITokenStream } from '../streams/token-stream.js'; + +/** + * ```abnf + * TopLevel = *(Namespace / Meta / Statement) + * ``` +*/ +export function parseTopLevel(s: ITokenStream): Ast.Node[] { + const nodes: Ast.Node[] = []; + + while (s.getKind() === TokenKind.NewLine) { + s.next(); + } + + while (s.getKind() !== TokenKind.EOF) { + switch (s.getKind()) { + case TokenKind.Colon2: { + nodes.push(parseNamespace(s)); + break; + } + case TokenKind.Sharp3: { + nodes.push(parseMeta(s)); + break; + } + default: { + nodes.push(parseStatement(s)); + break; + } + } + + // terminator + switch (s.getKind()) { + case TokenKind.NewLine: + case TokenKind.SemiColon: { + while ([TokenKind.NewLine, TokenKind.SemiColon].includes(s.getKind())) { + s.next(); + } + break; + } + case TokenKind.EOF: { + break; + } + default: { + throw new AiScriptSyntaxError('Multiple statements cannot be placed on a single line.', s.getPos()); + } + } + } + + return nodes; +} + +/** + * ```abnf + * Namespace = "::" IDENT "{" *(VarDef / FnDef / Namespace) "}" + * ``` +*/ +export function parseNamespace(s: ITokenStream): Ast.Node { + const startPos = s.getPos(); + + s.nextWith(TokenKind.Colon2); + + s.expect(TokenKind.Identifier); + const name = s.token.value!; + s.next(); + + const members: Ast.Node[] = []; + s.nextWith(TokenKind.OpenBrace); + + while (s.getKind() === TokenKind.NewLine) { + s.next(); + } + + while (s.getKind() !== TokenKind.CloseBrace) { + switch (s.getKind()) { + case TokenKind.VarKeyword: + case TokenKind.LetKeyword: + case TokenKind.At: { + members.push(parseDefStatement(s)); + break; + } + case TokenKind.Colon2: { + members.push(parseNamespace(s)); + break; + } + } + + // terminator + switch (s.getKind()) { + case TokenKind.NewLine: + case TokenKind.SemiColon: { + while ([TokenKind.NewLine, TokenKind.SemiColon].includes(s.getKind())) { + s.next(); + } + break; + } + case TokenKind.CloseBrace: { + break; + } + default: { + throw new AiScriptSyntaxError('Multiple statements cannot be placed on a single line.', s.getPos()); + } + } + } + s.nextWith(TokenKind.CloseBrace); + + return NODE('ns', { name, members }, startPos, s.getPos()); +} + +/** + * ```abnf + * Meta = "###" [IDENT] StaticExpr + * ``` +*/ +export function parseMeta(s: ITokenStream): Ast.Node { + const startPos = s.getPos(); + + s.nextWith(TokenKind.Sharp3); + + let name = null; + if (s.getKind() === TokenKind.Identifier) { + name = s.token.value!; + s.next(); + } + + const value = parseExpr(s, true); + + return NODE('meta', { name, value }, startPos, value.loc.end); +} diff --git a/src/parser/token.ts b/src/parser/token.ts new file mode 100644 index 00000000..d4bdaf49 --- /dev/null +++ b/src/parser/token.ts @@ -0,0 +1,132 @@ +export enum TokenKind { + EOF, + NewLine, + Identifier, + + // literal + NumberLiteral, + StringLiteral, + + // template string + Template, + TemplateStringElement, + TemplateExprElement, + + // keyword + NullKeyword, + TrueKeyword, + FalseKeyword, + EachKeyword, + ForKeyword, + LoopKeyword, + DoKeyword, + WhileKeyword, + BreakKeyword, + ContinueKeyword, + MatchKeyword, + CaseKeyword, + DefaultKeyword, + IfKeyword, + ElifKeyword, + ElseKeyword, + ReturnKeyword, + EvalKeyword, + VarKeyword, + LetKeyword, + ExistsKeyword, + + /** "!" */ + Not, + /** "!=" */ + NotEq, + /** "#[" */ + OpenSharpBracket, + /** "###" */ + Sharp3, + /** "%" */ + Percent, + /** "&&" */ + And2, + /** "(" */ + OpenParen, + /** ")" */ + CloseParen, + /** "*" */ + Asterisk, + /** "+" */ + Plus, + /** "+=" */ + PlusEq, + /** "," */ + Comma, + /** "-" */ + Minus, + /** "-=" */ + MinusEq, + /** "." */ + Dot, + /** "/" */ + Slash, + /** ":" */ + Colon, + /** "::" */ + Colon2, + /** ";" */ + SemiColon, + /** "<" */ + Lt, + /** "<=" */ + LtEq, + /** "<:" */ + Out, + /** "=" */ + Eq, + /** "==" */ + Eq2, + /** "=>" */ + Arrow, + /** ">" */ + Gt, + /** ">=" */ + GtEq, + /** "?" */ + Question, + /** "@" */ + At, + /** "[" */ + OpenBracket, + /** "\\" */ + BackSlash, + /** "]" */ + CloseBracket, + /** "^" */ + Hat, + /** "{" */ + OpenBrace, + /** "||" */ + Or2, + /** "}" */ + CloseBrace, +} + +export type TokenPosition = { column: number, line: number }; + +export class Token { + constructor( + public kind: TokenKind, + public pos: TokenPosition, + public hasLeftSpacing = false, + /** for number literal, string literal */ + public value?: string, + /** for template syntax */ + public children?: Token[], + ) { } +} + +/** + * - opts.value: for number literal, string literal + * - opts.children: for template syntax +*/ +export function TOKEN(kind: TokenKind, pos: TokenPosition, opts?: { hasLeftSpacing?: boolean, value?: Token['value'], children?: Token['children'] }): Token { + return new Token(kind, pos, opts?.hasLeftSpacing, opts?.value, opts?.children); +} diff --git a/src/parser/utils.ts b/src/parser/utils.ts new file mode 100644 index 00000000..1bd6d78f --- /dev/null +++ b/src/parser/utils.ts @@ -0,0 +1,20 @@ +import type * as Ast from '../node.js'; + +export function NODE(type: string, params: Record, start: Ast.Pos, end: Ast.Pos): Ast.Node { + const node: Record = { type }; + for (const key of Object.keys(params)) { + if (params[key] !== undefined) { + node[key] = params[key]; + } + } + node.loc = { start, end }; + return node as Ast.Node; +} + +export function CALL_NODE(name: string, args: Ast.Node[], start: Ast.Pos, end: Ast.Pos): Ast.Node { + return NODE('call', { + // 糖衣構文はidentifierがソースコードに出現しないので長さ0とする。 + target: NODE('identifier', { name }, start, start), + args, + }, start, end); +} diff --git a/src/parser/visit.ts b/src/parser/visit.ts index db617de2..17d11f0f 100644 --- a/src/parser/visit.ts +++ b/src/parser/visit.ts @@ -1,143 +1,132 @@ -import * as Cst from './node.js'; +import type * as Ast from '../node.js'; -export function visitNode(node: Cst.Node, fn: (node: Cst.Node) => Cst.Node): Cst.Node { +export function visitNode(node: Ast.Node, fn: (node: Ast.Node) => Ast.Node): Ast.Node { const result = fn(node); // nested nodes switch (result.type) { case 'def': { - result.expr = visitNode(result.expr, fn) as Cst.Definition['expr']; + result.expr = visitNode(result.expr, fn) as Ast.Definition['expr']; break; } case 'return': { - result.expr = visitNode(result.expr, fn) as Cst.Return['expr']; + result.expr = visitNode(result.expr, fn) as Ast.Return['expr']; break; } case 'each': { - result.items = visitNode(result.items, fn) as Cst.Each['items']; - result.for = visitNode(result.for, fn) as Cst.Each['for']; + result.items = visitNode(result.items, fn) as Ast.Each['items']; + result.for = visitNode(result.for, fn) as Ast.Each['for']; break; } case 'for': { if (result.from != null) { - result.from = visitNode(result.from, fn) as Cst.For['from']; + result.from = visitNode(result.from, fn) as Ast.For['from']; } if (result.to != null) { - result.to = visitNode(result.to, fn) as Cst.For['to']; + result.to = visitNode(result.to, fn) as Ast.For['to']; } if (result.times != null) { - result.times = visitNode(result.times, fn) as Cst.For['times']; + result.times = visitNode(result.times, fn) as Ast.For['times']; } - result.for = visitNode(result.for, fn) as Cst.For['for']; + result.for = visitNode(result.for, fn) as Ast.For['for']; break; } case 'loop': { for (let i = 0; i < result.statements.length; i++) { - result.statements[i] = visitNode(result.statements[i]!, fn) as Cst.Loop['statements'][number]; + result.statements[i] = visitNode(result.statements[i]!, fn) as Ast.Loop['statements'][number]; } break; } case 'addAssign': case 'subAssign': case 'assign': { - result.expr = visitNode(result.expr, fn) as Cst.Assign['expr']; - result.dest = visitNode(result.dest, fn) as Cst.Assign['dest']; - break; - } - case 'infix': { - for (let i = 0; i < result.operands.length; i++) { - result.operands[i] = visitNode(result.operands[i]!, fn) as Cst.Infix['operands'][number]; - } + result.expr = visitNode(result.expr, fn) as Ast.Assign['expr']; + result.dest = visitNode(result.dest, fn) as Ast.Assign['dest']; break; } case 'not': { - result.expr = visitNode(result.expr, fn) as Cst.Return['expr']; + result.expr = visitNode(result.expr, fn) as Ast.Return['expr']; break; } case 'if': { - result.cond = visitNode(result.cond, fn) as Cst.If['cond']; - result.then = visitNode(result.then, fn) as Cst.If['then']; + result.cond = visitNode(result.cond, fn) as Ast.If['cond']; + result.then = visitNode(result.then, fn) as Ast.If['then']; for (const prop of result.elseif) { - prop.cond = visitNode(prop.cond, fn) as Cst.If['elseif'][number]['cond']; - prop.then = visitNode(prop.then, fn) as Cst.If['elseif'][number]['then']; + prop.cond = visitNode(prop.cond, fn) as Ast.If['elseif'][number]['cond']; + prop.then = visitNode(prop.then, fn) as Ast.If['elseif'][number]['then']; } if (result.else != null) { - result.else = visitNode(result.else, fn) as Cst.If['else']; + result.else = visitNode(result.else, fn) as Ast.If['else']; } break; } case 'fn': { + for (const i of result.args.keys()) { + if (result.args[i]!.default) { + result.args[i]!.default = visitNode(result.args[i]!.default!, fn) as Ast.Fn['args'][number]['default']; + } + } for (let i = 0; i < result.children.length; i++) { - result.children[i] = visitNode(result.children[i]!, fn) as Cst.Fn['children'][number]; + result.children[i] = visitNode(result.children[i]!, fn) as Ast.Fn['children'][number]; } break; } case 'match': { - result.about = visitNode(result.about, fn) as Cst.Match['about']; + result.about = visitNode(result.about, fn) as Ast.Match['about']; for (const prop of result.qs) { - prop.q = visitNode(prop.q, fn) as Cst.Match['qs'][number]['q']; - prop.a = visitNode(prop.a, fn) as Cst.Match['qs'][number]['a']; + prop.q = visitNode(prop.q, fn) as Ast.Match['qs'][number]['q']; + prop.a = visitNode(prop.a, fn) as Ast.Match['qs'][number]['a']; } if (result.default != null) { - result.default = visitNode(result.default, fn) as Cst.Match['default']; + result.default = visitNode(result.default, fn) as Ast.Match['default']; } break; } case 'block': { for (let i = 0; i < result.statements.length; i++) { - result.statements[i] = visitNode(result.statements[i]!, fn) as Cst.Block['statements'][number]; + result.statements[i] = visitNode(result.statements[i]!, fn) as Ast.Block['statements'][number]; } break; } case 'exists': { - result.identifier = visitNode(result.identifier,fn) as Cst.Exists['identifier']; + result.identifier = visitNode(result.identifier,fn) as Ast.Exists['identifier']; break; } case 'tmpl': { for (let i = 0; i < result.tmpl.length; i++) { const item = result.tmpl[i]!; if (typeof item !== 'string') { - result.tmpl[i] = visitNode(item, fn) as Cst.Tmpl['tmpl'][number]; + result.tmpl[i] = visitNode(item, fn) as Ast.Tmpl['tmpl'][number]; } } break; } case 'obj': { for (const item of result.value) { - result.value.set(item[0], visitNode(item[1], fn) as Cst.Expression); + result.value.set(item[0], visitNode(item[1], fn) as Ast.Expression); } break; } case 'arr': { for (let i = 0; i < result.value.length; i++) { - result.value[i] = visitNode(result.value[i]!, fn) as Cst.Arr['value'][number]; + result.value[i] = visitNode(result.value[i]!, fn) as Ast.Arr['value'][number]; } break; } - case 'callChain': { - for (let i = 0; i < result.args.length; i++) { - result.args[i] = visitNode(result.args[i]!, fn) as Cst.Call['args'][number]; - } - break; - } - case 'indexChain': { - result.index = visitNode(result.index, fn) as Cst.Index['index']; - break; - } case 'call': { - result.target = visitNode(result.target, fn) as Cst.Call['target']; + result.target = visitNode(result.target, fn) as Ast.Call['target']; for (let i = 0; i < result.args.length; i++) { - result.args[i] = visitNode(result.args[i]!, fn) as Cst.Call['args'][number]; + result.args[i] = visitNode(result.args[i]!, fn) as Ast.Call['args'][number]; } break; } case 'index': { - result.target = visitNode(result.target, fn) as Cst.Index['target']; - result.index = visitNode(result.index, fn) as Cst.Index['index']; + result.target = visitNode(result.target, fn) as Ast.Index['target']; + result.index = visitNode(result.index, fn) as Ast.Index['index']; break; } case 'prop': { - result.target = visitNode(result.target, fn) as Cst.Prop['target']; + result.target = visitNode(result.target, fn) as Ast.Prop['target']; break; } case 'ns': { @@ -149,19 +138,11 @@ export function visitNode(node: Cst.Node, fn: (node: Cst.Node) => Cst.Node): Cst case 'or': case 'and': { - result.left = visitNode(result.left, fn) as (Cst.And | Cst.Or)['left']; - result.right = visitNode(result.right, fn) as (Cst.And | Cst.Or)['right']; + result.left = visitNode(result.left, fn) as (Ast.And | Ast.Or)['left']; + result.right = visitNode(result.right, fn) as (Ast.And | Ast.Or)['right']; break; } } - if (Cst.hasChainProp(result)) { - if (result.chain != null) { - for (let i = 0; i < result.chain.length; i++) { - result.chain[i] = visitNode(result.chain[i]!, fn) as Cst.ChainMember; - } - } - } - return result; } diff --git a/src/type.ts b/src/type.ts index 5e93acf0..94e835a9 100644 --- a/src/type.ts +++ b/src/type.ts @@ -151,7 +151,7 @@ export function getTypeBySource(typeSource: Ast.TypeSource): Type { return T_GENERIC(typeSource.name, [innerType]); } } - throw new AiScriptSyntaxError(`Unknown type: '${getTypeNameBySource(typeSource)}'`); + throw new AiScriptSyntaxError(`Unknown type: '${getTypeNameBySource(typeSource)}'`, typeSource.loc.start); } else { const argTypes = typeSource.args.map(arg => getTypeBySource(arg)); return T_FN(argTypes, getTypeBySource(typeSource.result)); diff --git a/test/index.ts b/test/index.ts index 1476dc93..5f3757cb 100644 --- a/test/index.ts +++ b/test/index.ts @@ -4,39 +4,12 @@ */ import * as assert from 'assert'; -import { expect, test } from '@jest/globals'; -import { Parser, Interpreter, utils, errors, Ast } from '../src'; -import { NUM, STR, NULL, ARR, OBJ, BOOL, TRUE, FALSE, ERROR ,FN_NATIVE, Value } from '../src/interpreter/value'; -let { AiScriptRuntimeError, AiScriptIndexOutOfRangeError } = errors; - -const exe = (program: string): Promise => new Promise((ok, err) => { - const aiscript = new Interpreter({}, { - out(value) { - ok(value); - }, - maxStep: 9999, - }); - - const parser = new Parser(); - const ast = parser.parse(program); - aiscript.exec(ast).catch(err); -}); - -const getMeta = (program: string) => { - const parser = new Parser(); - const ast = parser.parse(program); - - const metadata = Interpreter.collectMetadata(ast); - - return metadata; -}; +import { test } from '@jest/globals'; +import { Parser, Interpreter, Ast } from '../src'; +import { NUM, STR, NULL, ARR, OBJ, BOOL, TRUE, FALSE, ERROR ,FN_NATIVE } from '../src/interpreter/value'; +import { AiScriptSyntaxError, AiScriptRuntimeError, AiScriptIndexOutOfRangeError } from '../src/error'; +import { exe, eq } from './testutils'; -const eq = (a: Value, b: Value) => { - assert.deepEqual(a.type, b.type); - if (a.type !== 'null' && a.type !== 'fn' && b.type !== 'null' && b.type !== 'fn') { - assert.deepEqual(a.value, b.value); - } -}; test.concurrent('Hello, world!', async () => { const res = await exe('<: "Hello, world!"'); @@ -49,360 +22,6 @@ test.concurrent('empty script', async () => { assert.deepEqual(ast, []); }); -describe('Interpreter', () => { - describe('Scope', () => { - test.concurrent('getAll', async () => { - const aiscript = new Interpreter({}); - await aiscript.exec(Parser.parse(` - let a = 1 - @b() { - let x = a + 1 - x - } - if true { - var y = 2 - } - var c = true - `)); - const vars = aiscript.scope.getAll(); - assert.ok(vars.get('a') != null); - assert.ok(vars.get('b') != null); - assert.ok(vars.get('c') != null); - assert.ok(vars.get('x') == null); - assert.ok(vars.get('y') == null); - }); - }); -}); - -describe('error handler', () => { - test.concurrent('error from outside caller', async () => { - let outsideCaller: () => Promise = async () => {}; - let errCount: number = 0; - const aiscript = new Interpreter({ - emitError: FN_NATIVE((_args, _opts) => { - throw Error('emitError'); - }), - genOutsideCaller: FN_NATIVE(([fn], opts) => { - utils.assertFunction(fn); - outsideCaller = async () => { - opts.topCall(fn, []); - }; - }), - }, { - err(e) { errCount++ }, - }); - await aiscript.exec(Parser.parse(` - genOutsideCaller(emitError) - `)); - assert.strictEqual(errCount, 0); - await outsideCaller(); - assert.strictEqual(errCount, 1); - }); - - test.concurrent('array.map calls the handler just once', async () => { - let errCount: number = 0; - const aiscript = new Interpreter({}, { - err(e) { errCount++ }, - }); - await aiscript.exec(Parser.parse(` - Core:range(1,5).map(@(){ hoge }) - `)); - assert.strictEqual(errCount, 1); - }); -}); - -describe('ops', () => { - test.concurrent('==', async () => { - eq(await exe('<: (1 == 1)'), BOOL(true)); - eq(await exe('<: (1 == 2)'), BOOL(false)); - }); - - test.concurrent('!=', async () => { - eq(await exe('<: (1 != 2)'), BOOL(true)); - eq(await exe('<: (1 != 1)'), BOOL(false)); - }); - - test.concurrent('&&', async () => { - eq(await exe('<: (true && true)'), BOOL(true)); - eq(await exe('<: (true && false)'), BOOL(false)); - eq(await exe('<: (false && true)'), BOOL(false)); - eq(await exe('<: (false && false)'), BOOL(false)); - eq(await exe('<: (false && null)'), BOOL(false)); - try { - await exe('<: (true && null)'); - } catch (e) { - assert.ok(e instanceof AiScriptRuntimeError); - return; - } - - eq( - await exe(` - var tmp = null - - @func() { - tmp = true - return true - } - - false && func() - - <: tmp - `), - NULL - ) - - eq( - await exe(` - var tmp = null - - @func() { - tmp = true - return true - } - - true && func() - - <: tmp - `), - BOOL(true) - ) - - assert.fail(); - }); - - test.concurrent('||', async () => { - eq(await exe('<: (true || true)'), BOOL(true)); - eq(await exe('<: (true || false)'), BOOL(true)); - eq(await exe('<: (false || true)'), BOOL(true)); - eq(await exe('<: (false || false)'), BOOL(false)); - eq(await exe('<: (true || null)'), BOOL(true)); - try { - await exe('<: (false || null)'); - } catch (e) { - assert.ok(e instanceof AiScriptRuntimeError); - return; - } - - eq( - await exe(` - var tmp = null - - @func() { - tmp = true - return true - } - - true || func() - - <: tmp - `), - NULL - ) - - eq( - await exe(` - var tmp = null - - @func() { - tmp = true - return true - } - - false || func() - - <: tmp - `), - BOOL(true) - ) - - assert.fail(); - }); - - test.concurrent('+', async () => { - eq(await exe('<: (1 + 1)'), NUM(2)); - }); - - test.concurrent('-', async () => { - eq(await exe('<: (1 - 1)'), NUM(0)); - }); - - test.concurrent('*', async () => { - eq(await exe('<: (1 * 1)'), NUM(1)); - }); - - test.concurrent('^', async () => { - eq(await exe('<: (1 ^ 0)'), NUM(1)); - }); - - test.concurrent('/', async () => { - eq(await exe('<: (1 / 1)'), NUM(1)); - }); - - test.concurrent('%', async () => { - eq(await exe('<: (1 % 1)'), NUM(0)); - }); - - test.concurrent('>', async () => { - eq(await exe('<: (2 > 1)'), BOOL(true)); - eq(await exe('<: (1 > 1)'), BOOL(false)); - eq(await exe('<: (0 > 1)'), BOOL(false)); - }); - - test.concurrent('<', async () => { - eq(await exe('<: (2 < 1)'), BOOL(false)); - eq(await exe('<: (1 < 1)'), BOOL(false)); - eq(await exe('<: (0 < 1)'), BOOL(true)); - }); - - test.concurrent('>=', async () => { - eq(await exe('<: (2 >= 1)'), BOOL(true)); - eq(await exe('<: (1 >= 1)'), BOOL(true)); - eq(await exe('<: (0 >= 1)'), BOOL(false)); - }); - - test.concurrent('<=', async () => { - eq(await exe('<: (2 <= 1)'), BOOL(false)); - eq(await exe('<: (1 <= 1)'), BOOL(true)); - eq(await exe('<: (0 <= 1)'), BOOL(true)); - }); - - test.concurrent('precedence', async () => { - eq(await exe('<: 1 + 2 * 3 + 4'), NUM(11)); - eq(await exe('<: 1 + 4 / 4 + 1'), NUM(3)); - eq(await exe('<: 1 + 1 == 2 && 2 * 2 == 4'), BOOL(true)); - eq(await exe('<: (1 + 1) * 2'), NUM(4)); - }); - - test.concurrent('negative numbers', async () => { - eq(await exe('<: 1+-1'), NUM(0)); - eq(await exe('<: 1--1'), NUM(2));//反直観的、禁止される可能性がある? - eq(await exe('<: -1*-1'), NUM(1)); - eq(await exe('<: -1==-1'), BOOL(true)); - eq(await exe('<: 1>-1'), BOOL(true)); - eq(await exe('<: -1<1'), BOOL(true)); - }); - -}); - -describe('Infix expression', () => { - test.concurrent('simple infix expression', async () => { - eq(await exe('<: 0 < 1'), BOOL(true)); - eq(await exe('<: 1 + 1'), NUM(2)); - }); - - test.concurrent('combination', async () => { - eq(await exe('<: 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10'), NUM(55)); - eq(await exe('<: Core:add(1, 3) * Core:mul(2, 5)'), NUM(40)); - }); - - test.concurrent('use parentheses to distinguish expr', async () => { - eq(await exe('<: (1 + 10) * (2 + 5)'), NUM(77)); - }); - - test.concurrent('syntax symbols vs infix operators', async () => { - const res = await exe(` - <: match true { - 1 == 1 => "true" - 1 < 1 => "false" - } - `); - eq(res, STR('true')); - }); - - test.concurrent('number + if expression', async () => { - eq(await exe('<: 1 + if true 1 else 2'), NUM(2)); - }); - - test.concurrent('number + match expression', async () => { - const res = await exe(` - <: 1 + match 2 == 2 { - true => 3 - false => 4 - } - `); - eq(res, NUM(4)); - }); - - test.concurrent('eval + eval', async () => { - eq(await exe('<: eval { 1 } + eval { 1 }'), NUM(2)); - }); - - test.concurrent('disallow line break', async () => { - try { - await exe(` - <: 1 + - 1 + 1 - `); - } catch (e) { - assert.ok(true); - return; - } - assert.fail(); - }); - - test.concurrent('escaped line break', async () => { - eq(await exe(` - <: 1 + \\ - 1 + 1 - `), NUM(3)); - }); - - test.concurrent('infix-to-fncall on namespace', async () => { - eq( - await exe(` - :: Hoge { - @add(x, y) { - x + y - } - } - <: Hoge:add(1, 2) - `), - NUM(3) - ); - }); -}); - -describe('Comment', () => { - test.concurrent('single line comment', async () => { - const res = await exe(` - // let a = ... - let a = 42 - <: a - `); - eq(res, NUM(42)); - }); - - test.concurrent('multi line comment', async () => { - const res = await exe(` - /* variable declaration here... - let a = ... - */ - let a = 42 - <: a - `); - eq(res, NUM(42)); - }); - - test.concurrent('multi line comment 2', async () => { - const res = await exe(` - /* variable declaration here... - let a = ... - */ - let a = 42 - /* - another comment here - */ - <: a - `); - eq(res, NUM(42)); - }); - - test.concurrent('// as string', async () => { - const res = await exe('<: "//"'); - eq(res, STR('//')); - }); -}); - test.concurrent('式にコロンがあってもオブジェクトと判定されない', async () => { const res = await exe(` <: eval { @@ -434,14 +53,6 @@ test.concurrent('dec', async () => { eq(res, NUM(-6)); }); -test.concurrent('var', async () => { - const res = await exe(` - let a = 42 - <: a - `); - eq(res, NUM(42)); -}); - test.concurrent('参照が繋がらない', async () => { const res = await exe(` var f = @() { "a" } @@ -453,32 +64,6 @@ test.concurrent('参照が繋がらない', async () => { eq(res, STR('a')); }); -describe('Cannot put multiple statements in a line', () => { - test.concurrent('var def', async () => { - try { - await exe(` - let a = 42 let b = 11 - `); - } catch (e) { - assert.ok(true); - return; - } - assert.fail(); - }); - - test.concurrent('var def (op)', async () => { - try { - await exe(` - let a = 13 + 75 let b = 24 + 146 - `); - } catch (e) { - assert.ok(true); - return; - } - assert.fail(); - }); -}); - test.concurrent('empty function', async () => { const res = await exe(` @hoge() { } @@ -522,8 +107,8 @@ test.concurrent('Closure (counter)', async () => { @create_counter() { var count = 0 { - get_count: @() { count }; - count: @() { count = (count + 1) }; + get_count: @() { count }, + count: @() { count = (count + 1) }, } } @@ -551,331 +136,96 @@ test.concurrent('Recursion', async () => { eq(res, NUM(120)); }); -describe('Var name starts with reserved word', () => { - test.concurrent('let', async () => { +describe('Object', () => { + test.concurrent('property access', async () => { const res = await exe(` - @f() { - let letcat = "ai" - letcat + let obj = { + a: { + b: { + c: 42, + }, + }, } - <: f() + + <: obj.a.b.c `); - eq(res, STR('ai')); + eq(res, NUM(42)); }); - test.concurrent('var', async () => { + test.concurrent('property access (fn call)', async () => { const res = await exe(` - @f() { - let varcat = "ai" - varcat + @f() { 42 } + + let obj = { + a: { + b: { + c: f, + }, + }, } - <: f() + + <: obj.a.b.c() `); - eq(res, STR('ai')); + eq(res, NUM(42)); }); - test.concurrent('return', async () => { + test.concurrent('property assign', async () => { const res = await exe(` - @f() { - let returncat = "ai" - returncat + let obj = { + a: 1 + b: { + c: 2 + d: { + e: 3 + } + } } - <: f() + + obj.a = 24 + obj.b.d.e = 42 + + <: obj `); - eq(res, STR('ai')); + eq(res, OBJ(new Map([ + ['a', NUM(24)], + ['b', OBJ(new Map([ + ['c', NUM(2)], + ['d', OBJ(new Map([ + ['e', NUM(42)], + ]))], + ]))], + ]))); }); - test.concurrent('each', async () => { + /* 未実装 + test.concurrent('string key', async () => { const res = await exe(` - @f() { - let eachcat = "ai" - eachcat + let obj = { + "藍": 42, } - <: f() + + <: obj."藍" `); - eq(res, STR('ai')); + eq(res, NUM(42)); }); - test.concurrent('for', async () => { + test.concurrent('string key including colon and period', async () => { const res = await exe(` - @f() { - let forcat = "ai" - forcat + let obj = { + ":.:": 42, } - <: f() + + <: obj.":.:" `); - eq(res, STR('ai')); + eq(res, NUM(42)); }); - test.concurrent('loop', async () => { + test.concurrent('expression key', async () => { const res = await exe(` - @f() { - let loopcat = "ai" - loopcat - } - <: f() - `); - eq(res, STR('ai')); - }); - - test.concurrent('break', async () => { - const res = await exe(` - @f() { - let breakcat = "ai" - breakcat - } - <: f() - `); - eq(res, STR('ai')); - }); - - test.concurrent('continue', async () => { - const res = await exe(` - @f() { - let continuecat = "ai" - continuecat - } - <: f() - `); - eq(res, STR('ai')); - }); - - test.concurrent('if', async () => { - const res = await exe(` - @f() { - let ifcat = "ai" - ifcat - } - <: f() - `); - eq(res, STR('ai')); - }); - - test.concurrent('match', async () => { - const res = await exe(` - @f() { - let matchcat = "ai" - matchcat - } - <: f() - `); - eq(res, STR('ai')); - }); - - test.concurrent('true', async () => { - const res = await exe(` - @f() { - let truecat = "ai" - truecat - } - <: f() - `); - eq(res, STR('ai')); - }); - - test.concurrent('false', async () => { - const res = await exe(` - @f() { - let falsecat = "ai" - falsecat - } - <: f() - `); - eq(res, STR('ai')); - }); - - test.concurrent('null', async () => { - const res = await exe(` - @f() { - let nullcat = "ai" - nullcat - } - <: f() - `); - eq(res, STR('ai')); - }); -}); - -describe('name validation of reserved word', () => { - test.concurrent('def', async () => { - try { - await exe(` - let let = 1 - `); - } catch (e) { - assert.ok(true); - return; - } - assert.fail(); - }); - - test.concurrent('attr', async () => { - try { - await exe(` - #[let 1] - @f() { 1 } - `); - } catch (e) { - assert.ok(true); - return; - } - assert.fail(); - }); - - test.concurrent('ns', async () => { - try { - await exe(` - :: let { - @f() { 1 } - } - `); - } catch (e) { - assert.ok(true); - return; - } - assert.fail(); - }); - - test.concurrent('var', async () => { - try { - await exe(` - let - `); - } catch (e) { - assert.ok(true); - return; - } - assert.fail(); - }); - - test.concurrent('prop', async () => { - try { - await exe(` - let x = { let: 1 } - x.let - `); - } catch (e) { - assert.ok(true); - return; - } - assert.fail(); - }); - - test.concurrent('meta', async () => { - try { - await exe(` - ### let 1 - `); - } catch (e) { - assert.ok(true); - return; - } - assert.fail(); - }); - - test.concurrent('fn', async () => { - try { - await exe(` - @let() { 1 } - `); - } catch (e) { - assert.ok(true); - return; - } - assert.fail(); - }); -}); - -describe('Object', () => { - test.concurrent('property access', async () => { - const res = await exe(` - let obj = { - a: { - b: { - c: 42; - }; - }; - } - - <: obj.a.b.c - `); - eq(res, NUM(42)); - }); - - test.concurrent('property access (fn call)', async () => { - const res = await exe(` - @f() { 42 } - - let obj = { - a: { - b: { - c: f; - }; - }; - } - - <: obj.a.b.c() - `); - eq(res, NUM(42)); - }); - - test.concurrent('property assign', async () => { - const res = await exe(` - let obj = { - a: 1 - b: { - c: 2 - d: { - e: 3 - } - } - } - - obj.a = 24 - obj.b.d.e = 42 - - <: obj - `); - eq(res, OBJ(new Map([ - ['a', NUM(24)], - ['b', OBJ(new Map([ - ['c', NUM(2)], - ['d', OBJ(new Map([ - ['e', NUM(42)], - ]))], - ]))], - ]))); - }); - - /* 未実装 - test.concurrent('string key', async () => { - const res = await exe(` - let obj = { - "藍": 42; - } - - <: obj."藍" - `); - eq(res, NUM(42)); - }); - - test.concurrent('string key including colon and period', async () => { - const res = await exe(` - let obj = { - ":.:": 42; - } - - <: obj.":.:" - `); - eq(res, NUM(42)); - }); - - test.concurrent('expression key', async () => { - const res = await exe(` - let key = "藍" - - let obj = { - : 42; + let key = "藍" + + let obj = { + : 42, } <: obj @@ -968,8 +318,8 @@ describe('chain', () => { const res = await exe(` let obj = { a: { - b: [@(name) { name }, @(str) { "chan" }, @() { "kawaii" }]; - }; + b: [@(name) { name }, @(str) { "chan" }, @() { "kawaii" }], + }, } <: obj.a.b[0]("ai") @@ -981,8 +331,8 @@ describe('chain', () => { const res = await exe(` let obj = { a: { - b: ["ai", "chan", "kawaii"]; - }; + b: ["ai", "chan", "kawaii"], + }, } obj.a.b[1] = "taso" @@ -1000,8 +350,8 @@ describe('chain', () => { const res = await exe(` let obj = { a: { - b: ["ai", "chan", "kawaii"]; - }; + b: ["ai", "chan", "kawaii"], + }, } var x = null @@ -1016,8 +366,8 @@ describe('chain', () => { const res = await exe(` let arr = [ { - a: 1; - b: 2; + a: 1, + b: 2, } ] @@ -1038,8 +388,8 @@ describe('chain', () => { const res = await exe(` let obj = { a: { - b: [1, 2, 3]; - }; + b: [1, 2, 3], + }, } obj.a.b[1] += 1 @@ -1190,43 +540,6 @@ describe('chain', () => { }); }); -describe('Template syntax', () => { - test.concurrent('Basic', async () => { - const res = await exe(` - let str = "kawaii" - <: \`Ai is {str}!\` - `); - eq(res, STR('Ai is kawaii!')); - }); - - test.concurrent('convert to str', async () => { - const res = await exe(` - <: \`1 + 1 = {(1 + 1)}\` - `); - eq(res, STR('1 + 1 = 2')); - }); - - test.concurrent('invalid', async () => { - try { - await exe(` - <: \`{hoge}\` - `); - } catch (e) { - assert.ok(true); - return; - } - assert.fail(); - }); - - test.concurrent('Escape', async () => { - const res = await exe(` - let message = "Hello" - <: \`\\\`a\\{b\\}c\\\`\` - `); - eq(res, STR('`a{b}c`')); - }); -}); - test.concurrent('Throws error when divided by zero', async () => { try { await exe(` @@ -1270,14 +583,49 @@ describe('Function call', () => { eq(res, NUM(2)); }); - test.concurrent('with args (separated by space)', async () => { + test.concurrent('optional args', async () => { const res = await exe(` - @f(x y) { - (x + y) + @f(x, y?, z?) { + [x, y, z] } - <: f(1 1) + <: f(true) `); - eq(res, NUM(2)); + eq(res, ARR([TRUE, NULL, NULL])); + }); + + test.concurrent('args with default value', async () => { + const res = await exe(` + @f(x, y=1, z=2) { + [x, y, z] + } + <: f(5, 3) + `); + eq(res, ARR([NUM(5), NUM(3), NUM(2)])); + }); + + test.concurrent('args must not be both optional and default-valued', async () => { + try { + Parser.parse(` + @func(a? = 1){} + `); + } catch (e) { + assert.ok(e instanceof AiScriptSyntaxError); + return; + } + assert.fail(); + }); + + test.concurrent('missing arg', async () => { + try { + await exe(` + @func(a){} + func() + `); + } catch (e) { + assert.ok(e instanceof AiScriptRuntimeError); + return; + } + assert.fail(); }); test.concurrent('std: throw AiScript error when required arg missing', async () => { @@ -1291,17 +639,7 @@ describe('Function call', () => { } assert.fail(); }); - - test.concurrent('omitted args', async () => { - const res = await exe(` - @f(x, y) { - [x, y] - } - <: f(1) - `); - eq(res, ARR([NUM(1), NULL])); - }); -}); +}); describe('Return', () => { test.concurrent('Early return', async () => { @@ -1459,2277 +797,270 @@ describe('Return', () => { }); }); -describe('Eval', () => { - test.concurrent('returns value', async () => { +describe('type declaration', () => { + test.concurrent('def', async () => { const res = await exe(` - let foo = eval { - let a = 1 - let b = 2 - (a + b) - } - - <: foo + let abc: num = 1 + var xyz: str = "abc" + <: [abc, xyz] `); - eq(res, NUM(3)); + eq(res, ARR([NUM(1), STR('abc')])); }); -}); -describe('exists', () => { - test.concurrent('Basic', async () => { + test.concurrent('fn def', async () => { const res = await exe(` - let foo = null - <: [(exists foo) (exists bar)] - `); - eq(res, ARR([BOOL(true), BOOL(false)])); - }); -}); - -describe('if', () => { - test.concurrent('if', async () => { - const res1 = await exe(` - var msg = "ai" - if true { - msg = "kawaii" - } - <: msg - `); - eq(res1, STR('kawaii')); - - const res2 = await exe(` - var msg = "ai" - if false { - msg = "kawaii" - } - <: msg - `); - eq(res2, STR('ai')); - }); - - test.concurrent('else', async () => { - const res1 = await exe(` - var msg = null - if true { - msg = "ai" - } else { - msg = "kawaii" + @f(x: arr, y: str, z: @(num) => bool): arr { + x.push(0) + y = "abc" + var r: bool = z(x[0]) + x.push(if r 5 else 10) + x } - <: msg - `); - eq(res1, STR('ai')); - const res2 = await exe(` - var msg = null - if false { - msg = "ai" - } else { - msg = "kawaii" - } - <: msg + <: f([1, 2, 3], "a", @(n) { n == 1 }) `); - eq(res2, STR('kawaii')); + eq(res, ARR([NUM(1), NUM(2), NUM(3), NUM(0), NUM(5)])); }); +}); - test.concurrent('elif', async () => { - const res1 = await exe(` - var msg = "bebeyo" - if false { - msg = "ai" - } elif true { - msg = "kawaii" - } - <: msg - `); - eq(res1, STR('kawaii')); - - const res2 = await exe(` - var msg = "bebeyo" - if false { - msg = "ai" - } elif false { - msg = "kawaii" +describe('Attribute', () => { + test.concurrent('single attribute with function (str)', async () => { + let node: Ast.Node; + let attr: Ast.Attribute; + const parser = new Parser(); + const nodes = parser.parse(` + #[Event "Recieved"] + @onRecieved(data) { + data } - <: msg `); - eq(res2, STR('bebeyo')); + assert.equal(nodes.length, 1); + node = nodes[0]; + if (node.type !== 'def') assert.fail(); + assert.equal(node.name, 'onRecieved'); + assert.equal(node.attr.length, 1); + // attribute 1 + attr = node.attr[0]; + if (attr.type !== 'attr') assert.fail(); + assert.equal(attr.name, 'Event'); + if (attr.value.type !== 'str') assert.fail(); + assert.equal(attr.value.value, 'Recieved'); }); - test.concurrent('if ~ elif ~ else', async () => { - const res1 = await exe(` - var msg = null - if false { - msg = "ai" - } elif true { - msg = "chan" - } else { - msg = "kawaii" + test.concurrent('multiple attributes with function (obj, str, bool)', async () => { + let node: Ast.Node; + let attr: Ast.Attribute; + const parser = new Parser(); + const nodes = parser.parse(` + #[Endpoint { path: "/notes/create" }] + #[Desc "Create a note."] + #[Cat true] + @createNote(text) { + <: text } - <: msg `); - eq(res1, STR('chan')); - - const res2 = await exe(` - var msg = null - if false { - msg = "ai" - } elif false { - msg = "chan" - } else { - msg = "kawaii" + assert.equal(nodes.length, 1); + node = nodes[0]; + if (node.type !== 'def') assert.fail(); + assert.equal(node.name, 'createNote'); + assert.equal(node.attr.length, 3); + // attribute 1 + attr = node.attr[0]; + if (attr.type !== 'attr') assert.fail(); + assert.equal(attr.name, 'Endpoint'); + if (attr.value.type !== 'obj') assert.fail(); + assert.equal(attr.value.value.size, 1); + for (const [k, v] of attr.value.value) { + if (k === 'path') { + if (v.type !== 'str') assert.fail(); + assert.equal(v.value, '/notes/create'); + } + else { + assert.fail(); + } } - <: msg - `); - eq(res2, STR('kawaii')); + // attribute 2 + attr = node.attr[1]; + if (attr.type !== 'attr') assert.fail(); + assert.equal(attr.name, 'Desc'); + if (attr.value.type !== 'str') assert.fail(); + assert.equal(attr.value.value, 'Create a note.'); + // attribute 3 + attr = node.attr[2]; + if (attr.type !== 'attr') assert.fail(); + assert.equal(attr.name, 'Cat'); + if (attr.value.type !== 'bool') assert.fail(); + assert.equal(attr.value.value, true); }); - test.concurrent('expr', async () => { - const res1 = await exe(` - <: if true "ai" else "kawaii" - `); - eq(res1, STR('ai')); + // TODO: attributed function in block + // TODO: attribute target does not exist - const res2 = await exe(` - <: if false "ai" else "kawaii" + test.concurrent('single attribute (no value)', async () => { + let node: Ast.Node; + let attr: Ast.Attribute; + const parser = new Parser(); + const nodes = parser.parse(` + #[serializable] + let data = 1 `); - eq(res2, STR('kawaii')); + assert.equal(nodes.length, 1); + node = nodes[0]; + if (node.type !== 'def') assert.fail(); + assert.equal(node.name, 'data'); + assert.equal(node.attr.length, 1); + // attribute 1 + attr = node.attr[0]; + assert.ok(attr.type === 'attr'); + assert.equal(attr.name, 'serializable'); + if (attr.value.type !== 'bool') assert.fail(); + assert.equal(attr.value.value, true); }); }); -describe('match', () => { - test.concurrent('Basic', async () => { - const res = await exe(` - <: match 2 { - 1 => "a" - 2 => "b" - 3 => "c" - } +describe('Location', () => { + test.concurrent('function', async () => { + let node: Ast.Node; + const parser = new Parser(); + const nodes = parser.parse(` + @f(a) { a } `); - eq(res, STR('b')); + assert.equal(nodes.length, 1); + node = nodes[0]; + if (!node.loc) assert.fail(); + assert.deepEqual(node.loc, { + start: { line: 2, column: 4 }, + end: { line: 2, column: 15 }, + }); }); - - test.concurrent('When default not provided, returns null', async () => { - const res = await exe(` - <: match 42 { - 1 => "a" - 2 => "b" - 3 => "c" - } + test.concurrent('comment', async () => { + let node: Ast.Node; + const parser = new Parser(); + const nodes = parser.parse(` + /* + */ + // hoge + @f(a) { a } `); - eq(res, NULL); + assert.equal(nodes.length, 1); + node = nodes[0]; + if (!node.loc) assert.fail(); + assert.deepEqual(node.loc.start, { line: 5, column: 3 }); }); - - test.concurrent('With default', async () => { - const res = await exe(` - <: match 42 { - 1 => "a" - 2 => "b" - 3 => "c" - * => "d" - } + test.concurrent('template', async () => { + let node: Ast.Node; + const parser = new Parser(); + const nodes = parser.parse(` + \`hoge{1}fuga\` `); - eq(res, STR('d')); + assert.equal(nodes.length, 1); + node = nodes[0]; + if (!node.loc || node.type !== "tmpl") assert.fail(); + assert.deepEqual(node.loc, { + start: { line: 2, column: 4 }, + end: { line: 2, column: 17 }, + }); + assert.equal(node.tmpl.length, 3); + const [elem1, elem2, elem3] = node.tmpl as Ast.Expression[]; + assert.deepEqual(elem1.loc, { + start: { line: 2, column: 4 }, + end: { line: 2, column: 10 }, + }); + assert.deepEqual(elem2.loc, { + start: { line: 2, column: 10 }, + end: { line: 2, column: 11 }, + }); + assert.deepEqual(elem3.loc, { + start: { line: 2, column: 11 }, + end: { line: 2, column: 17 }, + }); }); +}); - test.concurrent('With block', async () => { +describe('Unicode', () => { + test.concurrent('len', async () => { const res = await exe(` - <: match 2 { - 1 => 1 - 2 => { - let a = 1 - let b = 2 - (a + b) - } - 3 => 3 - } + <: "👍🏽🍆🌮".len `); eq(res, NUM(3)); }); - test.concurrent('With return', async () => { + test.concurrent('pick', async () => { const res = await exe(` - @f(x) { - match x { - 1 => { - return "ai" - } - } - "foo" - } - <: f(1) + <: "👍🏽🍆🌮".pick(1) `); - eq(res, STR('ai')); + eq(res, STR('🍆')); }); -}); -describe('loop', () => { - test.concurrent('Basic', async () => { + test.concurrent('slice', async () => { const res = await exe(` - var count = 0 - loop { - if (count == 10) break - count = (count + 1) - } - <: count + <: "Emojis 👍🏽 are 🍆 poison. 🌮s are bad.".slice(7, 14) `); - eq(res, NUM(10)); + eq(res, STR('👍🏽 are 🍆')); }); - test.concurrent('with continue', async () => { + test.concurrent('split', async () => { const res = await exe(` - var a = ["ai" "chan" "kawaii" "yo" "!"] - var b = [] - loop { - var x = a.shift() - if (x == "chan") continue - if (x == "yo") break - b.push(x) - } - <: b + <: "👍🏽🍆🌮".split() `); - eq(res, ARR([STR('ai'), STR('kawaii')])); + eq(res, ARR([STR('👍🏽'), STR('🍆'), STR('🌮')])); }); }); -describe('for', () => { - test.concurrent('Basic', async () => { - const res = await exe(` - var count = 0 - for (let i, 10) { - count += i + 1 +describe('Security', () => { + test.concurrent('Cannot access js native property via var', async () => { + try { + await exe(` + <: constructor + `); + assert.fail(); + } catch (e) { + assert.ok(e instanceof AiScriptSyntaxError); } - <: count - `); - eq(res, NUM(55)); - }); - test.concurrent('initial value', async () => { - const res = await exe(` - var count = 0 - for (let i = 2, 10) { - count += i + try { + await exe(` + <: prototype + `); + assert.fail(); + } catch (e) { + assert.ok(e instanceof AiScriptRuntimeError); } - <: count - `); - eq(res, NUM(65)); - }); - test.concurrent('wuthout iterator', async () => { - const res = await exe(` - var count = 0 - for (10) { - count = (count + 1) + try { + await exe(` + <: __proto__ + `); + assert.fail(); + } catch (e) { + assert.ok(e instanceof AiScriptRuntimeError); } - <: count - `); - eq(res, NUM(10)); }); - test.concurrent('without brackets', async () => { - const res = await exe(` - var count = 0 - for let i, 10 { - count = (count + i) - } - <: count - `); - eq(res, NUM(45)); - }); + test.concurrent('Cannot access js native property via object', async () => { + const res2 = await exe(` + let obj = {} - test.concurrent('Break', async () => { - const res = await exe(` - var count = 0 - for (let i, 20) { - if (i == 11) break - count += i - } - <: count + <: obj.prototype `); - eq(res, NUM(55)); - }); + eq(res2, NULL); - test.concurrent('continue', async () => { - const res = await exe(` - var count = 0 - for (let i, 10) { - if (i == 5) continue - count = (count + 1) - } - <: count - `); - eq(res, NUM(9)); - }); + const res3 = await exe(` + let obj = {} - test.concurrent('single statement', async () => { - const res = await exe(` - var count = 0 - for 10 count += 1 - <: count + <: obj.__proto__ `); - eq(res, NUM(10)); + eq(res3, NULL); }); - test.concurrent('var name without space', async () => { + test.concurrent('Cannot access js native property via primitive prop', async () => { try { await exe(` - for (leti, 10) { - <: i - } + <: "".constructor `); + assert.fail(); } catch (e) { - assert.ok(true); - return; - } - assert.fail(); - }); -}); - -describe('for of', () => { - test.concurrent('standard', async () => { - const res = await exe(` - let msgs = [] - each let item, ["ai", "chan", "kawaii"] { - msgs.push([item, "!"].join()) - } - <: msgs - `); - eq(res, ARR([STR('ai!'), STR('chan!'), STR('kawaii!')])); - }); - - test.concurrent('Break', async () => { - const res = await exe(` - let msgs = [] - each let item, ["ai", "chan", "kawaii" "yo"] { - if (item == "kawaii") break - msgs.push([item, "!"].join()) - } - <: msgs - `); - eq(res, ARR([STR('ai!'), STR('chan!')])); - }); - - test.concurrent('single statement', async () => { - const res = await exe(` - let msgs = [] - each let item, ["ai", "chan", "kawaii"] msgs.push([item, "!"].join()) - <: msgs - `); - eq(res, ARR([STR('ai!'), STR('chan!'), STR('kawaii!')])); - }); - - test.concurrent('var name without space', async () => { - try { - await exe(` - each letitem, ["ai", "chan", "kawaii"] { - <: item - } - `); - } catch (e) { - assert.ok(true); - return; - } - assert.fail(); - }); -}); - -describe('not', () => { - test.concurrent('Basic', async () => { - const res = await exe(` - <: !true - `); - eq(res, BOOL(false)); - }); -}); - -describe('namespace', () => { - test.concurrent('standard', async () => { - const res = await exe(` - <: Foo:bar() - - :: Foo { - @bar() { "ai" } - } - `); - eq(res, STR('ai')); - }); - - test.concurrent('self ref', async () => { - const res = await exe(` - <: Foo:bar() - - :: Foo { - let ai = "kawaii" - @bar() { ai } - } - `); - eq(res, STR('kawaii')); - }); - - test.concurrent('cannot declare mutable variable', async () => { - try { - await exe(` - :: Foo { - var ai = "kawaii" - } - `); - } catch (e) { - assert.ok(true); - return; - } - assert.fail(); - }); - - test.concurrent('nested', async () => { - const res = await exe(` - <: Foo:Bar:baz() - - :: Foo { - :: Bar { - @baz() { "ai" } - } - } - `); - eq(res, STR('ai')); - }); - - test.concurrent('nested ref', async () => { - const res = await exe(` - <: Foo:baz - - :: Foo { - let baz = Bar:ai - :: Bar { - let ai = "kawaii" - } - } - `); - eq(res, STR('kawaii')); - }); -}); - -describe('literal', () => { - test.concurrent('string (single quote)', async () => { - const res = await exe(` - <: 'foo' - `); - eq(res, STR('foo')); - }); - - test.concurrent('string (double quote)', async () => { - const res = await exe(` - <: "foo" - `); - eq(res, STR('foo')); - }); - - test.concurrent('Escaped double quote', async () => { - const res = await exe('<: "ai saw a note \\"bebeyo\\"."'); - eq(res, STR('ai saw a note "bebeyo".')); - }); - - test.concurrent('Escaped single quote', async () => { - const res = await exe('<: \'ai saw a note \\\'bebeyo\\\'.\''); - eq(res, STR('ai saw a note \'bebeyo\'.')); - }); - - test.concurrent('bool (true)', async () => { - const res = await exe(` - <: true - `); - eq(res, BOOL(true)); - }); - - test.concurrent('bool (false)', async () => { - const res = await exe(` - <: false - `); - eq(res, BOOL(false)); - }); - - test.concurrent('number (Int)', async () => { - const res = await exe(` - <: 10 - `); - eq(res, NUM(10)); - }); - - test.concurrent('number (Float)', async () => { - const res = await exe(` - <: 0.5 - `); - eq(res, NUM(0.5)); - }); - - test.concurrent('arr (separated by comma)', async () => { - const res = await exe(` - <: [1, 2, 3] - `); - eq(res, ARR([NUM(1), NUM(2), NUM(3)])); - }); - - test.concurrent('arr (separated by comma) (with trailing comma)', async () => { - const res = await exe(` - <: [1, 2, 3,] - `); - eq(res, ARR([NUM(1), NUM(2), NUM(3)])); - }); - - test.concurrent('arr (separated by line break)', async () => { - const res = await exe(` - <: [ - 1 - 2 - 3 - ] - `); - eq(res, ARR([NUM(1), NUM(2), NUM(3)])); - }); - - test.concurrent('arr (separated by line break and comma)', async () => { - const res = await exe(` - <: [ - 1, - 2, - 3 - ] - `); - eq(res, ARR([NUM(1), NUM(2), NUM(3)])); - }); - - test.concurrent('arr (separated by line break and comma) (with trailing comma)', async () => { - const res = await exe(` - <: [ - 1, - 2, - 3, - ] - `); - eq(res, ARR([NUM(1), NUM(2), NUM(3)])); - }); - - test.concurrent('obj (separated by comma)', async () => { - const res = await exe(` - <: { a: 1, b: 2, c: 3 } - `); - eq(res, OBJ(new Map([['a', NUM(1)], ['b', NUM(2)], ['c', NUM(3)]]))); - }); - - test.concurrent('obj (separated by comma) (with trailing comma)', async () => { - const res = await exe(` - <: { a: 1, b: 2, c: 3, } - `); - eq(res, OBJ(new Map([['a', NUM(1)], ['b', NUM(2)], ['c', NUM(3)]]))); - }); - - test.concurrent('obj (separated by semicolon)', async () => { - const res = await exe(` - <: { a: 1; b: 2; c: 3 } - `); - eq(res, OBJ(new Map([['a', NUM(1)], ['b', NUM(2)], ['c', NUM(3)]]))); - }); - - test.concurrent('obj (separated by semicolon) (with trailing semicolon)', async () => { - const res = await exe(` - <: { a: 1; b: 2; c: 3; } - `); - eq(res, OBJ(new Map([['a', NUM(1)], ['b', NUM(2)], ['c', NUM(3)]]))); - }); - - test.concurrent('obj (separated by line break)', async () => { - const res = await exe(` - <: { - a: 1 - b: 2 - c: 3 - } - `); - eq(res, OBJ(new Map([['a', NUM(1)], ['b', NUM(2)], ['c', NUM(3)]]))); - }); - - test.concurrent('obj (separated by line break and semicolon)', async () => { - const res = await exe(` - <: { - a: 1; - b: 2; - c: 3 - } - `); - eq(res, OBJ(new Map([['a', NUM(1)], ['b', NUM(2)], ['c', NUM(3)]]))); - }); - - test.concurrent('obj (separated by line break and semicolon) (with trailing semicolon)', async () => { - const res = await exe(` - <: { - a: 1; - b: 2; - c: 3; - } - `); - eq(res, OBJ(new Map([['a', NUM(1)], ['b', NUM(2)], ['c', NUM(3)]]))); - }); - - test.concurrent('obj and arr (separated by line break)', async () => { - const res = await exe(` - <: { - a: 1 - b: [ - 1 - 2 - 3 - ] - c: 3 - } - `); - eq(res, OBJ(new Map([ - ['a', NUM(1)], - ['b', ARR([NUM(1), NUM(2), NUM(3)])], - ['c', NUM(3)] - ]))); - }); -}); - -describe('type declaration', () => { - test.concurrent('def', async () => { - const res = await exe(` - let abc: num = 1 - var xyz: str = "abc" - <: [abc xyz] - `); - eq(res, ARR([NUM(1), STR('abc')])); - }); - - test.concurrent('fn def', async () => { - const res = await exe(` - @f(x: arr, y: str, z: @(num) => bool): arr { - x.push(0) - y = "abc" - var r: bool = z(x[0]) - x.push(if r 5 else 10) - x - } - - <: f([1, 2, 3], "a", @(n) { n == 1 }) - `); - eq(res, ARR([NUM(1), NUM(2), NUM(3), NUM(0), NUM(5)])); - }); -}); - -describe('meta', () => { - test.concurrent('default meta', async () => { - const res = getMeta(` - ### { a: 1; b: 2; c: 3; } - `); - assert.deepEqual(res, new Map([ - [null, { - a: 1, - b: 2, - c: 3, - }] - ])); - assert.deepEqual(res!.get(null), { - a: 1, - b: 2, - c: 3, - }); - }); - - describe('String', () => { - test.concurrent('valid', async () => { - const res = getMeta(` - ### x "hoge" - `); - assert.deepEqual(res, new Map([ - ['x', 'hoge'] - ])); - }); - }); - - describe('Number', () => { - test.concurrent('valid', async () => { - const res = getMeta(` - ### x 42 - `); - assert.deepEqual(res, new Map([ - ['x', 42] - ])); - }); - }); - - describe('Boolean', () => { - test.concurrent('valid', async () => { - const res = getMeta(` - ### x true - `); - assert.deepEqual(res, new Map([ - ['x', true] - ])); - }); - }); - - describe('Null', () => { - test.concurrent('valid', async () => { - const res = getMeta(` - ### x null - `); - assert.deepEqual(res, new Map([ - ['x', null] - ])); - }); - }); - - describe('Array', () => { - test.concurrent('valid', async () => { - const res = getMeta(` - ### x [1 2 3] - `); - assert.deepEqual(res, new Map([ - ['x', [1, 2, 3]] - ])); - }); - - test.concurrent('invalid', async () => { - try { - getMeta(` - ### x [1 (2 + 2) 3] - `); - } catch (e) { - assert.ok(true); - return; - } - assert.fail(); - }); - }); - - describe('Object', () => { - test.concurrent('valid', async () => { - const res = getMeta(` - ### x { a: 1; b: 2; c: 3; } - `); - assert.deepEqual(res, new Map([ - ['x', { - a: 1, - b: 2, - c: 3, - }] - ])); - }); - - test.concurrent('invalid', async () => { - try { - getMeta(` - ### x { a: 1; b: (2 + 2); c: 3; } - `); - } catch (e) { - assert.ok(true); - return; - } - assert.fail(); - }); - }); - - describe('Template', () => { - test.concurrent('invalid', async () => { - try { - getMeta(` - ### x \`foo {bar} baz\` - `); - } catch (e) { - assert.ok(true); - return; - } - assert.fail(); - }); - }); - - describe('Expression', () => { - test.concurrent('invalid', async () => { - try { - getMeta(` - ### x (1 + 1) - `); - } catch (e) { - assert.ok(true); - return; - } - assert.fail(); - }); - }); -}); - -describe('lang version', () => { - test.concurrent('number', async () => { - const res = utils.getLangVersion(` - /// @2021 - @f(x) { - x - } - `); - assert.strictEqual(res, '2021'); - }); - - test.concurrent('chars', async () => { - const res = utils.getLangVersion(` - /// @ canary - const a = 1 - @f(x) { - x - } - f(a) - `); - assert.strictEqual(res, 'canary'); - }); - - test.concurrent('complex', async () => { - const res = utils.getLangVersion(` - /// @ 2.0-Alpha - @f(x) { - x - } - `); - assert.strictEqual(res, '2.0-Alpha'); - }); - - test.concurrent('no specified', async () => { - const res = utils.getLangVersion(` - @f(x) { - x - } - `); - assert.strictEqual(res, null); - }); -}); - -describe('Attribute', () => { - test.concurrent('single attribute with function (str)', async () => { - let node: Ast.Node; - let attr: Ast.Attribute; - const parser = new Parser(); - const nodes = parser.parse(` - #[Event "Recieved"] - @onRecieved(data) { - data - } - `); - assert.equal(nodes.length, 1); - node = nodes[0]; - if (node.type !== 'def') assert.fail(); - assert.equal(node.name, 'onRecieved'); - assert.equal(node.attr.length, 1); - // attribute 1 - attr = node.attr[0]; - if (attr.type !== 'attr') assert.fail(); - assert.equal(attr.name, 'Event'); - if (attr.value.type !== 'str') assert.fail(); - assert.equal(attr.value.value, 'Recieved'); - }); - - test.concurrent('multiple attributes with function (obj, str, bool)', async () => { - let node: Ast.Node; - let attr: Ast.Attribute; - const parser = new Parser(); - const nodes = parser.parse(` - #[Endpoint { path: "/notes/create"; }] - #[Desc "Create a note."] - #[Cat true] - @createNote(text) { - <: text - } - `); - assert.equal(nodes.length, 1); - node = nodes[0]; - if (node.type !== 'def') assert.fail(); - assert.equal(node.name, 'createNote'); - assert.equal(node.attr.length, 3); - // attribute 1 - attr = node.attr[0]; - if (attr.type !== 'attr') assert.fail(); - assert.equal(attr.name, 'Endpoint'); - if (attr.value.type !== 'obj') assert.fail(); - assert.equal(attr.value.value.size, 1); - for (const [k, v] of attr.value.value) { - if (k === 'path') { - if (v.type !== 'str') assert.fail(); - assert.equal(v.value, '/notes/create'); - } - else { - assert.fail(); - } - } - // attribute 2 - attr = node.attr[1]; - if (attr.type !== 'attr') assert.fail(); - assert.equal(attr.name, 'Desc'); - if (attr.value.type !== 'str') assert.fail(); - assert.equal(attr.value.value, 'Create a note.'); - // attribute 3 - attr = node.attr[2]; - if (attr.type !== 'attr') assert.fail(); - assert.equal(attr.name, 'Cat'); - if (attr.value.type !== 'bool') assert.fail(); - assert.equal(attr.value.value, true); - }); - - // TODO: attributed function in block - // TODO: attribute target does not exist - - test.concurrent('single attribute (no value)', async () => { - let node: Ast.Node; - let attr: Ast.Attribute; - const parser = new Parser(); - const nodes = parser.parse(` - #[serializable] - let data = 1 - `); - assert.equal(nodes.length, 1); - node = nodes[0]; - if (node.type !== 'def') assert.fail(); - assert.equal(node.name, 'data'); - assert.equal(node.attr.length, 1); - // attribute 1 - attr = node.attr[0]; - assert.ok(attr.type === 'attr'); - assert.equal(attr.name, 'serializable'); - if (attr.value.type !== 'bool') assert.fail(); - assert.equal(attr.value.value, true); - }); -}); - -describe('Location', () => { - test.concurrent('function', async () => { - let node: Ast.Node; - const parser = new Parser(); - const nodes = parser.parse(` - @f(a) { a } - `); - assert.equal(nodes.length, 1); - node = nodes[0]; - if (!node.loc) assert.fail(); - assert.deepEqual(node.loc, { start: 3, end: 13 }); - }); - test.concurrent('comment', async () => { - let node: Ast.Node; - const parser = new Parser(); - const nodes = parser.parse(` - /* - */ - // hoge - @f(a) { a } - `); - assert.equal(nodes.length, 1); - node = nodes[0]; - if (!node.loc) assert.fail(); - assert.deepEqual(node.loc, { start: 23, end: 33 }); - }); -}); - -describe('Variable declaration', () => { - test.concurrent('Do not assign to let (issue #328)', async () => { - const err = await exe(` - let hoge = 33 - hoge = 4 - `).then(() => undefined).catch(err => err); - - assert.ok(err instanceof AiScriptRuntimeError); - }); -}); - -describe('Variable assignment', () => { - test.concurrent('simple', async () => { - eq(await exe(` - var hoge = 25 - hoge = 7 - <: hoge - `), NUM(7)); - }); - test.concurrent('destructuring assignment', async () => { - eq(await exe(` - var hoge = 'foo' - var fuga = { value: 'bar' } - [{ value: hoge }, fuga] = [fuga, hoge] - <: [hoge, fuga] - `), ARR([STR('bar'), STR('foo')])); - }); -}); - -describe('primitive props', () => { - describe('num', () => { - test.concurrent('to_str', async () => { - const res = await exe(` - let num = 123 - <: num.to_str() - `); - eq(res, STR('123')); - }); - }); - - describe('str', () => { - test.concurrent('len', async () => { - const res = await exe(` - let str = "hello" - <: str.len - `); - eq(res, NUM(5)); - }); - - test.concurrent('to_num', async () => { - const res = await exe(` - let str = "123" - <: str.to_num() - `); - eq(res, NUM(123)); - }); - - test.concurrent('upper', async () => { - const res = await exe(` - let str = "hello" - <: str.upper() - `); - eq(res, STR('HELLO')); - }); - - test.concurrent('lower', async () => { - const res = await exe(` - let str = "HELLO" - <: str.lower() - `); - eq(res, STR('hello')); - }); - - test.concurrent('trim', async () => { - const res = await exe(` - let str = " hello " - <: str.trim() - `); - eq(res, STR('hello')); - }); - - test.concurrent('replace', async () => { - const res = await exe(` - let str = "hello" - <: str.replace("l", "x") - `); - eq(res, STR('hexxo')); - }); - - test.concurrent('index_of', async () => { - const res = await exe(` - let str = '0123401234' - <: [ - str.index_of('3') == 3, - str.index_of('5') == -1, - str.index_of('3', 3) == 3, - str.index_of('3', 4) == 8, - str.index_of('3', -1) == -1, - str.index_of('3', -2) == 8, - str.index_of('3', -7) == 3, - str.index_of('3', 10) == -1, - ].map(@(v){if (v) '1' else '0'}).join() - `); - eq(res, STR('11111111')); - }); - - test.concurrent('incl', async () => { - const res = await exe(` - let str = "hello" - <: [str.incl("ll"), str.incl("x")] - `); - eq(res, ARR([TRUE, FALSE])); - }); - - test.concurrent('split', async () => { - const res = await exe(` - let str = "a,b,c" - <: str.split(",") - `); - eq(res, ARR([STR('a'), STR('b'), STR('c')])); - }); - - test.concurrent('pick', async () => { - const res = await exe(` - let str = "hello" - <: str.pick(1) - `); - eq(res, STR('e')); - }); - - test.concurrent('slice', async () => { - const res = await exe(` - let str = "hello" - <: str.slice(1, 3) - `); - eq(res, STR('el')); - }); - - test.concurrent("codepoint_at", async () => { - const res = await exe(` - let str = "𩸽" - <: str.codepoint_at(0) - `); - eq(res, NUM(171581)); - }); - - test.concurrent("to_arr", async () => { - const res = await exe(` - let str = "𩸽👉🏿👨‍👦" - <: str.to_arr() - `); - eq( - res, - ARR([STR("𩸽"), STR("👉🏿"), STR("👨‍👦")]) - ); - }); - - test.concurrent("to_unicode_arr", async () => { - const res = await exe(` - let str = "𩸽👉🏿👨‍👦" - <: str.to_unicode_arr() - `); - eq( - res, - ARR([STR("𩸽"), STR("👉"), STR(String.fromCodePoint(0x1F3FF)), STR("👨"), STR("\u200d"), STR("👦")]) - ); - }); - - test.concurrent("to_unicode_codepoint_arr", async () => { - const res = await exe(` - let str = "𩸽👉🏿👨‍👦" - <: str.to_unicode_codepoint_arr() - `); - eq( - res, - ARR([NUM(171581), NUM(128073), NUM(127999), NUM(128104), NUM(8205), NUM(128102)]) - ); - }); - - test.concurrent("to_char_arr", async () => { - const res = await exe(` - let str = "abc𩸽👉🏿👨‍👦def" - <: str.to_char_arr() - `); - eq( - res, - ARR([97, 98, 99, 55399, 56893, 55357, 56393, 55356, 57343, 55357, 56424, 8205, 55357, 56422, 100, 101, 102].map((s) => STR(String.fromCharCode(s)))) - ); - }); - - test.concurrent("to_charcode_arr", async () => { - const res = await exe(` - let str = "abc𩸽👉🏿👨‍👦def" - <: str.to_charcode_arr() - `); - eq( - res, - ARR([NUM(97), NUM(98), NUM(99), NUM(55399), NUM(56893), NUM(55357), NUM(56393), NUM(55356), NUM(57343), NUM(55357), NUM(56424), NUM(8205), NUM(55357), NUM(56422), NUM(100), NUM(101), NUM(102)]) - ); - }); - - test.concurrent("to_utf8_byte_arr", async () => { - const res = await exe(` - let str = "abc𩸽👉🏿👨‍👦def" - <: str.to_utf8_byte_arr() - `); - eq( - res, - ARR([NUM(97), NUM(98), NUM(99), NUM(240), NUM(169), NUM(184), NUM(189), NUM(240), NUM(159), NUM(145), NUM(137), NUM(240), NUM(159), NUM(143), NUM(191), NUM(240), NUM(159), NUM(145), NUM(168), NUM(226), NUM(128), NUM(141), NUM(240), NUM(159), NUM(145), NUM(166), NUM(100), NUM(101), NUM(102)]) - ); - }); - - test.concurrent('starts_with (no index)', async () => { - const res = await exe(` - let str = "hello" - let empty = "" - <: [ - str.starts_with(""), str.starts_with("hello"), - str.starts_with("he"), str.starts_with("ell"), - empty.starts_with(""), empty.starts_with("he"), - ] - `); - eq(res, ARR([ - TRUE, TRUE, - TRUE, FALSE, - TRUE, FALSE, - ])); - }); - - test.concurrent('starts_with (with index)', async () => { - const res = await exe(` - let str = "hello" - let empty = "" - <: [ - str.starts_with("", 4), str.starts_with("he", 0), - str.starts_with("ll", 2), str.starts_with("lo", 3), - str.starts_with("lo", -2), str.starts_with("hel", -5), - str.starts_with("he", 2), str.starts_with("loa", 3), - str.starts_with("lo", -6), str.starts_with("", -7), - str.starts_with("lo", 6), str.starts_with("", 7), - empty.starts_with("", 2), empty.starts_with("ll", 2), - ] - `); - eq(res, ARR([ - TRUE, TRUE, - TRUE, TRUE, - TRUE, TRUE, - FALSE, FALSE, - FALSE, TRUE, - FALSE, TRUE, - TRUE, FALSE, - ])); - }); - - test.concurrent('ends_with (no index)', async () => { - const res = await exe(` - let str = "hello" - let empty = "" - <: [ - str.ends_with(""), str.ends_with("hello"), - str.ends_with("lo"), str.ends_with("ell"), - empty.ends_with(""), empty.ends_with("he"), - ] - `); - eq(res, ARR([ - TRUE, TRUE, - TRUE, FALSE, - TRUE, FALSE, - ])); - }); - - test.concurrent('ends_with (with index)', async () => { - const res = await exe(` - let str = "hello" - let empty = "" - <: [ - str.ends_with("", 3), str.ends_with("lo", 5), - str.ends_with("ll", 4), str.ends_with("he", 2), - str.ends_with("ll", -1), str.ends_with("he", -3), - str.ends_with("he", 5), str.ends_with("lo", 3), - str.ends_with("lo", -6), str.ends_with("", -7), - str.ends_with("lo", 6), str.ends_with("", 7), - empty.ends_with("", 2), empty.ends_with("ll", 2), - ] - `); - eq(res, ARR([ - TRUE, TRUE, - TRUE, TRUE, - TRUE, TRUE, - FALSE, FALSE, - FALSE, TRUE, - FALSE, TRUE, - TRUE, FALSE, - ])); - }); - - test.concurrent("pad_start", async () => { - const res = await exe(` - let str = "abc" - <: [ - str.pad_start(0), str.pad_start(1), str.pad_start(2), str.pad_start(3), str.pad_start(4), str.pad_start(5), - str.pad_start(0, "0"), str.pad_start(1, "0"), str.pad_start(2, "0"), str.pad_start(3, "0"), str.pad_start(4, "0"), str.pad_start(5, "0"), - str.pad_start(0, "01"), str.pad_start(1, "01"), str.pad_start(2, "01"), str.pad_start(3, "01"), str.pad_start(4, "01"), str.pad_start(5, "01"), - ] - `); - eq(res, ARR([ - STR("abc"), STR("abc"), STR("abc"), STR("abc"), STR(" abc"), STR(" abc"), - STR("abc"), STR("abc"), STR("abc"), STR("abc"), STR("0abc"), STR("00abc"), - STR("abc"), STR("abc"), STR("abc"), STR("abc"), STR("0abc"), STR("01abc"), - ])); - }); - - test.concurrent("pad_end", async () => { - const res = await exe(` - let str = "abc" - <: [ - str.pad_end(0), str.pad_end(1), str.pad_end(2), str.pad_end(3), str.pad_end(4), str.pad_end(5), - str.pad_end(0, "0"), str.pad_end(1, "0"), str.pad_end(2, "0"), str.pad_end(3, "0"), str.pad_end(4, "0"), str.pad_end(5, "0"), - str.pad_end(0, "01"), str.pad_end(1, "01"), str.pad_end(2, "01"), str.pad_end(3, "01"), str.pad_end(4, "01"), str.pad_end(5, "01"), - ] - `); - eq(res, ARR([ - STR("abc"), STR("abc"), STR("abc"), STR("abc"), STR("abc "), STR("abc "), - STR("abc"), STR("abc"), STR("abc"), STR("abc"), STR("abc0"), STR("abc00"), - STR("abc"), STR("abc"), STR("abc"), STR("abc"), STR("abc0"), STR("abc01"), - ])); - }); - }); - - describe('arr', () => { - test.concurrent('len', async () => { - const res = await exe(` - let arr = [1, 2, 3] - <: arr.len - `); - eq(res, NUM(3)); - }); - - test.concurrent('push', async () => { - const res = await exe(` - let arr = [1, 2, 3] - arr.push(4) - <: arr - `); - eq(res, ARR([NUM(1), NUM(2), NUM(3), NUM(4)])); - }); - - test.concurrent('unshift', async () => { - const res = await exe(` - let arr = [1, 2, 3] - arr.unshift(4) - <: arr - `); - eq(res, ARR([NUM(4), NUM(1), NUM(2), NUM(3)])); - }); - - test.concurrent('pop', async () => { - const res = await exe(` - let arr = [1, 2, 3] - let popped = arr.pop() - <: [popped, arr] - `); - eq(res, ARR([NUM(3), ARR([NUM(1), NUM(2)])])); - }); - - test.concurrent('shift', async () => { - const res = await exe(` - let arr = [1, 2, 3] - let shifted = arr.shift() - <: [shifted, arr] - `); - eq(res, ARR([NUM(1), ARR([NUM(2), NUM(3)])])); - }); - - test.concurrent('concat', async () => { - const res = await exe(` - let arr = [1, 2, 3] - let concated = arr.concat([4, 5]) - <: [concated, arr] - `); - eq(res, ARR([ - ARR([NUM(1), NUM(2), NUM(3), NUM(4), NUM(5)]), - ARR([NUM(1), NUM(2), NUM(3)]) - ])); - }); - - test.concurrent('slice', async () => { - const res = await exe(` - let arr = ["ant", "bison", "camel", "duck", "elephant"] - let sliced = arr.slice(2, 4) - <: [sliced, arr] - `); - eq(res, ARR([ - ARR([STR('camel'), STR('duck')]), - ARR([STR('ant'), STR('bison'), STR('camel'), STR('duck'), STR('elephant')]) - ])); - }); - - test.concurrent('join', async () => { - const res = await exe(` - let arr = ["a", "b", "c"] - <: arr.join("-") - `); - eq(res, STR('a-b-c')); - }); - - test.concurrent('map', async () => { - const res = await exe(` - let arr = [1, 2, 3] - <: arr.map(@(item) { item * 2 }) - `); - eq(res, ARR([NUM(2), NUM(4), NUM(6)])); - }); - - test.concurrent('map with index', async () => { - const res = await exe(` - let arr = [1, 2, 3] - <: arr.map(@(item, index) { item * index }) - `); - eq(res, ARR([NUM(0), NUM(2), NUM(6)])); - }); - - test.concurrent('filter', async () => { - const res = await exe(` - let arr = [1, 2, 3] - <: arr.filter(@(item) { item != 2 }) - `); - eq(res, ARR([NUM(1), NUM(3)])); - }); - - test.concurrent('filter with index', async () => { - const res = await exe(` - let arr = [1, 2, 3, 4] - <: arr.filter(@(item, index) { item != 2 && index != 3 }) - `); - eq(res, ARR([NUM(1), NUM(3)])); - }); - - test.concurrent('reduce', async () => { - const res = await exe(` - let arr = [1, 2, 3, 4] - <: arr.reduce(@(accumulator, currentValue) { (accumulator + currentValue) }) - `); - eq(res, NUM(10)); - }); - - test.concurrent('reduce with index', async () => { - const res = await exe(` - let arr = [1, 2, 3, 4] - <: arr.reduce(@(accumulator, currentValue, index) { (accumulator + (currentValue * index)) } 0) - `); - eq(res, NUM(20)); - }); - - test.concurrent('reduce of empty array without initial value', async () => { - await expect(exe(` - let arr = [1, 2, 3, 4] - <: [].reduce(@(){}) - `)).rejects.toThrow('Reduce of empty array without initial value'); - }); - - test.concurrent('find', async () => { - const res = await exe(` - let arr = ["abc", "def", "ghi"] - <: arr.find(@(item) { item.incl("e") }) - `); - eq(res, STR('def')); - }); - - test.concurrent('find with index', async () => { - const res = await exe(` - let arr = ["abc1", "def1", "ghi1", "abc2", "def2", "ghi2"] - <: arr.find(@(item, index) { item.incl("e") && index > 1 }) - `); - eq(res, STR('def2')); - }); - - test.concurrent('incl', async () => { - const res = await exe(` - let arr = ["abc", "def", "ghi"] - <: [arr.incl("def"), arr.incl("jkl")] - `); - eq(res, ARR([TRUE, FALSE])); - }); - - test.concurrent('index_of', async () => { - const res = await exe(` - let arr = [0,1,2,3,4,0,1,2,3,4] - <: [ - arr.index_of(3) == 3, - arr.index_of(5) == -1, - arr.index_of(3, 3) == 3, - arr.index_of(3, 4) == 8, - arr.index_of(3, -1) == -1, - arr.index_of(3, -2) == 8, - arr.index_of(3, -7) == 3, - arr.index_of(3, 10) == -1, - ].map(@(v){if (v) '1' else '0'}).join() - `); - eq(res, STR('11111111')); - }); - - test.concurrent('reverse', async () => { - const res = await exe(` - let arr = [1, 2, 3] - arr.reverse() - <: arr - `); - eq(res, ARR([NUM(3), NUM(2), NUM(1)])); - }); - - test.concurrent('copy', async () => { - const res = await exe(` - let arr = [1, 2, 3] - let copied = arr.copy() - copied.reverse() - <: [copied, arr] - `); - eq(res, ARR([ - ARR([NUM(3), NUM(2), NUM(1)]), - ARR([NUM(1), NUM(2), NUM(3)]) - ])); - }); - - test.concurrent('sort num array', async () => { - const res = await exe(` - var arr = [2, 10, 3] - let comp = @(a, b) { a - b } - arr.sort(comp) - <: arr - `); - eq(res, ARR([NUM(2), NUM(3), NUM(10)])); - }); - - test.concurrent('sort string array (with Str:lt)', async () => { - const res = await exe(` - var arr = ["hoge", "huga", "piyo", "hoge"] - arr.sort(Str:lt) - <: arr - `); - eq(res, ARR([STR('hoge'), STR('hoge'), STR('huga'), STR('piyo')])); - }); - - test.concurrent('sort string array (with Str:gt)', async () => { - const res = await exe(` - var arr = ["hoge", "huga", "piyo", "hoge"] - arr.sort(Str:gt) - <: arr - `); - eq(res, ARR([ STR('piyo'), STR('huga'), STR('hoge'), STR('hoge')])); - }); - - test.concurrent('sort object array', async () => { - const res = await exe(` - var arr = [{x: 2}, {x: 10}, {x: 3}] - let comp = @(a, b) { a.x - b.x } - - arr.sort(comp) - <: arr - `); - eq(res, ARR([OBJ(new Map([['x', NUM(2)]])), OBJ(new Map([['x', NUM(3)]])), OBJ(new Map([['x', NUM(10)]]))])); - }); - - test.concurrent('fill', async () => { - const res = await exe(` - var arr1 = [0, 1, 2] - let arr2 = arr1.fill(3) - let arr3 = [0, 1, 2].fill(3, 1) - let arr4 = [0, 1, 2].fill(3, 1, 2) - let arr5 = [0, 1, 2].fill(3, -2, -1) - <: [arr1, arr2, arr3, arr4, arr5] - `); - eq(res, ARR([ - ARR([NUM(3), NUM(3), NUM(3)]), //target changed - ARR([NUM(3), NUM(3), NUM(3)]), - ARR([NUM(0), NUM(3), NUM(3)]), - ARR([NUM(0), NUM(3), NUM(2)]), - ARR([NUM(0), NUM(3), NUM(2)]), - ])); - }); - - test.concurrent('repeat', async () => { - const res = await exe(` - var arr1 = [0, 1, 2] - let arr2 = arr1.repeat(3) - let arr3 = arr1.repeat(0) - <: [arr1, arr2, arr3] - `); - eq(res, ARR([ - ARR([NUM(0), NUM(1), NUM(2)]), // target not changed - ARR([ - NUM(0), NUM(1), NUM(2), - NUM(0), NUM(1), NUM(2), - NUM(0), NUM(1), NUM(2), - ]), - ARR([]), - ])); - }); - - test.concurrent('splice (full)', async () => { - const res = await exe(` - let arr1 = [0, 1, 2, 3] - let arr2 = arr1.splice(1, 2, [10]) - <: [arr1, arr2] - `); - eq(res, ARR([ - ARR([NUM(0), NUM(10), NUM(3)]), - ARR([NUM(1), NUM(2)]), - ])); - }); - - test.concurrent('splice (negative-index)', async () => { - const res = await exe(` - let arr1 = [0, 1, 2, 3] - let arr2 = arr1.splice(-1, 0, [10, 20]) - <: [arr1, arr2] - `); - eq(res, ARR([ - ARR([NUM(0), NUM(1), NUM(2), NUM(10), NUM(20), NUM(3)]), - ARR([]), - ])); - }); - - test.concurrent('splice (larger-index)', async () => { - const res = await exe(` - let arr1 = [0, 1, 2, 3] - let arr2 = arr1.splice(4, 100, [10, 20]) - <: [arr1, arr2] - `); - eq(res, ARR([ - ARR([NUM(0), NUM(1), NUM(2), NUM(3), NUM(10), NUM(20)]), - ARR([]), - ])); - }); - - test.concurrent('splice (single argument)', async () => { - const res = await exe(` - let arr1 = [0, 1, 2, 3] - let arr2 = arr1.splice(1) - <: [arr1, arr2] - `); - eq(res, ARR([ - ARR([NUM(0)]), - ARR([NUM(1), NUM(2), NUM(3)]), - ])); - }); - - test.concurrent('flat', async () => { - const res = await exe(` - var arr1 = [0, [1], [2, 3], [4, [5, 6]]] - let arr2 = arr1.flat() - let arr3 = arr1.flat(2) - <: [arr1, arr2, arr3] - `); - eq(res, ARR([ - ARR([ - NUM(0), ARR([NUM(1)]), ARR([NUM(2), NUM(3)]), - ARR([NUM(4), ARR([NUM(5), NUM(6)])]) - ]), // target not changed - ARR([ - NUM(0), NUM(1), NUM(2), NUM(3), - NUM(4), ARR([NUM(5), NUM(6)]), - ]), - ARR([ - NUM(0), NUM(1), NUM(2), NUM(3), - NUM(4), NUM(5), NUM(6), - ]), - ])); - }); - - test.concurrent('flat_map', async () => { - const res = await exe(` - let arr1 = [0, 1, 2] - let arr2 = ["a", "b"] - let arr3 = arr1.flat_map(@(x){ arr2.map(@(y){ [x, y] }) }) - <: [arr1, arr3] - `); - eq(res, ARR([ - ARR([NUM(0), NUM(1), NUM(2)]), // target not changed - ARR([ - ARR([NUM(0), STR("a")]), - ARR([NUM(0), STR("b")]), - ARR([NUM(1), STR("a")]), - ARR([NUM(1), STR("b")]), - ARR([NUM(2), STR("a")]), - ARR([NUM(2), STR("b")]), - ]), - ])); - }); - - test.concurrent('every', async () => { - const res = await exe(` - let arr1 = [0, 1, 2, 3] - let res1 = arr1.every(@(v,i){v==0 || i > 0}) - let res2 = arr1.every(@(v,i){v==0 && i > 0}) - let res3 = [].every(@(v,i){false}) - <: [arr1, res1, res2, res3] - `); - eq(res, ARR([ - ARR([NUM(0), NUM(1), NUM(2), NUM(3)]), // target not changed - TRUE, - FALSE, - TRUE, - ])); - }); - - test.concurrent('some', async () => { - const res = await exe(` - let arr1 = [0, 1, 2, 3] - let res1 = arr1.some(@(v,i){v%2==0 && i <= 2}) - let res2 = arr1.some(@(v,i){v%2==0 && i > 2}) - <: [arr1, res1, res2] - `); - eq(res, ARR([ - ARR([NUM(0), NUM(1), NUM(2), NUM(3)]), // target not changed - TRUE, - FALSE, - ])); - }); - - test.concurrent('insert', async () => { - const res = await exe(` - let arr1 = [0, 1, 2] - let res = [] - res.push(arr1.insert(3, 10)) // [0, 1, 2, 10] - res.push(arr1.insert(2, 20)) // [0, 1, 20, 2, 10] - res.push(arr1.insert(0, 30)) // [30, 0, 1, 20, 2, 10] - res.push(arr1.insert(-1, 40)) // [30, 0, 1, 20, 2, 40, 10] - res.push(arr1.insert(-4, 50)) // [30, 0, 1, 50, 20, 2, 40, 10] - res.push(arr1.insert(100, 60)) // [30, 0, 1, 50, 20, 2, 40, 10, 60] - res.push(arr1) - <: res - `); - eq(res, ARR([ - NULL, NULL, NULL, NULL, NULL, NULL, - ARR([NUM(30), NUM(0), NUM(1), NUM(50), NUM(20), NUM(2), NUM(40), NUM(10), NUM(60)]) - ])); - }); - - test.concurrent('remove', async () => { - const res = await exe(` - let arr1 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] - let res = [] - res.push(arr1.remove(9)) // 9 [0, 1, 2, 3, 4, 5, 6, 7, 8] - res.push(arr1.remove(3)) // 3 [0, 1, 2, 4, 5, 6, 7, 8] - res.push(arr1.remove(0)) // 0 [1, 2, 4, 5, 6, 7, 8] - res.push(arr1.remove(-1)) // 8 [1, 2, 4, 5, 6, 7] - res.push(arr1.remove(-5)) // 2 [1, 4, 5, 6, 7] - res.push(arr1.remove(100)) // null [1, 4, 5, 6, 7] - res.push(arr1) - <: res - `); - eq(res, ARR([ - NUM(9), NUM(3), NUM(0), NUM(8), NUM(2), NULL, - ARR([NUM(1), NUM(4), NUM(5), NUM(6), NUM(7)]) - ])); - }); - - test.concurrent('at (without default value)', async () => { - const res = await exe(` - let arr1 = [10, 20, 30] - <: [ - arr1 - arr1.at(0), arr1.at(1), arr1.at(2) - arr1.at(-3), arr1.at(-2), arr1.at(-1) - arr1.at(3), arr1.at(4), arr1.at(5) - arr1.at(-6), arr1.at(-5), arr1.at(-4) - ] - `); - eq(res, ARR([ - ARR([NUM(10), NUM(20), NUM(30)]), - NUM(10), NUM(20), NUM(30), - NUM(10), NUM(20), NUM(30), - NULL, NULL, NULL, - NULL, NULL, NULL, - ])); - }); - - test.concurrent('at (with default value)', async () => { - const res = await exe(` - let arr1 = [10, 20, 30] - <: [ - arr1 - arr1.at(0, 100), arr1.at(1, 100), arr1.at(2, 100) - arr1.at(-3, 100), arr1.at(-2, 100), arr1.at(-1, 100) - arr1.at(3, 100), arr1.at(4, 100), arr1.at(5, 100) - arr1.at(-6, 100), arr1.at(-5, 100), arr1.at(-4, 100) - ] - `); - eq(res, ARR([ - ARR([NUM(10), NUM(20), NUM(30)]), - NUM(10), NUM(20), NUM(30), - NUM(10), NUM(20), NUM(30), - NUM(100), NUM(100), NUM(100), - NUM(100), NUM(100), NUM(100), - ])); - }); - }); -}); - -describe('std', () => { - describe('Core', () => { - test.concurrent('range', async () => { - eq(await exe('<: Core:range(1, 10)'), ARR([NUM(1), NUM(2), NUM(3), NUM(4), NUM(5), NUM(6), NUM(7), NUM(8), NUM(9), NUM(10)])); - eq(await exe('<: Core:range(1, 1)'), ARR([NUM(1)])); - eq(await exe('<: Core:range(9, 7)'), ARR([NUM(9), NUM(8), NUM(7)])); - }); - - test.concurrent('to_str', async () => { - eq(await exe('<: Core:to_str("abc")'), STR('abc')); - eq(await exe('<: Core:to_str(123)'), STR('123')); - eq(await exe('<: Core:to_str(true)'), STR('true')); - eq(await exe('<: Core:to_str(false)'), STR('false')); - eq(await exe('<: Core:to_str(null)'), STR('null')); - eq(await exe('<: Core:to_str({ a: "abc", b: 1234 })'), STR('{ a: "abc", b: 1234 }')); - eq(await exe('<: Core:to_str([ true, 123, null ])'), STR('[ true, 123, null ]')); - eq(await exe('<: Core:to_str(@( a, b, c ) {})'), STR('@( a, b, c ) { ... }')); - eq(await exe(` - let arr = [] - arr.push(arr) - <: Core:to_str(arr) - `), STR('[ ... ]')); - eq(await exe(` - let arr = [] - arr.push({ value: arr }) - <: Core:to_str(arr) - `), STR('[ { value: ... } ]')); - }); - - test.concurrent('abort', async () => { - assert.rejects( - exe('Core:abort("hoge")'), - { name: '', message: 'hoge' }, - ); - }); - }); - - describe('Arr', () => { - test.concurrent('create', async () => { - eq(await exe("<: Arr:create(0)"), ARR([])); - eq(await exe("<: Arr:create(3)"), ARR([NULL, NULL, NULL])); - eq(await exe("<: Arr:create(3, 1)"), ARR([NUM(1), NUM(1), NUM(1)])); - }); - }); - - describe('Math', () => { - test.concurrent('trig', async () => { - eq(await exe("<: Math:sin(Math:PI / 2)"), NUM(1)); - eq(await exe("<: Math:sin(0 - (Math:PI / 2))"), NUM(-1)); - eq(await exe("<: Math:sin(Math:PI / 4) * Math:cos(Math:PI / 4)"), NUM(0.5)); - }); - - test.concurrent('abs', async () => { - eq(await exe("<: Math:abs(1 - 6)"), NUM(5)); - }); - - test.concurrent('pow and sqrt', async () => { - eq(await exe("<: Math:sqrt(3^2 + 4^2)"), NUM(5)); - }); - - test.concurrent('round', async () => { - eq(await exe("<: Math:round(3.14)"), NUM(3)); - eq(await exe("<: Math:round(-1.414213)"), NUM(-1)); - }); - - test.concurrent('ceil', async () => { - eq(await exe("<: Math:ceil(2.71828)"), NUM(3)); - eq(await exe("<: Math:ceil(0 - Math:PI)"), NUM(-3)); - eq(await exe("<: Math:ceil(1 / Math:Infinity)"), NUM(0)); - }); - - test.concurrent('floor', async () => { - eq(await exe("<: Math:floor(23.14069)"), NUM(23)); - eq(await exe("<: Math:floor(Math:Infinity / 0)"), NUM(Infinity)); - }); - - test.concurrent('min', async () => { - eq(await exe("<: Math:min(2, 3)"), NUM(2)); - }); - - test.concurrent('max', async () => { - eq(await exe("<: Math:max(-2, -3)"), NUM(-2)); - }); - - /* flaky - test.concurrent('rnd', async () => { - const steps = 512; - - const res = await exe(` - let counts = [] // 0 ~ 10 の出現回数を格納する配列 - for (11) { - counts.push(0) // 初期化 - } - - for (${steps}) { - let rnd = Math:rnd(0 10) // 0 以上 10 以下の整数乱数 - counts[rnd] = counts[rnd] + 1 - } - <: counts`); - - function chiSquareTest(observed: number[], expected: number[]) { - let chiSquare = 0; // カイ二乗値 - for (let i = 0; i < observed.length; i++) { - chiSquare += Math.pow(observed[i] - expected[i], 2) / expected[i]; - } - return chiSquare; - } - - let observed: Array = []; - for (let i = 0; i < res.value.length; i++) { - observed.push(res.value[i].value); - } - let expected = new Array(11).fill(steps / 10); - let chiSquare = chiSquareTest(observed, expected); - - // 自由度が (11 - 1) の母分散の カイ二乗分布 95% 信頼区間は [3.94, 18.31] - assert.deepEqual(3.94 <= chiSquare && chiSquare <= 18.31, true, `カイ二乗値(${chiSquare})が母分散の95%信頼区間にありません`); - }); - */ - - test.concurrent('rnd with arg', async () => { - eq(await exe("<: Math:rnd(1, 1.5)"), NUM(1)); - }); - - test.concurrent('gen_rng', async () => { - // 2つのシード値から1~maxの乱数をn回生成して一致率を見る - const res = await exe(` - @test(seed1, seed2) { - let n = 100 - let max = 100000 - let threshold = 0.05 - let random1 = Math:gen_rng(seed1) - let random2 = Math:gen_rng(seed2) - var same = 0 - for n { - if random1(1, max) == random2(1, max) { - same += 1 - } - } - let rate = same / n - if seed1 == seed2 { rate == 1 } - else { rate < threshold } - } - let seed1 = \`{Util:uuid()}\` - let seed2 = \`{Date:year()}\` - <: [ - test(seed1, seed1) - test(seed1, seed2) - ] - `) - eq(res, ARR([BOOL(true), BOOL(true)])); - }); - }); - - describe('Obj', () => { - test.concurrent('keys', async () => { - const res = await exe(` - let o = { a: 1; b: 2; c: 3; } - - <: Obj:keys(o) - `); - eq(res, ARR([STR('a'), STR('b'), STR('c')])); - }); - - test.concurrent('vals', async () => { - const res = await exe(` - let o = { _nul: null; _num: 24; _str: 'hoge'; _arr: []; _obj: {}; } - - <: Obj:vals(o) - `); - eq(res, ARR([NULL, NUM(24), STR('hoge'), ARR([]), OBJ(new Map([]))])); - }); - - test.concurrent('kvs', async () => { - const res = await exe(` - let o = { a: 1; b: 2; c: 3; } - - <: Obj:kvs(o) - `); - eq(res, ARR([ - ARR([STR('a'), NUM(1)]), - ARR([STR('b'), NUM(2)]), - ARR([STR('c'), NUM(3)]) - ])); - }); - - test.concurrent('merge', async () => { - const res = await exe(` - let o1 = { a: 1; b: 2; } - let o2 = { b: 3; c: 4; } - - <: Obj:merge(o1, o2) - `); - eq(res, utils.jsToVal({ a: 1, b: 3, c: 4})); - }); - }); - - describe('Str', () => { - test.concurrent('lf', async () => { - const res = await exe(` - <: Str:lf - `); - eq(res, STR('\n')); - }); - - test.concurrent('from_codepoint', async () => { - const res = await exe(` - <: Str:from_codepoint(65) - `); - eq(res, STR('A')); - }); - - test.concurrent('from_unicode_codepoints', async () => { - const res = await exe(` - <: Str:from_unicode_codepoints([171581, 128073, 127999, 128104, 8205, 128102]) - `); - eq(res, STR('𩸽👉🏿👨‍👦')); - }); - - test.concurrent('from_utf8_bytes', async () => { - const res = await exe(` - <: Str:from_utf8_bytes([240, 169, 184, 189, 240, 159, 145, 137, 240, 159, 143, 191, 240, 159, 145, 168, 226, 128, 141, 240, 159, 145, 166]) - `); - eq(res, STR('𩸽👉🏿👨‍👦')); - }); - - test.concurrent('charcode_at', async () => { - let res = await exe(` - <: "aiscript".split().map(@(x, _) { x.charcode_at(0) }) - `); - eq(res, ARR([97, 105, 115, 99, 114, 105, 112, 116].map(x => NUM(x)))); - - res = await exe(` - <: "".charcode_at(0) - `); - eq(res, NULL); - }); - }); - - describe('Uri', () => { - test.concurrent('encode_full', async () => { - const res = await exe(` - <: Uri:encode_full("https://example.com/?q=あいちゃん") - `); - eq(res, STR('https://example.com/?q=%E3%81%82%E3%81%84%E3%81%A1%E3%82%83%E3%82%93')); - }); - - test.concurrent('encode_component', async () => { - const res = await exe(` - <: Uri:encode_component("https://example.com/?q=あいちゃん") - `); - eq(res, STR('https%3A%2F%2Fexample.com%2F%3Fq%3D%E3%81%82%E3%81%84%E3%81%A1%E3%82%83%E3%82%93')); - }); - - test.concurrent('decode_full', async () => { - const res = await exe(` - <: Uri:decode_full("https%3A%2F%2Fexample.com%2F%3Fq%3D%E3%81%82%E3%81%84%E3%81%A1%E3%82%83%E3%82%93") - `); - eq(res, STR('https%3A%2F%2Fexample.com%2F%3Fq%3Dあいちゃん')); - }); - - test.concurrent('decode_component', async () => { - const res = await exe(` - <: Uri:decode_component("https%3A%2F%2Fexample.com%2F%3Fq%3D%E3%81%82%E3%81%84%E3%81%A1%E3%82%83%E3%82%93") - `); - eq(res, STR('https://example.com/?q=あいちゃん')); - }); - }); - - describe('Error', () => { - test.concurrent('create', async () => { - eq( - await exe(` - <: Error:create('ai', {chan: 'kawaii'}) - `), - ERROR('ai', OBJ(new Map([['chan', STR('kawaii')]]))) - ); - }); - }); - - describe('Json', () => { - test.concurrent('stringify: fn', async () => { - const res = await exe(` - <: Json:stringify(@(){}) - `); - eq(res, STR('""')); - }); - - test.concurrent('parsable', async () => { - [ - 'null', - '"hoge"', - '[]', - '{}', - ].forEach(async (str) => { - const res = await exe(` - <: [ - Json:parsable('${str}') - Json:stringify(Json:parse('${str}')) - ] - `); - eq(res, ARR([TRUE, STR(str)])); - }); - }); - test.concurrent('unparsable', async () => { - [ - '', - 'hoge', - '[', - ].forEach(async (str) => { - const res = await exe(` - <: [ - Json:parsable('${str}') - Json:parse('${str}') - ] - `); - eq(res, ARR([FALSE, ERROR('not_json')])); - }); - }); - }); - - describe('Date', () => { - const example_time = new Date(2024, 1 - 1, 2, 3, 4, 5, 6).getTime(); - const zero_date = new Date(0); - test.concurrent('year', async () => { - const res = await exe(` - <: [Date:year(0), Date:year(${example_time})] - `); - eq(res, ARR([NUM(zero_date.getFullYear()), NUM(2024)])); - }); - - test.concurrent('month', async () => { - const res = await exe(` - <: [Date:month(0), Date:month(${example_time})] - `); - eq(res, ARR([NUM(zero_date.getMonth() + 1), NUM(1)])); - }); - - test.concurrent('day', async () => { - const res = await exe(` - <: [Date:day(0), Date:day(${example_time})] - `); - eq(res, ARR([NUM(zero_date.getDate()), NUM(2)])); - }); - - test.concurrent('hour', async () => { - const res = await exe(` - <: [Date:hour(0), Date:hour(${example_time})] - `); - eq(res, ARR([NUM(zero_date.getHours()), NUM(3)])); - }); - - test.concurrent('minute', async () => { - const res = await exe(` - <: [Date:minute(0), Date:minute(${example_time})] - `); - eq(res, ARR([NUM(zero_date.getMinutes()), NUM(4)])); - }); - - test.concurrent('second', async () => { - const res = await exe(` - <: [Date:second(0), Date:second(${example_time})] - `); - eq(res, ARR([NUM(zero_date.getSeconds()), NUM(5)])); - }); - - test.concurrent('millisecond', async () => { - const res = await exe(` - <: [Date:millisecond(0), Date:millisecond(${example_time})] - `); - eq(res, ARR([NUM(zero_date.getMilliseconds()), NUM(6)])); - }); - - test.concurrent('to_iso_str', async () => { - const res = await exe(` - let d1 = Date:parse("2024-04-12T01:47:46.021+09:00") - let s1 = Date:to_iso_str(d1) - let d2 = Date:parse(s1) - <: [d1, d2, s1] - `); - eq(res.value[0], res.value[1]); - assert.match(res.value[2].value, /^[0-9]{4,4}-[0-9]{2,2}-[0-9]{2,2}T[0-9]{2,2}:[0-9]{2,2}:[0-9]{2,2}\.[0-9]{3,3}(Z|[-+][0-9]{2,2}:[0-9]{2,2})$/); - }); - - test.concurrent('to_iso_str (UTC)', async () => { - const res = await exe(` - let d1 = Date:parse("2024-04-12T01:47:46.021+09:00") - let s1 = Date:to_iso_str(d1, 0) - let d2 = Date:parse(s1) - <: [d1, d2, s1] - `); - eq(res.value[0], res.value[1]); - eq(res.value[2], STR("2024-04-11T16:47:46.021Z")); - }); - - test.concurrent('to_iso_str (+09:00)', async () => { - const res = await exe(` - let d1 = Date:parse("2024-04-12T01:47:46.021+09:00") - let s1 = Date:to_iso_str(d1, 9*60) - let d2 = Date:parse(s1) - <: [d1, d2, s1] - `); - eq(res.value[0], res.value[1]); - eq(res.value[2], STR("2024-04-12T01:47:46.021+09:00")); - }); - - test.concurrent('to_iso_str (-05:18)', async () => { - const res = await exe(` - let d1 = Date:parse("2024-04-12T01:47:46.021+09:00") - let s1 = Date:to_iso_str(d1, -5*60-18) - let d2 = Date:parse(s1) - <: [d1, d2, s1] - `); - eq(res.value[0], res.value[1]); - eq(res.value[2], STR("2024-04-11T11:29:46.021-05:18")); - }); - }); -}); - -describe('Unicode', () => { - test.concurrent('len', async () => { - const res = await exe(` - <: "👍🏽🍆🌮".len - `); - eq(res, NUM(3)); - }); - - test.concurrent('pick', async () => { - const res = await exe(` - <: "👍🏽🍆🌮".pick(1) - `); - eq(res, STR('🍆')); - }); - - test.concurrent('slice', async () => { - const res = await exe(` - <: "Emojis 👍🏽 are 🍆 poison. 🌮s are bad.".slice(7, 14) - `); - eq(res, STR('👍🏽 are 🍆')); - }); - - test.concurrent('split', async () => { - const res = await exe(` - <: "👍🏽🍆🌮".split() - `); - eq(res, ARR([STR('👍🏽'), STR('🍆'), STR('🌮')])); - }); -}); - -describe('Security', () => { - test.concurrent('Cannot access js native property via var', async () => { - try { - await exe(` - <: constructor - `); - assert.fail(); - } catch (e) { - assert.ok(e instanceof AiScriptRuntimeError); - } - - try { - await exe(` - <: prototype - `); - assert.fail(); - } catch (e) { - assert.ok(e instanceof AiScriptRuntimeError); - } - - try { - await exe(` - <: __proto__ - `); - assert.fail(); - } catch (e) { - assert.ok(e instanceof AiScriptRuntimeError); - } - }); - - test.concurrent('Cannot access js native property via object', async () => { - const res1 = await exe(` - let obj = {} - - <: obj.constructor - `); - eq(res1, NULL); - - const res2 = await exe(` - let obj = {} - - <: obj.prototype - `); - eq(res2, NULL); - - const res3 = await exe(` - let obj = {} - - <: obj.__proto__ - `); - eq(res3, NULL); - }); - - test.concurrent('Cannot access js native property via primitive prop', async () => { - try { - await exe(` - <: "".constructor - `); - assert.fail(); - } catch (e) { - assert.ok(e instanceof AiScriptRuntimeError); + assert.ok(e instanceof AiScriptSyntaxError); } try { diff --git a/test/interpreter.ts b/test/interpreter.ts new file mode 100644 index 00000000..446613c8 --- /dev/null +++ b/test/interpreter.ts @@ -0,0 +1,116 @@ +import * as assert from 'assert'; +import { expect, test } from '@jest/globals'; +import { Parser, Interpreter, values, errors, utils, Ast } from '../src'; + +let { FN_NATIVE } = values; +let { AiScriptRuntimeError, AiScriptIndexOutOfRangeError } = errors; + +describe('Scope', () => { + test.concurrent('getAll', async () => { + const aiscript = new Interpreter({}); + await aiscript.exec(Parser.parse(` + let a = 1 + @b() { + let x = a + 1 + x + } + if true { + var y = 2 + } + var c = true + `)); + const vars = aiscript.scope.getAll(); + assert.ok(vars.get('a') != null); + assert.ok(vars.get('b') != null); + assert.ok(vars.get('c') != null); + assert.ok(vars.get('x') == null); + assert.ok(vars.get('y') == null); + }); +}); + +describe('error handler', () => { + test.concurrent('error from outside caller', async () => { + let outsideCaller: () => Promise = async () => {}; + let errCount: number = 0; + const aiscript = new Interpreter({ + emitError: FN_NATIVE((_args, _opts) => { + throw Error('emitError'); + }), + genOutsideCaller: FN_NATIVE(([fn], opts) => { + utils.assertFunction(fn); + outsideCaller = async () => { + opts.topCall(fn, []); + }; + }), + }, { + err(e) { /*console.log(e.toString());*/ errCount++ }, + }); + await aiscript.exec(Parser.parse(` + genOutsideCaller(emitError) + `)); + assert.strictEqual(errCount, 0); + await outsideCaller(); + assert.strictEqual(errCount, 1); + }); + + test.concurrent('array.map calls the handler just once', async () => { + let errCount: number = 0; + const aiscript = new Interpreter({}, { + err(e) { errCount++ }, + }); + await aiscript.exec(Parser.parse(` + Core:range(1,5).map(@(){ hoge }) + `)); + assert.strictEqual(errCount, 1); + }); +}); + +describe('error location', () => { + const exeAndGetErrPos = (src: string): Promise => new Promise((ok, ng) => { + const aiscript = new Interpreter({ + emitError: FN_NATIVE((_args, _opts) => { + throw Error('emitError'); + }), + }, { + err(e) { ok(e.pos) }, + }); + aiscript.exec(Parser.parse(src)).then(() => ng('error has not occured.')); + }); + + test.concurrent('Non-aiscript Error', async () => { + return expect(exeAndGetErrPos(`/* (の位置 + */ + emitError() + `)).resolves.toEqual({ line: 3, column: 13}); + }); + + test.concurrent('No "var" in namespace declaration', async () => { + return expect(exeAndGetErrPos(`// vの位置 + :: Ai { + let chan = 'kawaii' + var kun = '!?' + } + `)).resolves.toEqual({ line: 4, column: 5}); + }); + + test.concurrent('Index out of range', async () => { + return expect(exeAndGetErrPos(`// [の位置 + let arr = [] + arr[0] + `)).resolves.toEqual({ line: 3, column: 7}); + }); + + test.concurrent('Error in passed function', async () => { + return expect(exeAndGetErrPos(`// /の位置 + [0, 1, 2].map(@(v){ + 0/v + }) + `)).resolves.toEqual({ line: 3, column: 6}); + }); + + test.concurrent('No such prop', async () => { + return expect(exeAndGetErrPos(`// .の位置 + [].ai + `)).resolves.toEqual({ line: 2, column: 6}); + }); +}); diff --git a/test/keywords.ts b/test/keywords.ts new file mode 100644 index 00000000..88f183a1 --- /dev/null +++ b/test/keywords.ts @@ -0,0 +1,89 @@ +import { expect, test } from '@jest/globals'; +import { Parser, Interpreter } from '../src'; +import { AiScriptSyntaxError } from '../src/error'; +import { exe } from './testutils'; + +const reservedWords = [ + 'null', + 'true', + 'false', + 'each', + 'for', + 'do', + 'while', + 'loop', + 'break', + 'continue', + 'match', + 'case', + 'default', + 'if', + 'elif', + 'else', + 'return', + 'eval', + 'var', + 'let', + 'exists', +] as const; + +const sampleCodes = Object.entries({ + variable: word => + ` + let ${word} = "ai" + ${word} + `, + + function: word => + ` + @${word}() { 'ai' } + ${word}() + `, + + attribute: word => + ` + #[${word} 1] + @f() { 1 } + `, + + namespace: word => + ` + :: ${word} { + @f() { 1 } + } + ${word}:f() + `, + + prop: word => + ` + let x = { ${word}: 1 } + x.${word} + `, + + meta: word => + ` + ### ${word} 1 + `, +}); + +function pickRandom(arr: T[]): T { + return arr[Math.floor(Math.random() * arr.length)]; +} + +describe.each( + sampleCodes +)('reserved word validation on %s', (_, sampleCode) => { + + test.concurrent.each( + [pickRandom(reservedWords)] + )('%s must be rejected', (word) => { + return expect(exe(sampleCode(word))).rejects.toThrow(AiScriptSyntaxError); + }); + + test.concurrent.each( + [pickRandom(reservedWords)] + )('%scat must be allowed', (word) => { + return exe(sampleCode(word+'cat')); + }); + +}); diff --git a/test/literals.ts b/test/literals.ts new file mode 100644 index 00000000..77a8ab33 --- /dev/null +++ b/test/literals.ts @@ -0,0 +1,189 @@ +import * as assert from 'assert'; +import { expect, test } from '@jest/globals'; +import { } from '../src'; +import { NUM, STR, NULL, ARR, OBJ, BOOL, TRUE, FALSE, ERROR ,FN_NATIVE } from '../src/interpreter/value'; +import { } from '../src/error'; +import { exe, eq } from './testutils'; + +describe('literal', () => { + test.concurrent('string (single quote)', async () => { + const res = await exe(` + <: 'foo' + `); + eq(res, STR('foo')); + }); + + test.concurrent('string (double quote)', async () => { + const res = await exe(` + <: "foo" + `); + eq(res, STR('foo')); + }); + + test.concurrent('Escaped double quote', async () => { + const res = await exe('<: "ai saw a note \\"bebeyo\\"."'); + eq(res, STR('ai saw a note "bebeyo".')); + }); + + test.concurrent('Escaped single quote', async () => { + const res = await exe('<: \'ai saw a note \\\'bebeyo\\\'.\''); + eq(res, STR('ai saw a note \'bebeyo\'.')); + }); + + test.concurrent('bool (true)', async () => { + const res = await exe(` + <: true + `); + eq(res, BOOL(true)); + }); + + test.concurrent('bool (false)', async () => { + const res = await exe(` + <: false + `); + eq(res, BOOL(false)); + }); + + test.concurrent('number (Int)', async () => { + const res = await exe(` + <: 10 + `); + eq(res, NUM(10)); + }); + + test.concurrent('number (Float)', async () => { + const res = await exe(` + <: 0.5 + `); + eq(res, NUM(0.5)); + }); + + test.concurrent('arr (separated by comma)', async () => { + const res = await exe(` + <: [1, 2, 3] + `); + eq(res, ARR([NUM(1), NUM(2), NUM(3)])); + }); + + test.concurrent('arr (separated by comma) (with trailing comma)', async () => { + const res = await exe(` + <: [1, 2, 3,] + `); + eq(res, ARR([NUM(1), NUM(2), NUM(3)])); + }); + + test.concurrent('arr (separated by line break)', async () => { + const res = await exe(` + <: [ + 1 + 2 + 3 + ] + `); + eq(res, ARR([NUM(1), NUM(2), NUM(3)])); + }); + + test.concurrent('arr (separated by line break and comma)', async () => { + const res = await exe(` + <: [ + 1, + 2, + 3 + ] + `); + eq(res, ARR([NUM(1), NUM(2), NUM(3)])); + }); + + test.concurrent('arr (separated by line break and comma) (with trailing comma)', async () => { + const res = await exe(` + <: [ + 1, + 2, + 3, + ] + `); + eq(res, ARR([NUM(1), NUM(2), NUM(3)])); + }); + + test.concurrent('obj (separated by comma)', async () => { + const res = await exe(` + <: { a: 1, b: 2, c: 3 } + `); + eq(res, OBJ(new Map([['a', NUM(1)], ['b', NUM(2)], ['c', NUM(3)]]))); + }); + + test.concurrent('obj (separated by comma) (with trailing comma)', async () => { + const res = await exe(` + <: { a: 1, b: 2, c: 3, } + `); + eq(res, OBJ(new Map([['a', NUM(1)], ['b', NUM(2)], ['c', NUM(3)]]))); + }); + + test.concurrent('obj (separated by line break)', async () => { + const res = await exe(` + <: { + a: 1 + b: 2 + c: 3 + } + `); + eq(res, OBJ(new Map([['a', NUM(1)], ['b', NUM(2)], ['c', NUM(3)]]))); + }); + + test.concurrent('obj and arr (separated by line break)', async () => { + const res = await exe(` + <: { + a: 1 + b: [ + 1 + 2 + 3 + ] + c: 3 + } + `); + eq(res, OBJ(new Map([ + ['a', NUM(1)], + ['b', ARR([NUM(1), NUM(2), NUM(3)])], + ['c', NUM(3)] + ]))); + }); +}); + +describe('Template syntax', () => { + test.concurrent('Basic', async () => { + const res = await exe(` + let str = "kawaii" + <: \`Ai is {str}!\` + `); + eq(res, STR('Ai is kawaii!')); + }); + + test.concurrent('convert to str', async () => { + const res = await exe(` + <: \`1 + 1 = {(1 + 1)}\` + `); + eq(res, STR('1 + 1 = 2')); + }); + + test.concurrent('invalid', async () => { + try { + await exe(` + <: \`{hoge}\` + `); + } catch (e) { + assert.ok(true); + return; + } + assert.fail(); + }); + + test.concurrent('Escape', async () => { + const res = await exe(` + let message = "Hello" + <: \`\\\`a\\{b\\}c\\\`\` + `); + eq(res, STR('`a{b}c`')); + }); +}); + diff --git a/test/parser.ts b/test/parser.ts new file mode 100644 index 00000000..8fc188da --- /dev/null +++ b/test/parser.ts @@ -0,0 +1,146 @@ +import * as assert from 'assert'; +import { Scanner } from '../src/parser/scanner'; +import { TOKEN, TokenKind, TokenPosition } from '../src/parser/token'; +import { CharStream } from '../src/parser/streams/char-stream'; + +describe('CharStream', () => { + test.concurrent('char', async () => { + const source = 'abc'; + const stream = new CharStream(source); + assert.strictEqual('a', stream.char); + }); + + test.concurrent('next', async () => { + const source = 'abc'; + const stream = new CharStream(source); + stream.next(); + assert.strictEqual('b', stream.char); + }); + + describe('prev', () => { + test.concurrent('move', async () => { + const source = 'abc'; + const stream = new CharStream(source); + stream.next(); + assert.strictEqual('b', stream.char); + stream.prev(); + assert.strictEqual('a', stream.char); + }); + + test.concurrent('境界外には移動しない', async () => { + const source = 'abc'; + const stream = new CharStream(source); + stream.prev(); + assert.strictEqual('a', stream.char); + }); + }); + + test.concurrent('eof', async () => { + const source = 'abc'; + const stream = new CharStream(source); + assert.strictEqual(false, stream.eof); + stream.next(); + assert.strictEqual(false, stream.eof); + stream.next(); + assert.strictEqual(false, stream.eof); + stream.next(); + assert.strictEqual(true, stream.eof); + }); + + test.concurrent('EOFでcharを参照するとエラー', async () => { + const source = ''; + const stream = new CharStream(source); + assert.strictEqual(true, stream.eof); + try { + stream.char; + } catch (e) { + return; + } + assert.fail(); + }); + + test.concurrent('CRは読み飛ばされる', async () => { + const source = 'a\r\nb'; + const stream = new CharStream(source); + assert.strictEqual('a', stream.char); + stream.next(); + assert.strictEqual('\n', stream.char); + stream.next(); + assert.strictEqual('b', stream.char); + stream.next(); + assert.strictEqual(true, stream.eof); + }); +}); + +describe('Scanner', () => { + function init(source: string) { + const stream = new Scanner(source); + return stream; + } + function next(stream: Scanner, kind: TokenKind, pos: TokenPosition, opts: { hasLeftSpacing?: boolean, value?: string }) { + assert.deepStrictEqual(stream.token, TOKEN(kind, pos, opts)); + stream.next(); + } + + test.concurrent('eof', async () => { + const source = ''; + const stream = init(source); + next(stream, TokenKind.EOF, { line: 1, column: 1 }, { }); + next(stream, TokenKind.EOF, { line: 1, column: 1 }, { }); + }); + test.concurrent('keyword', async () => { + const source = 'if'; + const stream = init(source); + next(stream, TokenKind.IfKeyword, { line: 1, column: 1 }, { }); + next(stream, TokenKind.EOF, { line: 1, column: 3 }, { }); + }); + test.concurrent('identifier', async () => { + const source = 'xyz'; + const stream = init(source); + next(stream, TokenKind.Identifier, { line: 1, column: 1 }, { value: 'xyz' }); + next(stream, TokenKind.EOF, { line: 1, column: 4 }, { }); + }); + test.concurrent('invalid token', async () => { + const source = '$'; + try { + const stream = new Scanner(source); + } catch (e) { + return; + } + assert.fail(); + }); + test.concurrent('words', async () => { + const source = 'abc xyz'; + const stream = init(source); + next(stream, TokenKind.Identifier, { line: 1, column: 1 }, { value: 'abc' }); + next(stream, TokenKind.Identifier, { line: 1, column: 5 }, { hasLeftSpacing: true, value: 'xyz' }); + next(stream, TokenKind.EOF, { line: 1, column: 8 }, { }); + }); + test.concurrent('stream', async () => { + const source = '@abc() { }'; + const stream = init(source); + next(stream, TokenKind.At, { line: 1, column: 1 }, { }); + next(stream, TokenKind.Identifier, { line: 1, column: 2 }, { value: 'abc' }); + next(stream, TokenKind.OpenParen, { line: 1, column: 5 }, { }); + next(stream, TokenKind.CloseParen, { line: 1, column: 6 }, { }); + next(stream, TokenKind.OpenBrace, { line: 1, column: 8 }, { hasLeftSpacing: true }); + next(stream, TokenKind.CloseBrace, { line: 1, column: 10 }, { hasLeftSpacing: true }); + next(stream, TokenKind.EOF, { line: 1, column: 11 }, { }); + }); + test.concurrent('multi-lines', async () => { + const source = 'aaa\nbbb'; + const stream = init(source); + next(stream, TokenKind.Identifier, { line: 1, column: 1 }, { value: 'aaa' }); + next(stream, TokenKind.NewLine, { line: 1, column: 4 }, { }); + next(stream, TokenKind.Identifier, { line: 2, column: 1 }, { value: 'bbb' }); + next(stream, TokenKind.EOF, { line: 2, column: 4 }, { }); + }); + test.concurrent('lookahead', async () => { + const source = '@abc() { }'; + const stream = init(source); + assert.deepStrictEqual(stream.lookahead(1), TOKEN(TokenKind.Identifier, { line: 1, column: 2 }, { value: 'abc' })); + next(stream, TokenKind.At, { line: 1, column: 1 }, { }); + next(stream, TokenKind.Identifier, { line: 1, column: 2 }, { value: 'abc' }); + next(stream, TokenKind.OpenParen, { line: 1, column: 5 }, { }); + }); +}); diff --git a/test/primitive-props.ts b/test/primitive-props.ts new file mode 100644 index 00000000..f0bb2f3f --- /dev/null +++ b/test/primitive-props.ts @@ -0,0 +1,805 @@ +import { expect, test } from '@jest/globals'; +import { NUM, STR, NULL, ARR, OBJ, BOOL, TRUE, FALSE, ERROR ,FN_NATIVE } from '../src/interpreter/value'; +import { exe, eq } from './testutils'; + + +describe('num', () => { + test.concurrent('to_str', async () => { + const res = await exe(` + let num = 123 + <: num.to_str() + `); + eq(res, STR('123')); + }); + test.concurrent('to_hex', async () => { + // TODO -0, 巨大数, 無限小数, Infinity等入力時の結果は未定義 + const res = await exe(` + <: [ + 0, 10, 16, + -10, -16, + 0.5, + ].map(@(v){v.to_hex()}) + `); + eq(res, ARR([ + STR('0'), STR('a'), STR('10'), + STR('-a'), STR('-10'), + STR('0.8'), + ])); + }); +}); + +describe('str', () => { + test.concurrent('len', async () => { + const res = await exe(` + let str = "hello" + <: str.len + `); + eq(res, NUM(5)); + }); + + test.concurrent('to_num', async () => { + const res = await exe(` + let str = "123" + <: str.to_num() + `); + eq(res, NUM(123)); + }); + + test.concurrent('upper', async () => { + const res = await exe(` + let str = "hello" + <: str.upper() + `); + eq(res, STR('HELLO')); + }); + + test.concurrent('lower', async () => { + const res = await exe(` + let str = "HELLO" + <: str.lower() + `); + eq(res, STR('hello')); + }); + + test.concurrent('trim', async () => { + const res = await exe(` + let str = " hello " + <: str.trim() + `); + eq(res, STR('hello')); + }); + + test.concurrent('replace', async () => { + const res = await exe(` + let str = "hello" + <: str.replace("l", "x") + `); + eq(res, STR('hexxo')); + }); + + test.concurrent('index_of', async () => { + const res = await exe(` + let str = '0123401234' + <: [ + str.index_of('3') == 3, + str.index_of('5') == -1, + str.index_of('3', 3) == 3, + str.index_of('3', 4) == 8, + str.index_of('3', -1) == -1, + str.index_of('3', -2) == 8, + str.index_of('3', -7) == 3, + str.index_of('3', 10) == -1, + ].map(@(v){if (v) '1' else '0'}).join() + `); + eq(res, STR('11111111')); + }); + + test.concurrent('incl', async () => { + const res = await exe(` + let str = "hello" + <: [str.incl("ll"), str.incl("x")] + `); + eq(res, ARR([TRUE, FALSE])); + }); + + test.concurrent('split', async () => { + const res = await exe(` + let str = "a,b,c" + <: str.split(",") + `); + eq(res, ARR([STR('a'), STR('b'), STR('c')])); + }); + + test.concurrent('pick', async () => { + const res = await exe(` + let str = "hello" + <: str.pick(1) + `); + eq(res, STR('e')); + }); + + test.concurrent('slice', async () => { + const res = await exe(` + let str = "hello" + <: str.slice(1, 3) + `); + eq(res, STR('el')); + }); + + test.concurrent("codepoint_at", async () => { + const res = await exe(` + let str = "𩸽" + <: str.codepoint_at(0) + `); + eq(res, NUM(171581)); + }); + + test.concurrent("to_arr", async () => { + const res = await exe(` + let str = "𩸽👉🏿👨‍👦" + <: str.to_arr() + `); + eq( + res, + ARR([STR("𩸽"), STR("👉🏿"), STR("👨‍👦")]) + ); + }); + + test.concurrent("to_unicode_arr", async () => { + const res = await exe(` + let str = "𩸽👉🏿👨‍👦" + <: str.to_unicode_arr() + `); + eq( + res, + ARR([STR("𩸽"), STR("👉"), STR(String.fromCodePoint(0x1F3FF)), STR("👨"), STR("\u200d"), STR("👦")]) + ); + }); + + test.concurrent("to_unicode_codepoint_arr", async () => { + const res = await exe(` + let str = "𩸽👉🏿👨‍👦" + <: str.to_unicode_codepoint_arr() + `); + eq( + res, + ARR([NUM(171581), NUM(128073), NUM(127999), NUM(128104), NUM(8205), NUM(128102)]) + ); + }); + + test.concurrent("to_char_arr", async () => { + const res = await exe(` + let str = "abc𩸽👉🏿👨‍👦def" + <: str.to_char_arr() + `); + eq( + res, + ARR([97, 98, 99, 55399, 56893, 55357, 56393, 55356, 57343, 55357, 56424, 8205, 55357, 56422, 100, 101, 102].map((s) => STR(String.fromCharCode(s)))) + ); + }); + + test.concurrent("to_charcode_arr", async () => { + const res = await exe(` + let str = "abc𩸽👉🏿👨‍👦def" + <: str.to_charcode_arr() + `); + eq( + res, + ARR([NUM(97), NUM(98), NUM(99), NUM(55399), NUM(56893), NUM(55357), NUM(56393), NUM(55356), NUM(57343), NUM(55357), NUM(56424), NUM(8205), NUM(55357), NUM(56422), NUM(100), NUM(101), NUM(102)]) + ); + }); + + test.concurrent("to_utf8_byte_arr", async () => { + const res = await exe(` + let str = "abc𩸽👉🏿👨‍👦def" + <: str.to_utf8_byte_arr() + `); + eq( + res, + ARR([NUM(97), NUM(98), NUM(99), NUM(240), NUM(169), NUM(184), NUM(189), NUM(240), NUM(159), NUM(145), NUM(137), NUM(240), NUM(159), NUM(143), NUM(191), NUM(240), NUM(159), NUM(145), NUM(168), NUM(226), NUM(128), NUM(141), NUM(240), NUM(159), NUM(145), NUM(166), NUM(100), NUM(101), NUM(102)]) + ); + }); + + test.concurrent('starts_with (no index)', async () => { + const res = await exe(` + let str = "hello" + let empty = "" + <: [ + str.starts_with(""), str.starts_with("hello"), + str.starts_with("he"), str.starts_with("ell"), + empty.starts_with(""), empty.starts_with("he"), + ] + `); + eq(res, ARR([ + TRUE, TRUE, + TRUE, FALSE, + TRUE, FALSE, + ])); + }); + + test.concurrent('starts_with (with index)', async () => { + const res = await exe(` + let str = "hello" + let empty = "" + <: [ + str.starts_with("", 4), str.starts_with("he", 0), + str.starts_with("ll", 2), str.starts_with("lo", 3), + str.starts_with("lo", -2), str.starts_with("hel", -5), + str.starts_with("he", 2), str.starts_with("loa", 3), + str.starts_with("lo", -6), str.starts_with("", -7), + str.starts_with("lo", 6), str.starts_with("", 7), + empty.starts_with("", 2), empty.starts_with("ll", 2), + ] + `); + eq(res, ARR([ + TRUE, TRUE, + TRUE, TRUE, + TRUE, TRUE, + FALSE, FALSE, + FALSE, TRUE, + FALSE, TRUE, + TRUE, FALSE, + ])); + }); + + test.concurrent('ends_with (no index)', async () => { + const res = await exe(` + let str = "hello" + let empty = "" + <: [ + str.ends_with(""), str.ends_with("hello"), + str.ends_with("lo"), str.ends_with("ell"), + empty.ends_with(""), empty.ends_with("he"), + ] + `); + eq(res, ARR([ + TRUE, TRUE, + TRUE, FALSE, + TRUE, FALSE, + ])); + }); + + test.concurrent('ends_with (with index)', async () => { + const res = await exe(` + let str = "hello" + let empty = "" + <: [ + str.ends_with("", 3), str.ends_with("lo", 5), + str.ends_with("ll", 4), str.ends_with("he", 2), + str.ends_with("ll", -1), str.ends_with("he", -3), + str.ends_with("he", 5), str.ends_with("lo", 3), + str.ends_with("lo", -6), str.ends_with("", -7), + str.ends_with("lo", 6), str.ends_with("", 7), + empty.ends_with("", 2), empty.ends_with("ll", 2), + ] + `); + eq(res, ARR([ + TRUE, TRUE, + TRUE, TRUE, + TRUE, TRUE, + FALSE, FALSE, + FALSE, TRUE, + FALSE, TRUE, + TRUE, FALSE, + ])); + }); + + test.concurrent("pad_start", async () => { + const res = await exe(` + let str = "abc" + <: [ + str.pad_start(0), str.pad_start(1), str.pad_start(2), str.pad_start(3), str.pad_start(4), str.pad_start(5), + str.pad_start(0, "0"), str.pad_start(1, "0"), str.pad_start(2, "0"), str.pad_start(3, "0"), str.pad_start(4, "0"), str.pad_start(5, "0"), + str.pad_start(0, "01"), str.pad_start(1, "01"), str.pad_start(2, "01"), str.pad_start(3, "01"), str.pad_start(4, "01"), str.pad_start(5, "01"), + ] + `); + eq(res, ARR([ + STR("abc"), STR("abc"), STR("abc"), STR("abc"), STR(" abc"), STR(" abc"), + STR("abc"), STR("abc"), STR("abc"), STR("abc"), STR("0abc"), STR("00abc"), + STR("abc"), STR("abc"), STR("abc"), STR("abc"), STR("0abc"), STR("01abc"), + ])); + }); + + test.concurrent("pad_end", async () => { + const res = await exe(` + let str = "abc" + <: [ + str.pad_end(0), str.pad_end(1), str.pad_end(2), str.pad_end(3), str.pad_end(4), str.pad_end(5), + str.pad_end(0, "0"), str.pad_end(1, "0"), str.pad_end(2, "0"), str.pad_end(3, "0"), str.pad_end(4, "0"), str.pad_end(5, "0"), + str.pad_end(0, "01"), str.pad_end(1, "01"), str.pad_end(2, "01"), str.pad_end(3, "01"), str.pad_end(4, "01"), str.pad_end(5, "01"), + ] + `); + eq(res, ARR([ + STR("abc"), STR("abc"), STR("abc"), STR("abc"), STR("abc "), STR("abc "), + STR("abc"), STR("abc"), STR("abc"), STR("abc"), STR("abc0"), STR("abc00"), + STR("abc"), STR("abc"), STR("abc"), STR("abc"), STR("abc0"), STR("abc01"), + ])); + }); +}); + +describe('arr', () => { + test.concurrent('len', async () => { + const res = await exe(` + let arr = [1, 2, 3] + <: arr.len + `); + eq(res, NUM(3)); + }); + + test.concurrent('push', async () => { + const res = await exe(` + let arr = [1, 2, 3] + arr.push(4) + <: arr + `); + eq(res, ARR([NUM(1), NUM(2), NUM(3), NUM(4)])); + }); + + test.concurrent('unshift', async () => { + const res = await exe(` + let arr = [1, 2, 3] + arr.unshift(4) + <: arr + `); + eq(res, ARR([NUM(4), NUM(1), NUM(2), NUM(3)])); + }); + + test.concurrent('pop', async () => { + const res = await exe(` + let arr = [1, 2, 3] + let popped = arr.pop() + <: [popped, arr] + `); + eq(res, ARR([NUM(3), ARR([NUM(1), NUM(2)])])); + }); + + test.concurrent('shift', async () => { + const res = await exe(` + let arr = [1, 2, 3] + let shifted = arr.shift() + <: [shifted, arr] + `); + eq(res, ARR([NUM(1), ARR([NUM(2), NUM(3)])])); + }); + + test.concurrent('concat', async () => { + const res = await exe(` + let arr = [1, 2, 3] + let concated = arr.concat([4, 5]) + <: [concated, arr] + `); + eq(res, ARR([ + ARR([NUM(1), NUM(2), NUM(3), NUM(4), NUM(5)]), + ARR([NUM(1), NUM(2), NUM(3)]) + ])); + }); + + test.concurrent('slice', async () => { + const res = await exe(` + let arr = ["ant", "bison", "camel", "duck", "elephant"] + let sliced = arr.slice(2, 4) + <: [sliced, arr] + `); + eq(res, ARR([ + ARR([STR('camel'), STR('duck')]), + ARR([STR('ant'), STR('bison'), STR('camel'), STR('duck'), STR('elephant')]) + ])); + }); + + test.concurrent('join', async () => { + const res = await exe(` + let arr = ["a", "b", "c"] + <: arr.join("-") + `); + eq(res, STR('a-b-c')); + }); + + test.concurrent('map', async () => { + const res = await exe(` + let arr = [1, 2, 3] + <: arr.map(@(item) { item * 2 }) + `); + eq(res, ARR([NUM(2), NUM(4), NUM(6)])); + }); + + test.concurrent('map with index', async () => { + const res = await exe(` + let arr = [1, 2, 3] + <: arr.map(@(item, index) { item * index }) + `); + eq(res, ARR([NUM(0), NUM(2), NUM(6)])); + }); + + test.concurrent('filter', async () => { + const res = await exe(` + let arr = [1, 2, 3] + <: arr.filter(@(item) { item != 2 }) + `); + eq(res, ARR([NUM(1), NUM(3)])); + }); + + test.concurrent('filter with index', async () => { + const res = await exe(` + let arr = [1, 2, 3, 4] + <: arr.filter(@(item, index) { item != 2 && index != 3 }) + `); + eq(res, ARR([NUM(1), NUM(3)])); + }); + + test.concurrent('reduce', async () => { + const res = await exe(` + let arr = [1, 2, 3, 4] + <: arr.reduce(@(accumulator, currentValue) { (accumulator + currentValue) }) + `); + eq(res, NUM(10)); + }); + + test.concurrent('reduce with index', async () => { + const res = await exe(` + let arr = [1, 2, 3, 4] + <: arr.reduce(@(accumulator, currentValue, index) { (accumulator + (currentValue * index)) }, 0) + `); + eq(res, NUM(20)); + }); + + test.concurrent('reduce of empty array without initial value', async () => { + await expect(exe(` + let arr = [1, 2, 3, 4] + <: [].reduce(@(){}) + `)).rejects.toThrow('Reduce of empty array without initial value'); + }); + + test.concurrent('find', async () => { + const res = await exe(` + let arr = ["abc", "def", "ghi"] + <: arr.find(@(item) { item.incl("e") }) + `); + eq(res, STR('def')); + }); + + test.concurrent('find with index', async () => { + const res = await exe(` + let arr = ["abc1", "def1", "ghi1", "abc2", "def2", "ghi2"] + <: arr.find(@(item, index) { item.incl("e") && index > 1 }) + `); + eq(res, STR('def2')); + }); + + test.concurrent('incl', async () => { + const res = await exe(` + let arr = ["abc", "def", "ghi"] + <: [arr.incl("def"), arr.incl("jkl")] + `); + eq(res, ARR([TRUE, FALSE])); + }); + + test.concurrent('index_of', async () => { + const res = await exe(` + let arr = [0,1,2,3,4,0,1,2,3,4] + <: [ + arr.index_of(3) == 3, + arr.index_of(5) == -1, + arr.index_of(3, 3) == 3, + arr.index_of(3, 4) == 8, + arr.index_of(3, -1) == -1, + arr.index_of(3, -2) == 8, + arr.index_of(3, -7) == 3, + arr.index_of(3, 10) == -1, + ].map(@(v){if (v) '1' else '0'}).join() + `); + eq(res, STR('11111111')); + }); + + test.concurrent('reverse', async () => { + const res = await exe(` + let arr = [1, 2, 3] + arr.reverse() + <: arr + `); + eq(res, ARR([NUM(3), NUM(2), NUM(1)])); + }); + + test.concurrent('copy', async () => { + const res = await exe(` + let arr = [1, 2, 3] + let copied = arr.copy() + copied.reverse() + <: [copied, arr] + `); + eq(res, ARR([ + ARR([NUM(3), NUM(2), NUM(1)]), + ARR([NUM(1), NUM(2), NUM(3)]) + ])); + }); + + test.concurrent('sort num array', async () => { + const res = await exe(` + var arr = [2, 10, 3] + let comp = @(a, b) { a - b } + arr.sort(comp) + <: arr + `); + eq(res, ARR([NUM(2), NUM(3), NUM(10)])); + }); + + test.concurrent('sort string array (with Str:lt)', async () => { + const res = await exe(` + var arr = ["hoge", "huga", "piyo", "hoge"] + arr.sort(Str:lt) + <: arr + `); + eq(res, ARR([STR('hoge'), STR('hoge'), STR('huga'), STR('piyo')])); + }); + + test.concurrent('sort string array (with Str:gt)', async () => { + const res = await exe(` + var arr = ["hoge", "huga", "piyo", "hoge"] + arr.sort(Str:gt) + <: arr + `); + eq(res, ARR([ STR('piyo'), STR('huga'), STR('hoge'), STR('hoge')])); + }); + + test.concurrent('sort object array', async () => { + const res = await exe(` + var arr = [{x: 2}, {x: 10}, {x: 3}] + let comp = @(a, b) { a.x - b.x } + + arr.sort(comp) + <: arr + `); + eq(res, ARR([OBJ(new Map([['x', NUM(2)]])), OBJ(new Map([['x', NUM(3)]])), OBJ(new Map([['x', NUM(10)]]))])); + }); + + test.concurrent('sort (stable)', async () => { + const res = await exe(` + var arr = [[2, 0], [10, 1], [3, 2], [3, 3], [2, 4]] + let comp = @(a, b) { a[0] - b[0] } + + arr.sort(comp) + <: arr + `); + eq(res, ARR([ + ARR([NUM(2), NUM(0)]), + ARR([NUM(2), NUM(4)]), + ARR([NUM(3), NUM(2)]), + ARR([NUM(3), NUM(3)]), + ARR([NUM(10), NUM(1)]), + ])); + }); + + test.concurrent('fill', async () => { + const res = await exe(` + var arr1 = [0, 1, 2] + let arr2 = arr1.fill(3) + let arr3 = [0, 1, 2].fill(3, 1) + let arr4 = [0, 1, 2].fill(3, 1, 2) + let arr5 = [0, 1, 2].fill(3, -2, -1) + <: [arr1, arr2, arr3, arr4, arr5] + `); + eq(res, ARR([ + ARR([NUM(3), NUM(3), NUM(3)]), //target changed + ARR([NUM(3), NUM(3), NUM(3)]), + ARR([NUM(0), NUM(3), NUM(3)]), + ARR([NUM(0), NUM(3), NUM(2)]), + ARR([NUM(0), NUM(3), NUM(2)]), + ])); + }); + + test.concurrent('repeat', async () => { + const res = await exe(` + var arr1 = [0, 1, 2] + let arr2 = arr1.repeat(3) + let arr3 = arr1.repeat(0) + <: [arr1, arr2, arr3] + `); + eq(res, ARR([ + ARR([NUM(0), NUM(1), NUM(2)]), // target not changed + ARR([ + NUM(0), NUM(1), NUM(2), + NUM(0), NUM(1), NUM(2), + NUM(0), NUM(1), NUM(2), + ]), + ARR([]), + ])); + }); + + test.concurrent('splice (full)', async () => { + const res = await exe(` + let arr1 = [0, 1, 2, 3] + let arr2 = arr1.splice(1, 2, [10]) + <: [arr1, arr2] + `); + eq(res, ARR([ + ARR([NUM(0), NUM(10), NUM(3)]), + ARR([NUM(1), NUM(2)]), + ])); + }); + + test.concurrent('splice (negative-index)', async () => { + const res = await exe(` + let arr1 = [0, 1, 2, 3] + let arr2 = arr1.splice(-1, 0, [10, 20]) + <: [arr1, arr2] + `); + eq(res, ARR([ + ARR([NUM(0), NUM(1), NUM(2), NUM(10), NUM(20), NUM(3)]), + ARR([]), + ])); + }); + + test.concurrent('splice (larger-index)', async () => { + const res = await exe(` + let arr1 = [0, 1, 2, 3] + let arr2 = arr1.splice(4, 100, [10, 20]) + <: [arr1, arr2] + `); + eq(res, ARR([ + ARR([NUM(0), NUM(1), NUM(2), NUM(3), NUM(10), NUM(20)]), + ARR([]), + ])); + }); + + test.concurrent('splice (single argument)', async () => { + const res = await exe(` + let arr1 = [0, 1, 2, 3] + let arr2 = arr1.splice(1) + <: [arr1, arr2] + `); + eq(res, ARR([ + ARR([NUM(0)]), + ARR([NUM(1), NUM(2), NUM(3)]), + ])); + }); + + test.concurrent('flat', async () => { + const res = await exe(` + var arr1 = [0, [1], [2, 3], [4, [5, 6]]] + let arr2 = arr1.flat() + let arr3 = arr1.flat(2) + <: [arr1, arr2, arr3] + `); + eq(res, ARR([ + ARR([ + NUM(0), ARR([NUM(1)]), ARR([NUM(2), NUM(3)]), + ARR([NUM(4), ARR([NUM(5), NUM(6)])]) + ]), // target not changed + ARR([ + NUM(0), NUM(1), NUM(2), NUM(3), + NUM(4), ARR([NUM(5), NUM(6)]), + ]), + ARR([ + NUM(0), NUM(1), NUM(2), NUM(3), + NUM(4), NUM(5), NUM(6), + ]), + ])); + }); + + test.concurrent('flat_map', async () => { + const res = await exe(` + let arr1 = [0, 1, 2] + let arr2 = ["a", "b"] + let arr3 = arr1.flat_map(@(x){ arr2.map(@(y){ [x, y] }) }) + <: [arr1, arr3] + `); + eq(res, ARR([ + ARR([NUM(0), NUM(1), NUM(2)]), // target not changed + ARR([ + ARR([NUM(0), STR("a")]), + ARR([NUM(0), STR("b")]), + ARR([NUM(1), STR("a")]), + ARR([NUM(1), STR("b")]), + ARR([NUM(2), STR("a")]), + ARR([NUM(2), STR("b")]), + ]), + ])); + }); + + test.concurrent('every', async () => { + const res = await exe(` + let arr1 = [0, 1, 2, 3] + let res1 = arr1.every(@(v,i){v==0 || i > 0}) + let res2 = arr1.every(@(v,i){v==0 && i > 0}) + let res3 = [].every(@(v,i){false}) + <: [arr1, res1, res2, res3] + `); + eq(res, ARR([ + ARR([NUM(0), NUM(1), NUM(2), NUM(3)]), // target not changed + TRUE, + FALSE, + TRUE, + ])); + }); + + test.concurrent('some', async () => { + const res = await exe(` + let arr1 = [0, 1, 2, 3] + let res1 = arr1.some(@(v,i){v%2==0 && i <= 2}) + let res2 = arr1.some(@(v,i){v%2==0 && i > 2}) + <: [arr1, res1, res2] + `); + eq(res, ARR([ + ARR([NUM(0), NUM(1), NUM(2), NUM(3)]), // target not changed + TRUE, + FALSE, + ])); + }); + + test.concurrent('insert', async () => { + const res = await exe(` + let arr1 = [0, 1, 2] + let res = [] + res.push(arr1.insert(3, 10)) // [0, 1, 2, 10] + res.push(arr1.insert(2, 20)) // [0, 1, 20, 2, 10] + res.push(arr1.insert(0, 30)) // [30, 0, 1, 20, 2, 10] + res.push(arr1.insert(-1, 40)) // [30, 0, 1, 20, 2, 40, 10] + res.push(arr1.insert(-4, 50)) // [30, 0, 1, 50, 20, 2, 40, 10] + res.push(arr1.insert(100, 60)) // [30, 0, 1, 50, 20, 2, 40, 10, 60] + res.push(arr1) + <: res + `); + eq(res, ARR([ + NULL, NULL, NULL, NULL, NULL, NULL, + ARR([NUM(30), NUM(0), NUM(1), NUM(50), NUM(20), NUM(2), NUM(40), NUM(10), NUM(60)]) + ])); + }); + + test.concurrent('remove', async () => { + const res = await exe(` + let arr1 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + let res = [] + res.push(arr1.remove(9)) // 9 [0, 1, 2, 3, 4, 5, 6, 7, 8] + res.push(arr1.remove(3)) // 3 [0, 1, 2, 4, 5, 6, 7, 8] + res.push(arr1.remove(0)) // 0 [1, 2, 4, 5, 6, 7, 8] + res.push(arr1.remove(-1)) // 8 [1, 2, 4, 5, 6, 7] + res.push(arr1.remove(-5)) // 2 [1, 4, 5, 6, 7] + res.push(arr1.remove(100)) // null [1, 4, 5, 6, 7] + res.push(arr1) + <: res + `); + eq(res, ARR([ + NUM(9), NUM(3), NUM(0), NUM(8), NUM(2), NULL, + ARR([NUM(1), NUM(4), NUM(5), NUM(6), NUM(7)]) + ])); + }); + + test.concurrent('at (without default value)', async () => { + const res = await exe(` + let arr1 = [10, 20, 30] + <: [ + arr1 + arr1.at(0), arr1.at(1), arr1.at(2) + arr1.at(-3), arr1.at(-2), arr1.at(-1) + arr1.at(3), arr1.at(4), arr1.at(5) + arr1.at(-6), arr1.at(-5), arr1.at(-4) + ] + `); + eq(res, ARR([ + ARR([NUM(10), NUM(20), NUM(30)]), + NUM(10), NUM(20), NUM(30), + NUM(10), NUM(20), NUM(30), + NULL, NULL, NULL, + NULL, NULL, NULL, + ])); + }); + + test.concurrent('at (with default value)', async () => { + const res = await exe(` + let arr1 = [10, 20, 30] + <: [ + arr1 + arr1.at(0, 100), arr1.at(1, 100), arr1.at(2, 100) + arr1.at(-3, 100), arr1.at(-2, 100), arr1.at(-1, 100) + arr1.at(3, 100), arr1.at(4, 100), arr1.at(5, 100) + arr1.at(-6, 100), arr1.at(-5, 100), arr1.at(-4, 100) + ] + `); + eq(res, ARR([ + ARR([NUM(10), NUM(20), NUM(30)]), + NUM(10), NUM(20), NUM(30), + NUM(10), NUM(20), NUM(30), + NUM(100), NUM(100), NUM(100), + NUM(100), NUM(100), NUM(100), + ])); + }); +}); diff --git a/test/std.ts b/test/std.ts new file mode 100644 index 00000000..e4f8f5e6 --- /dev/null +++ b/test/std.ts @@ -0,0 +1,423 @@ +import * as assert from 'assert'; +import { test } from '@jest/globals'; +import { utils } from '../src'; +import { NUM, STR, NULL, ARR, OBJ, BOOL, TRUE, FALSE, ERROR ,FN_NATIVE } from '../src/interpreter/value'; +import { exe, eq } from './testutils'; + + +describe('Core', () => { + test.concurrent('range', async () => { + eq(await exe('<: Core:range(1, 10)'), ARR([NUM(1), NUM(2), NUM(3), NUM(4), NUM(5), NUM(6), NUM(7), NUM(8), NUM(9), NUM(10)])); + eq(await exe('<: Core:range(1, 1)'), ARR([NUM(1)])); + eq(await exe('<: Core:range(9, 7)'), ARR([NUM(9), NUM(8), NUM(7)])); + }); + + test.concurrent('to_str', async () => { + eq(await exe('<: Core:to_str("abc")'), STR('abc')); + eq(await exe('<: Core:to_str(123)'), STR('123')); + eq(await exe('<: Core:to_str(true)'), STR('true')); + eq(await exe('<: Core:to_str(false)'), STR('false')); + eq(await exe('<: Core:to_str(null)'), STR('null')); + eq(await exe('<: Core:to_str({ a: "abc", b: 1234 })'), STR('{ a: "abc", b: 1234 }')); + eq(await exe('<: Core:to_str([ true, 123, null ])'), STR('[ true, 123, null ]')); + eq(await exe('<: Core:to_str(@( a, b, c ) {})'), STR('@( a, b, c ) { ... }')); + eq(await exe(` + let arr = [] + arr.push(arr) + <: Core:to_str(arr) + `), STR('[ ... ]')); + eq(await exe(` + let arr = [] + arr.push({ value: arr }) + <: Core:to_str(arr) + `), STR('[ { value: ... } ]')); + }); + + test.concurrent('abort', async () => { + assert.rejects( + exe('Core:abort("hoge")'), + e => e.message.includes('hoge'), + ); + }); +}); + +describe('Arr', () => { + test.concurrent('create', async () => { + eq(await exe("<: Arr:create(0)"), ARR([])); + eq(await exe("<: Arr:create(3)"), ARR([NULL, NULL, NULL])); + eq(await exe("<: Arr:create(3, 1)"), ARR([NUM(1), NUM(1), NUM(1)])); + }); +}); + +describe('Math', () => { + test.concurrent('trig', async () => { + eq(await exe("<: Math:sin(Math:PI / 2)"), NUM(1)); + eq(await exe("<: Math:sin(0 - (Math:PI / 2))"), NUM(-1)); + eq(await exe("<: Math:sin(Math:PI / 4) * Math:cos(Math:PI / 4)"), NUM(0.5)); + }); + + test.concurrent('abs', async () => { + eq(await exe("<: Math:abs(1 - 6)"), NUM(5)); + }); + + test.concurrent('pow and sqrt', async () => { + eq(await exe("<: Math:sqrt(3^2 + 4^2)"), NUM(5)); + }); + + test.concurrent('round', async () => { + eq(await exe("<: Math:round(3.14)"), NUM(3)); + eq(await exe("<: Math:round(-1.414213)"), NUM(-1)); + }); + + test.concurrent('ceil', async () => { + eq(await exe("<: Math:ceil(2.71828)"), NUM(3)); + eq(await exe("<: Math:ceil(0 - Math:PI)"), NUM(-3)); + eq(await exe("<: Math:ceil(1 / Math:Infinity)"), NUM(0)); + }); + + test.concurrent('floor', async () => { + eq(await exe("<: Math:floor(23.14069)"), NUM(23)); + eq(await exe("<: Math:floor(Math:Infinity / 0)"), NUM(Infinity)); + }); + + test.concurrent('min', async () => { + eq(await exe("<: Math:min(2, 3)"), NUM(2)); + }); + + test.concurrent('max', async () => { + eq(await exe("<: Math:max(-2, -3)"), NUM(-2)); + }); + + /* flaky + test.concurrent('rnd', async () => { + const steps = 512; + + const res = await exe(` + let counts = [] // 0 ~ 10 の出現回数を格納する配列 + for (11) { + counts.push(0) // 初期化 + } + + for (${steps}) { + let rnd = Math:rnd(0 10) // 0 以上 10 以下の整数乱数 + counts[rnd] = counts[rnd] + 1 + } + <: counts`); + + function chiSquareTest(observed: number[], expected: number[]) { + let chiSquare = 0; // カイ二乗値 + for (let i = 0; i < observed.length; i++) { + chiSquare += Math.pow(observed[i] - expected[i], 2) / expected[i]; + } + return chiSquare; + } + + let observed: Array = []; + for (let i = 0; i < res.value.length; i++) { + observed.push(res.value[i].value); + } + let expected = new Array(11).fill(steps / 10); + let chiSquare = chiSquareTest(observed, expected); + + // 自由度が (11 - 1) の母分散の カイ二乗分布 95% 信頼区間は [3.94, 18.31] + assert.deepEqual(3.94 <= chiSquare && chiSquare <= 18.31, true, `カイ二乗値(${chiSquare})が母分散の95%信頼区間にありません`); + }); + */ + + test.concurrent('rnd with arg', async () => { + eq(await exe("<: Math:rnd(1, 1.5)"), NUM(1)); + }); + + test.concurrent('gen_rng', async () => { + // 2つのシード値から1~maxの乱数をn回生成して一致率を見る + const res = await exe(` + @test(seed1, seed2) { + let n = 100 + let max = 100000 + let threshold = 0.05 + let random1 = Math:gen_rng(seed1) + let random2 = Math:gen_rng(seed2) + var same = 0 + for n { + if random1(1, max) == random2(1, max) { + same += 1 + } + } + let rate = same / n + if seed1 == seed2 { rate == 1 } + else { rate < threshold } + } + let seed1 = \`{Util:uuid()}\` + let seed2 = \`{Date:year()}\` + <: [ + test(seed1, seed1) + test(seed1, seed2) + ] + `) + eq(res, ARR([BOOL(true), BOOL(true)])); + }); +}); + +describe('Obj', () => { + test.concurrent('keys', async () => { + const res = await exe(` + let o = { a: 1, b: 2, c: 3, } + + <: Obj:keys(o) + `); + eq(res, ARR([STR('a'), STR('b'), STR('c')])); + }); + + test.concurrent('vals', async () => { + const res = await exe(` + let o = { _nul: null, _num: 24, _str: 'hoge', _arr: [], _obj: {}, } + + <: Obj:vals(o) + `); + eq(res, ARR([NULL, NUM(24), STR('hoge'), ARR([]), OBJ(new Map([]))])); + }); + + test.concurrent('kvs', async () => { + const res = await exe(` + let o = { a: 1, b: 2, c: 3, } + + <: Obj:kvs(o) + `); + eq(res, ARR([ + ARR([STR('a'), NUM(1)]), + ARR([STR('b'), NUM(2)]), + ARR([STR('c'), NUM(3)]) + ])); + }); + + test.concurrent('merge', async () => { + const res = await exe(` + let o1 = { a: 1, b: 2 } + let o2 = { b: 3, c: 4 } + + <: Obj:merge(o1, o2) + `); + eq(res, utils.jsToVal({ a: 1, b: 3, c: 4})); + }); +}); + +describe('Str', () => { + test.concurrent('lf', async () => { + const res = await exe(` + <: Str:lf + `); + eq(res, STR('\n')); + }); + + test.concurrent('from_codepoint', async () => { + const res = await exe(` + <: Str:from_codepoint(65) + `); + eq(res, STR('A')); + }); + + test.concurrent('from_unicode_codepoints', async () => { + const res = await exe(` + <: Str:from_unicode_codepoints([171581, 128073, 127999, 128104, 8205, 128102]) + `); + eq(res, STR('𩸽👉🏿👨‍👦')); + }); + + test.concurrent('from_utf8_bytes', async () => { + const res = await exe(` + <: Str:from_utf8_bytes([240, 169, 184, 189, 240, 159, 145, 137, 240, 159, 143, 191, 240, 159, 145, 168, 226, 128, 141, 240, 159, 145, 166]) + `); + eq(res, STR('𩸽👉🏿👨‍👦')); + }); + + test.concurrent('charcode_at', async () => { + let res = await exe(` + <: "aiscript".split().map(@(x, _) { x.charcode_at(0) }) + `); + eq(res, ARR([97, 105, 115, 99, 114, 105, 112, 116].map(x => NUM(x)))); + + res = await exe(` + <: "".charcode_at(0) + `); + eq(res, NULL); + }); +}); + +describe('Uri', () => { + test.concurrent('encode_full', async () => { + const res = await exe(` + <: Uri:encode_full("https://example.com/?q=あいちゃん") + `); + eq(res, STR('https://example.com/?q=%E3%81%82%E3%81%84%E3%81%A1%E3%82%83%E3%82%93')); + }); + + test.concurrent('encode_component', async () => { + const res = await exe(` + <: Uri:encode_component("https://example.com/?q=あいちゃん") + `); + eq(res, STR('https%3A%2F%2Fexample.com%2F%3Fq%3D%E3%81%82%E3%81%84%E3%81%A1%E3%82%83%E3%82%93')); + }); + + test.concurrent('decode_full', async () => { + const res = await exe(` + <: Uri:decode_full("https%3A%2F%2Fexample.com%2F%3Fq%3D%E3%81%82%E3%81%84%E3%81%A1%E3%82%83%E3%82%93") + `); + eq(res, STR('https%3A%2F%2Fexample.com%2F%3Fq%3Dあいちゃん')); + }); + + test.concurrent('decode_component', async () => { + const res = await exe(` + <: Uri:decode_component("https%3A%2F%2Fexample.com%2F%3Fq%3D%E3%81%82%E3%81%84%E3%81%A1%E3%82%83%E3%82%93") + `); + eq(res, STR('https://example.com/?q=あいちゃん')); + }); +}); + +describe('Error', () => { + test.concurrent('create', async () => { + eq( + await exe(` + <: Error:create('ai', {chan: 'kawaii'}) + `), + ERROR('ai', OBJ(new Map([['chan', STR('kawaii')]]))) + ); + }); +}); + +describe('Json', () => { + test.concurrent('stringify: fn', async () => { + const res = await exe(` + <: Json:stringify(@(){}) + `); + eq(res, STR('""')); + }); + + test.concurrent('parsable', async () => { + [ + 'null', + '"hoge"', + '[]', + '{}', + ].forEach(async (str) => { + const res = await exe(` + <: [ + Json:parsable('${str}') + Json:stringify(Json:parse('${str}')) + ] + `); + eq(res, ARR([TRUE, STR(str)])); + }); + }); + test.concurrent('unparsable', async () => { + [ + '', + 'hoge', + '[', + ].forEach(async (str) => { + const res = await exe(` + <: [ + Json:parsable('${str}') + Json:parse('${str}') + ] + `); + eq(res, ARR([FALSE, ERROR('not_json')])); + }); + }); +}); + +describe('Date', () => { + const example_time = new Date(2024, 1 - 1, 2, 3, 4, 5, 6).getTime(); + const zero_date = new Date(0); + test.concurrent('year', async () => { + const res = await exe(` + <: [Date:year(0), Date:year(${example_time})] + `); + eq(res, ARR([NUM(zero_date.getFullYear()), NUM(2024)])); + }); + + test.concurrent('month', async () => { + const res = await exe(` + <: [Date:month(0), Date:month(${example_time})] + `); + eq(res, ARR([NUM(zero_date.getMonth() + 1), NUM(1)])); + }); + + test.concurrent('day', async () => { + const res = await exe(` + <: [Date:day(0), Date:day(${example_time})] + `); + eq(res, ARR([NUM(zero_date.getDate()), NUM(2)])); + }); + + test.concurrent('hour', async () => { + const res = await exe(` + <: [Date:hour(0), Date:hour(${example_time})] + `); + eq(res, ARR([NUM(zero_date.getHours()), NUM(3)])); + }); + + test.concurrent('minute', async () => { + const res = await exe(` + <: [Date:minute(0), Date:minute(${example_time})] + `); + eq(res, ARR([NUM(zero_date.getMinutes()), NUM(4)])); + }); + + test.concurrent('second', async () => { + const res = await exe(` + <: [Date:second(0), Date:second(${example_time})] + `); + eq(res, ARR([NUM(zero_date.getSeconds()), NUM(5)])); + }); + + test.concurrent('millisecond', async () => { + const res = await exe(` + <: [Date:millisecond(0), Date:millisecond(${example_time})] + `); + eq(res, ARR([NUM(zero_date.getMilliseconds()), NUM(6)])); + }); + + test.concurrent('to_iso_str', async () => { + const res = await exe(` + let d1 = Date:parse("2024-04-12T01:47:46.021+09:00") + let s1 = Date:to_iso_str(d1) + let d2 = Date:parse(s1) + <: [d1, d2, s1] + `); + eq(res.value[0], res.value[1]); + assert.match(res.value[2].value, /^[0-9]{4,4}-[0-9]{2,2}-[0-9]{2,2}T[0-9]{2,2}:[0-9]{2,2}:[0-9]{2,2}\.[0-9]{3,3}(Z|[-+][0-9]{2,2}:[0-9]{2,2})$/); + }); + + test.concurrent('to_iso_str (UTC)', async () => { + const res = await exe(` + let d1 = Date:parse("2024-04-12T01:47:46.021+09:00") + let s1 = Date:to_iso_str(d1, 0) + let d2 = Date:parse(s1) + <: [d1, d2, s1] + `); + eq(res.value[0], res.value[1]); + eq(res.value[2], STR("2024-04-11T16:47:46.021Z")); + }); + + test.concurrent('to_iso_str (+09:00)', async () => { + const res = await exe(` + let d1 = Date:parse("2024-04-12T01:47:46.021+09:00") + let s1 = Date:to_iso_str(d1, 9*60) + let d2 = Date:parse(s1) + <: [d1, d2, s1] + `); + eq(res.value[0], res.value[1]); + eq(res.value[2], STR("2024-04-12T01:47:46.021+09:00")); + }); + + test.concurrent('to_iso_str (-05:18)', async () => { + const res = await exe(` + let d1 = Date:parse("2024-04-12T01:47:46.021+09:00") + let s1 = Date:to_iso_str(d1, -5*60-18) + let d2 = Date:parse(s1) + <: [d1, d2, s1] + `); + eq(res.value[0], res.value[1]); + eq(res.value[2], STR("2024-04-11T11:29:46.021-05:18")); + }); +}); diff --git a/test/syntax.ts b/test/syntax.ts new file mode 100644 index 00000000..d092591c --- /dev/null +++ b/test/syntax.ts @@ -0,0 +1,1515 @@ +import * as assert from 'assert'; +import { expect, test } from '@jest/globals'; +import { utils } from '../src'; +import { NUM, STR, NULL, ARR, OBJ, BOOL, TRUE, FALSE, ERROR ,FN_NATIVE } from '../src/interpreter/value'; +import { AiScriptRuntimeError } from '../src/error'; +import { exe, getMeta, eq } from './testutils'; + +/* + * General + */ +describe('terminator', () => { + describe('top-level', () => { + test.concurrent('newline', async () => { + const res = await exe(` + :: A { + let x = 1 + } + :: B { + let x = 2 + } + <: A:x + `); + eq(res, NUM(1)); + }); + + test.concurrent('semi colon', async () => { + const res = await exe(` + ::A{let x = 1};::B{let x = 2} + <: A:x + `); + eq(res, NUM(1)); + }); + + test.concurrent('semi colon of the tail', async () => { + const res = await exe(` + ::A{let x = 1}; + <: A:x + `); + eq(res, NUM(1)); + }); + }); + + describe('block', () => { + test.concurrent('newline', async () => { + const res = await exe(` + eval { + let x = 1 + let y = 2 + <: x + y + } + `); + eq(res, NUM(3)); + }); + + test.concurrent('semi colon', async () => { + const res = await exe(` + eval{let x=1;let y=2;<:x+y} + `); + eq(res, NUM(3)); + }); + + test.concurrent('semi colon of the tail', async () => { + const res = await exe(` + eval{let x=1;<:x;} + `); + eq(res, NUM(1)); + }); + }); + + describe('namespace', () => { + test.concurrent('newline', async () => { + const res = await exe(` + :: A { + let x = 1 + let y = 2 + } + <: A:x + A:y + `); + eq(res, NUM(3)); + }); + + test.concurrent('semi colon', async () => { + const res = await exe(` + ::A{let x=1;let y=2} + <: A:x + A:y + `); + eq(res, NUM(3)); + }); + + test.concurrent('semi colon of the tail', async () => { + const res = await exe(` + ::A{let x=1;} + <: A:x + `); + eq(res, NUM(1)); + }); + }); +}); + +describe('separator', () => { + describe('match', () => { + test.concurrent('multi line', async () => { + const res = await exe(` + let x = 1 + <: match x { + case 1 => "a" + case 2 => "b" + } + `); + eq(res, STR('a')); + }); + + test.concurrent('multi line with semi colon', async () => { + const res = await exe(` + let x = 1 + <: match x { + case 1 => "a", + case 2 => "b" + } + `); + eq(res, STR('a')); + }); + + test.concurrent('single line', async () => { + const res = await exe(` + let x = 1 + <:match x{case 1=>"a",case 2=>"b"} + `); + eq(res, STR('a')); + }); + + test.concurrent('single line with tail semi colon', async () => { + const res = await exe(` + let x = 1 + <: match x{case 1=>"a",case 2=>"b",} + `); + eq(res, STR('a')); + }); + + test.concurrent('multi line (default)', async () => { + const res = await exe(` + let x = 3 + <: match x { + case 1 => "a" + case 2 => "b" + default => "c" + } + `); + eq(res, STR('c')); + }); + + test.concurrent('multi line with semi colon (default)', async () => { + const res = await exe(` + let x = 3 + <: match x { + case 1 => "a", + case 2 => "b", + default => "c" + } + `); + eq(res, STR('c')); + }); + + test.concurrent('single line (default)', async () => { + const res = await exe(` + let x = 3 + <:match x{case 1=>"a",case 2=>"b",default=>"c"} + `); + eq(res, STR('c')); + }); + + test.concurrent('single line with tail semi colon (default)', async () => { + const res = await exe(` + let x = 3 + <:match x{case 1=>"a",case 2=>"b",default=>"c",} + `); + eq(res, STR('c')); + }); + }); + + describe('call', () => { + test.concurrent('multi line', async () => { + const res = await exe(` + @f(a, b, c) { + a * b + c + } + <: f( + 2 + 3 + 1 + ) + `); + eq(res, NUM(7)); + }); + + test.concurrent('multi line with comma', async () => { + const res = await exe(` + @f(a, b, c) { + a * b + c + } + <: f( + 2, + 3, + 1 + ) + `); + eq(res, NUM(7)); + }); + + test.concurrent('single line', async () => { + const res = await exe(` + @f(a, b, c) { + a * b + c + } + <:f(2,3,1) + `); + eq(res, NUM(7)); + }); + + test.concurrent('single line with tail comma', async () => { + const res = await exe(` + @f(a, b, c) { + a * b + c + } + <:f(2,3,1,) + `); + eq(res, NUM(7)); + }); + }); + + describe('obj', () => { + test.concurrent('multi line', async () => { + const res = await exe(` + let x = { + a: 1 + b: 2 + } + <: x.b + `); + eq(res, NUM(2)); + }); + + test.concurrent('multi line, multi newlines', async () => { + const res = await exe(` + let x = { + + a: 1 + + b: 2 + + } + <: x.b + `); + eq(res, NUM(2)); + }); + + test.concurrent('multi line with comma', async () => { + const res = await exe(` + let x = { + a: 1, + b: 2 + } + <: x.b + `); + eq(res, NUM(2)); + }); + + test.concurrent('single line', async () => { + const res = await exe(` + let x={a:1,b:2} + <: x.b + `); + eq(res, NUM(2)); + }); + + test.concurrent('single line with tail comma', async () => { + const res = await exe(` + let x={a:1,b:2,} + <: x.b + `); + eq(res, NUM(2)); + }); + }); + + describe('arr', () => { + test.concurrent('multi line', async () => { + const res = await exe(` + let x = [ + 1 + 2 + ] + <: x[1] + `); + eq(res, NUM(2)); + }); + + test.concurrent('multi line, multi newlines', async () => { + const res = await exe(` + let x = [ + + 1 + + 2 + + ] + <: x[1] + `); + eq(res, NUM(2)); + }); + + test.concurrent('multi line with comma', async () => { + const res = await exe(` + let x = [ + 1, + 2 + ] + <: x[1] + `); + eq(res, NUM(2)); + }); + + test.concurrent('multi line with comma, multi newlines', async () => { + const res = await exe(` + let x = [ + + 1, + + 2 + + ] + <: x[1] + `); + eq(res, NUM(2)); + }); + + test.concurrent('multi line with comma and tail comma', async () => { + const res = await exe(` + let x = [ + 1, + 2, + ] + <: x[1] + `); + eq(res, NUM(2)); + }); + + test.concurrent('multi line with comma and tail comma, multi newlines', async () => { + const res = await exe(` + let x = [ + + 1, + + 2, + + ] + <: x[1] + `); + eq(res, NUM(2)); + }); + + test.concurrent('single line', async () => { + const res = await exe(` + let x=[1,2] + <: x[1] + `); + eq(res, NUM(2)); + }); + + test.concurrent('single line with tail comma', async () => { + const res = await exe(` + let x=[1,2,] + <: x[1] + `); + eq(res, NUM(2)); + }); + }); + + describe('function params', () => { + test.concurrent('single line', async () => { + const res = await exe(` + @f(a, b) { + a + b + } + <: f(1, 2) + `); + eq(res, NUM(3)); + }); + + test.concurrent('single line with tail comma', async () => { + const res = await exe(` + @f(a, b, ) { + a + b + } + <: f(1, 2) + `); + eq(res, NUM(3)); + }); + + test.concurrent('multi line', async () => { + const res = await exe(` + @f( + a + b + ) { + a + b + } + <: f(1, 2) + `); + eq(res, NUM(3)); + }); + + test.concurrent('multi line with comma', async () => { + const res = await exe(` + @f( + a, + b + ) { + a + b + } + <: f(1, 2) + `); + eq(res, NUM(3)); + }); + + test.concurrent('multi line with tail comma', async () => { + const res = await exe(` + @f( + a, + b, + ) { + a + b + } + <: f(1, 2) + `); + eq(res, NUM(3)); + }); + }); +}); + + +describe('Comment', () => { + test.concurrent('single line comment', async () => { + const res = await exe(` + // let a = ... + let a = 42 + <: a + `); + eq(res, NUM(42)); + }); + + test.concurrent('multi line comment', async () => { + const res = await exe(` + /* variable declaration here... + let a = ... + */ + let a = 42 + <: a + `); + eq(res, NUM(42)); + }); + + test.concurrent('multi line comment 2', async () => { + const res = await exe(` + /* variable declaration here... + let a = ... + */ + let a = 42 + /* + another comment here + */ + <: a + `); + eq(res, NUM(42)); + }); + + test.concurrent('// as string', async () => { + const res = await exe('<: "//"'); + eq(res, STR('//')); + }); + + test.concurrent('line tail', async () => { + const res = await exe(` + let x = 'a' // comment + let y = 'b' + <: x + `); + eq(res, STR('a')); + }); +}); + +describe('lang version', () => { + test.concurrent('number', async () => { + const res = utils.getLangVersion(` + /// @2021 + @f(x) { + x + } + `); + assert.strictEqual(res, '2021'); + }); + + test.concurrent('chars', async () => { + const res = utils.getLangVersion(` + /// @ canary + const a = 1 + @f(x) { + x + } + f(a) + `); + assert.strictEqual(res, 'canary'); + }); + + test.concurrent('complex', async () => { + const res = utils.getLangVersion(` + /// @ 2.0-Alpha + @f(x) { + x + } + `); + assert.strictEqual(res, '2.0-Alpha'); + }); + + test.concurrent('no specified', async () => { + const res = utils.getLangVersion(` + @f(x) { + x + } + `); + assert.strictEqual(res, null); + }); +}); + +/* + * Statements + */ +describe('Cannot put multiple statements in a line', () => { + test.concurrent('var def', async () => { + try { + await exe(` + let a = 42 let b = 11 + `); + } catch (e) { + assert.ok(true); + return; + } + assert.fail(); + }); + + test.concurrent('var def (op)', async () => { + try { + await exe(` + let a = 13 + 75 let b = 24 + 146 + `); + } catch (e) { + assert.ok(true); + return; + } + assert.fail(); + }); + + test.concurrent('var def in block', async () => { + try { + await exe(` + eval { + let a = 42 let b = 11 + } + `); + } catch (e) { + assert.ok(true); + return; + } + assert.fail(); + }); +}); + +describe('Variable declaration', () => { + test.concurrent('let', async () => { + const res = await exe(` + let a = 42 + <: a + `); + eq(res, NUM(42)); + }); + test.concurrent('Do not assign to let (issue #328)', async () => { + const err = await exe(` + let hoge = 33 + hoge = 4 + `).then(() => undefined).catch(err => err); + + assert.ok(err instanceof AiScriptRuntimeError); + }); + test.concurrent('empty function', async () => { + const res = await exe(` + @hoge() { } + <: hoge() + `); + eq(res, NULL); + }); +}); + +describe('Variable assignment', () => { + test.concurrent('simple', async () => { + eq(await exe(` + var hoge = 25 + hoge = 7 + <: hoge + `), NUM(7)); + }); + test.concurrent('destructuring assignment', async () => { + eq(await exe(` + var hoge = 'foo' + var fuga = { value: 'bar' } + [{ value: hoge }, fuga] = [fuga, hoge] + <: [hoge, fuga] + `), ARR([STR('bar'), STR('foo')])); + }); +}); + +describe('for', () => { + test.concurrent('Basic', async () => { + const res = await exe(` + var count = 0 + for (let i, 10) { + count += i + 1 + } + <: count + `); + eq(res, NUM(55)); + }); + + test.concurrent('initial value', async () => { + const res = await exe(` + var count = 0 + for (let i = 2, 10) { + count += i + } + <: count + `); + eq(res, NUM(65)); + }); + + test.concurrent('wuthout iterator', async () => { + const res = await exe(` + var count = 0 + for (10) { + count = (count + 1) + } + <: count + `); + eq(res, NUM(10)); + }); + + test.concurrent('without brackets', async () => { + const res = await exe(` + var count = 0 + for let i, 10 { + count = (count + i) + } + <: count + `); + eq(res, NUM(45)); + }); + + test.concurrent('Break', async () => { + const res = await exe(` + var count = 0 + for (let i, 20) { + if (i == 11) break + count += i + } + <: count + `); + eq(res, NUM(55)); + }); + + test.concurrent('continue', async () => { + const res = await exe(` + var count = 0 + for (let i, 10) { + if (i == 5) continue + count = (count + 1) + } + <: count + `); + eq(res, NUM(9)); + }); + + test.concurrent('single statement', async () => { + const res = await exe(` + var count = 0 + for 10 count += 1 + <: count + `); + eq(res, NUM(10)); + }); + + test.concurrent('var name without space', async () => { + try { + await exe(` + for (leti, 10) { + <: i + } + `); + } catch (e) { + assert.ok(true); + return; + } + assert.fail(); + }); +}); + +describe('each', () => { + test.concurrent('standard', async () => { + const res = await exe(` + let msgs = [] + each let item, ["ai", "chan", "kawaii"] { + msgs.push([item, "!"].join()) + } + <: msgs + `); + eq(res, ARR([STR('ai!'), STR('chan!'), STR('kawaii!')])); + }); + + test.concurrent('Break', async () => { + const res = await exe(` + let msgs = [] + each let item, ["ai", "chan", "kawaii", "yo"] { + if (item == "kawaii") break + msgs.push([item, "!"].join()) + } + <: msgs + `); + eq(res, ARR([STR('ai!'), STR('chan!')])); + }); + + test.concurrent('single statement', async () => { + const res = await exe(` + let msgs = [] + each let item, ["ai", "chan", "kawaii"] msgs.push([item, "!"].join()) + <: msgs + `); + eq(res, ARR([STR('ai!'), STR('chan!'), STR('kawaii!')])); + }); + + test.concurrent('var name without space', async () => { + try { + await exe(` + each letitem, ["ai", "chan", "kawaii"] { + <: item + } + `); + } catch (e) { + assert.ok(true); + return; + } + assert.fail(); + }); +}); + +describe('while', () => { + test.concurrent('Basic', async () => { + const res = await exe(` + var count = 0 + while count < 42 { + count += 1 + } + <: count + `); + eq(res, NUM(42)); + }); + + test.concurrent('start false', async () => { + const res = await exe(` + while false { + <: 'hoge' + } + `); + eq(res, NULL); + }); +}); + +describe('do-while', () => { + test.concurrent('Basic', async () => { + const res = await exe(` + var count = 0 + do { + count += 1 + } while count < 42 + <: count + `); + eq(res, NUM(42)); + }); + + test.concurrent('start false', async () => { + const res = await exe(` + do { + <: 'hoge' + } while false + `); + eq(res, STR('hoge')); + }); +}); + +describe('loop', () => { + test.concurrent('Basic', async () => { + const res = await exe(` + var count = 0 + loop { + if (count == 10) break + count = (count + 1) + } + <: count + `); + eq(res, NUM(10)); + }); + + test.concurrent('with continue', async () => { + const res = await exe(` + var a = ["ai", "chan", "kawaii", "yo", "!"] + var b = [] + loop { + var x = a.shift() + if (x == "chan") continue + if (x == "yo") break + b.push(x) + } + <: b + `); + eq(res, ARR([STR('ai'), STR('kawaii')])); + }); +}); + +/* + * Global statements + */ +describe('meta', () => { + test.concurrent('default meta', async () => { + const res = getMeta(` + ### { a: 1, b: 2, c: 3, } + `); + eq(res, new Map([ + [null, { + a: 1, + b: 2, + c: 3, + }] + ])); + eq(res!.get(null), { + a: 1, + b: 2, + c: 3, + }); + }); + + describe('String', () => { + test.concurrent('valid', async () => { + const res = getMeta(` + ### x "hoge" + `); + eq(res, new Map([ + ['x', 'hoge'] + ])); + }); + }); + + describe('Number', () => { + test.concurrent('valid', async () => { + const res = getMeta(` + ### x 42 + `); + eq(res, new Map([ + ['x', 42] + ])); + }); + }); + + describe('Boolean', () => { + test.concurrent('valid', async () => { + const res = getMeta(` + ### x true + `); + eq(res, new Map([ + ['x', true] + ])); + }); + }); + + describe('Null', () => { + test.concurrent('valid', async () => { + const res = getMeta(` + ### x null + `); + eq(res, new Map([ + ['x', null] + ])); + }); + }); + + describe('Array', () => { + test.concurrent('valid', async () => { + const res = getMeta(` + ### x [1, 2, 3] + `); + eq(res, new Map([ + ['x', [1, 2, 3]] + ])); + }); + + test.concurrent('invalid', async () => { + try { + getMeta(` + ### x [1, (2 + 2), 3] + `); + } catch (e) { + assert.ok(true); + return; + } + assert.fail(); + }); + }); + + describe('Object', () => { + test.concurrent('valid', async () => { + const res = getMeta(` + ### x { a: 1, b: 2, c: 3, } + `); + eq(res, new Map([ + ['x', { + a: 1, + b: 2, + c: 3, + }] + ])); + }); + + test.concurrent('invalid', async () => { + try { + getMeta(` + ### x { a: 1, b: (2 + 2), c: 3, } + `); + } catch (e) { + assert.ok(true); + return; + } + assert.fail(); + }); + }); + + describe('Template', () => { + test.concurrent('invalid', async () => { + try { + getMeta(` + ### x \`foo {bar} baz\` + `); + } catch (e) { + assert.ok(true); + return; + } + assert.fail(); + }); + }); + + describe('Expression', () => { + test.concurrent('invalid', async () => { + try { + getMeta(` + ### x (1 + 1) + `); + } catch (e) { + assert.ok(true); + return; + } + assert.fail(); + }); + }); +}); + +describe('namespace', () => { + test.concurrent('standard', async () => { + const res = await exe(` + <: Foo:bar() + + :: Foo { + @bar() { "ai" } + } + `); + eq(res, STR('ai')); + }); + + test.concurrent('self ref', async () => { + const res = await exe(` + <: Foo:bar() + + :: Foo { + let ai = "kawaii" + @bar() { ai } + } + `); + eq(res, STR('kawaii')); + }); + + test.concurrent('cannot declare mutable variable', async () => { + try { + await exe(` + :: Foo { + var ai = "kawaii" + } + `); + } catch (e) { + assert.ok(true); + return; + } + assert.fail(); + }); + + test.concurrent('nested', async () => { + const res = await exe(` + <: Foo:Bar:baz() + + :: Foo { + :: Bar { + @baz() { "ai" } + } + } + `); + eq(res, STR('ai')); + }); + + test.concurrent('nested ref', async () => { + const res = await exe(` + <: Foo:baz + + :: Foo { + let baz = Bar:ai + :: Bar { + let ai = "kawaii" + } + } + `); + eq(res, STR('kawaii')); + }); +}); + +describe('operators', () => { + test.concurrent('==', async () => { + eq(await exe('<: (1 == 1)'), BOOL(true)); + eq(await exe('<: (1 == 2)'), BOOL(false)); + eq(await exe('<: (Core:type == Core:type)'), BOOL(true)); + eq(await exe('<: (Core:type == Core:gt)'), BOOL(false)); + eq(await exe('<: (@(){} == @(){})'), BOOL(false)); + eq(await exe('<: (Core:eq == @(){})'), BOOL(false)); + eq(await exe(` + let f = @(){} + let g = f + + <: (f == g) + `), BOOL(true)); + }); + + test.concurrent('!=', async () => { + eq(await exe('<: (1 != 2)'), BOOL(true)); + eq(await exe('<: (1 != 1)'), BOOL(false)); + }); + + test.concurrent('&&', async () => { + eq(await exe('<: (true && true)'), BOOL(true)); + eq(await exe('<: (true && false)'), BOOL(false)); + eq(await exe('<: (false && true)'), BOOL(false)); + eq(await exe('<: (false && false)'), BOOL(false)); + eq(await exe('<: (false && null)'), BOOL(false)); + try { + await exe('<: (true && null)'); + } catch (e) { + assert.ok(e instanceof AiScriptRuntimeError); + return; + } + + eq( + await exe(` + var tmp = null + + @func() { + tmp = true + return true + } + + false && func() + + <: tmp + `), + NULL + ) + + eq( + await exe(` + var tmp = null + + @func() { + tmp = true + return true + } + + true && func() + + <: tmp + `), + BOOL(true) + ) + + assert.fail(); + }); + + test.concurrent('||', async () => { + eq(await exe('<: (true || true)'), BOOL(true)); + eq(await exe('<: (true || false)'), BOOL(true)); + eq(await exe('<: (false || true)'), BOOL(true)); + eq(await exe('<: (false || false)'), BOOL(false)); + eq(await exe('<: (true || null)'), BOOL(true)); + try { + await exe('<: (false || null)'); + } catch (e) { + assert.ok(e instanceof AiScriptRuntimeError); + return; + } + + eq( + await exe(` + var tmp = null + + @func() { + tmp = true + return true + } + + true || func() + + <: tmp + `), + NULL + ) + + eq( + await exe(` + var tmp = null + + @func() { + tmp = true + return true + } + + false || func() + + <: tmp + `), + BOOL(true) + ) + + assert.fail(); + }); + + test.concurrent('+', async () => { + eq(await exe('<: (1 + 1)'), NUM(2)); + }); + + test.concurrent('-', async () => { + eq(await exe('<: (1 - 1)'), NUM(0)); + }); + + test.concurrent('*', async () => { + eq(await exe('<: (1 * 1)'), NUM(1)); + }); + + test.concurrent('^', async () => { + eq(await exe('<: (1 ^ 0)'), NUM(1)); + }); + + test.concurrent('/', async () => { + eq(await exe('<: (1 / 1)'), NUM(1)); + }); + + test.concurrent('%', async () => { + eq(await exe('<: (1 % 1)'), NUM(0)); + }); + + test.concurrent('>', async () => { + eq(await exe('<: (2 > 1)'), BOOL(true)); + eq(await exe('<: (1 > 1)'), BOOL(false)); + eq(await exe('<: (0 > 1)'), BOOL(false)); + }); + + test.concurrent('<', async () => { + eq(await exe('<: (2 < 1)'), BOOL(false)); + eq(await exe('<: (1 < 1)'), BOOL(false)); + eq(await exe('<: (0 < 1)'), BOOL(true)); + }); + + test.concurrent('>=', async () => { + eq(await exe('<: (2 >= 1)'), BOOL(true)); + eq(await exe('<: (1 >= 1)'), BOOL(true)); + eq(await exe('<: (0 >= 1)'), BOOL(false)); + }); + + test.concurrent('<=', async () => { + eq(await exe('<: (2 <= 1)'), BOOL(false)); + eq(await exe('<: (1 <= 1)'), BOOL(true)); + eq(await exe('<: (0 <= 1)'), BOOL(true)); + }); + + test.concurrent('precedence', async () => { + eq(await exe('<: 1 + 2 * 3 + 4'), NUM(11)); + eq(await exe('<: 1 + 4 / 4 + 1'), NUM(3)); + eq(await exe('<: 1 + 1 == 2 && 2 * 2 == 4'), BOOL(true)); + eq(await exe('<: (1 + 1) * 2'), NUM(4)); + }); + + test.concurrent('negative numbers', async () => { + eq(await exe('<: 1+-1'), NUM(0)); + eq(await exe('<: 1--1'), NUM(2));//反直観的、禁止される可能性がある? + eq(await exe('<: -1*-1'), NUM(1)); + eq(await exe('<: -1==-1'), BOOL(true)); + eq(await exe('<: 1>-1'), BOOL(true)); + eq(await exe('<: -1<1'), BOOL(true)); + }); + +}); + +describe('not', () => { + test.concurrent('Basic', async () => { + const res = await exe(` + <: !true + `); + eq(res, BOOL(false)); + }); +}); + +describe('Infix expression', () => { + test.concurrent('simple infix expression', async () => { + eq(await exe('<: 0 < 1'), BOOL(true)); + eq(await exe('<: 1 + 1'), NUM(2)); + }); + + test.concurrent('combination', async () => { + eq(await exe('<: 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10'), NUM(55)); + eq(await exe('<: Core:add(1, 3) * Core:mul(2, 5)'), NUM(40)); + }); + + test.concurrent('use parentheses to distinguish expr', async () => { + eq(await exe('<: (1 + 10) * (2 + 5)'), NUM(77)); + }); + + test.concurrent('syntax symbols vs infix operators', async () => { + const res = await exe(` + <: match true { + case 1 == 1 => "true" + case 1 < 1 => "false" + } + `); + eq(res, STR('true')); + }); + + test.concurrent('number + if expression', async () => { + eq(await exe('<: 1 + if true 1 else 2'), NUM(2)); + }); + + test.concurrent('number + match expression', async () => { + const res = await exe(` + <: 1 + match 2 == 2 { + case true => 3 + case false => 4 + } + `); + eq(res, NUM(4)); + }); + + test.concurrent('eval + eval', async () => { + eq(await exe('<: eval { 1 } + eval { 1 }'), NUM(2)); + }); + + test.concurrent('disallow line break', async () => { + try { + await exe(` + <: 1 + + 1 + 1 + `); + } catch (e) { + assert.ok(true); + return; + } + assert.fail(); + }); + + test.concurrent('escaped line break', async () => { + eq(await exe(` + <: 1 + \\ + 1 + 1 + `), NUM(3)); + }); + + test.concurrent('infix-to-fncall on namespace', async () => { + eq( + await exe(` + :: Hoge { + @add(x, y) { + x + y + } + } + <: Hoge:add(1, 2) + `), + NUM(3) + ); + }); +}); + +describe('if', () => { + test.concurrent('if', async () => { + const res1 = await exe(` + var msg = "ai" + if true { + msg = "kawaii" + } + <: msg + `); + eq(res1, STR('kawaii')); + + const res2 = await exe(` + var msg = "ai" + if false { + msg = "kawaii" + } + <: msg + `); + eq(res2, STR('ai')); + }); + + test.concurrent('else', async () => { + const res1 = await exe(` + var msg = null + if true { + msg = "ai" + } else { + msg = "kawaii" + } + <: msg + `); + eq(res1, STR('ai')); + + const res2 = await exe(` + var msg = null + if false { + msg = "ai" + } else { + msg = "kawaii" + } + <: msg + `); + eq(res2, STR('kawaii')); + }); + + test.concurrent('elif', async () => { + const res1 = await exe(` + var msg = "bebeyo" + if false { + msg = "ai" + } elif true { + msg = "kawaii" + } + <: msg + `); + eq(res1, STR('kawaii')); + + const res2 = await exe(` + var msg = "bebeyo" + if false { + msg = "ai" + } elif false { + msg = "kawaii" + } + <: msg + `); + eq(res2, STR('bebeyo')); + }); + + test.concurrent('if ~ elif ~ else', async () => { + const res1 = await exe(` + var msg = null + if false { + msg = "ai" + } elif true { + msg = "chan" + } else { + msg = "kawaii" + } + <: msg + `); + eq(res1, STR('chan')); + + const res2 = await exe(` + var msg = null + if false { + msg = "ai" + } elif false { + msg = "chan" + } else { + msg = "kawaii" + } + <: msg + `); + eq(res2, STR('kawaii')); + }); + + test.concurrent('expr', async () => { + const res1 = await exe(` + <: if true "ai" else "kawaii" + `); + eq(res1, STR('ai')); + + const res2 = await exe(` + <: if false "ai" else "kawaii" + `); + eq(res2, STR('kawaii')); + }); +}); + +describe('eval', () => { + test.concurrent('returns value', async () => { + const res = await exe(` + let foo = eval { + let a = 1 + let b = 2 + (a + b) + } + + <: foo + `); + eq(res, NUM(3)); + }); +}); + +describe('match', () => { + test.concurrent('Basic', async () => { + const res = await exe(` + <: match 2 { + case 1 => "a" + case 2 => "b" + case 3 => "c" + } + `); + eq(res, STR('b')); + }); + + test.concurrent('When default not provided, returns null', async () => { + const res = await exe(` + <: match 42 { + case 1 => "a" + case 2 => "b" + case 3 => "c" + } + `); + eq(res, NULL); + }); + + test.concurrent('With default', async () => { + const res = await exe(` + <: match 42 { + case 1 => "a" + case 2 => "b" + case 3 => "c" + default => "d" + } + `); + eq(res, STR('d')); + }); + + test.concurrent('With block', async () => { + const res = await exe(` + <: match 2 { + case 1 => 1 + case 2 => { + let a = 1 + let b = 2 + (a + b) + } + case 3 => 3 + } + `); + eq(res, NUM(3)); + }); + + test.concurrent('With return', async () => { + const res = await exe(` + @f(x) { + match x { + case 1 => { + return "ai" + } + } + "foo" + } + <: f(1) + `); + eq(res, STR('ai')); + }); +}); + +describe('exists', () => { + test.concurrent('Basic', async () => { + const res = await exe(` + let foo = null + <: [(exists foo), (exists bar)] + `); + eq(res, ARR([BOOL(true), BOOL(false)])); + }); +}); + diff --git a/test/testutils.ts b/test/testutils.ts new file mode 100644 index 00000000..f1e3a380 --- /dev/null +++ b/test/testutils.ts @@ -0,0 +1,36 @@ +import * as assert from 'assert'; +import { Parser, Interpreter } from '../src'; + +export async function exe(script: string): Promise { + const parser = new Parser(); + let result = undefined; + const interpreter = new Interpreter({}, { + out(value) { + if (!result) result = value; + else if (!Array.isArray(result)) result = [result, value]; + else result.push(value); + }, + log(type, {val}) { + if (type === 'end') result ??= val; + }, + maxStep: 9999, + }); + const ast = parser.parse(script); + await interpreter.exec(ast); + return result; +}; + +export const getMeta = (script: string) => { + const parser = new Parser(); + const ast = parser.parse(script); + + const metadata = Interpreter.collectMetadata(ast); + + return metadata; +}; + +export const eq = (a, b) => { + assert.deepEqual(a.type, b.type); + assert.deepEqual(a.value, b.value); +}; + diff --git a/unreleased/next-past.md b/unreleased/next-past.md new file mode 100644 index 00000000..6f2fd89e --- /dev/null +++ b/unreleased/next-past.md @@ -0,0 +1,14 @@ +- 新しいAiScriptパーサーを実装 + - スペースの厳密さが緩和 + - **Breaking Change** 改行トークンを導入。改行の扱いが今までより厳密になりました。改行することができる部分以外では文法エラーになります。 +- 文字列リテラルやテンプレートで、`\`とそれに続く1文字は全てエスケープシーケンスとして扱われるように +- 文法エラーの表示を改善。理由を詳細に表示するように。 +- 複数行のコメントがある時に文法エラーの表示行数がずれる問題を解消しました。 +- 実行時エラーの発生位置が表示されるように。 +- **Breaking Change** パースの都合によりmatch文の構文を変更。パターンの前に`case`キーワードが必要となり、`*`は`default`に変更。 +- **Breaking Change** 多くの予約語を追加。これまで変数名等に使えていた名前に影響が出る可能性があります。 +- **Breaking Change** 配列及び関数の引数において、空白区切りが使用できなくなりました。`,`または改行が必要です。 +- **Breaking Change** 関数同士の比較の実装 +- **Breaking Change** `+`や`!`などの演算子の優先順位に変更があります。新しい順序は[syntax.md](docs/syntax.md#%E6%BC%94%E7%AE%97%E5%AD%90)を参照して下さい。 +- **Breaking Change** 組み込み関数`Num:to_hex`は組み込みプロパティ`num#to_hex`に移動しました。 +- **Breaking Change** `arr.sort`を安定ソートに変更 diff --git a/unreleased/optional-args.md b/unreleased/optional-args.md new file mode 100644 index 00000000..99ffc49d --- /dev/null +++ b/unreleased/optional-args.md @@ -0,0 +1,2 @@ +- 省略可能引数と初期値付き引数を追加。引数名に`?`を後置することでその引数は省略可能となります。引数に`=<式>`を後置すると引数に初期値を設定できます。省略可能引数は初期値`null`の引数と同等です。 + - BREAKING: いずれでもない引数が省略されると即時エラーとなるようになりました。 diff --git a/unreleased/while.md b/unreleased/while.md new file mode 100644 index 00000000..c49dc5c1 --- /dev/null +++ b/unreleased/while.md @@ -0,0 +1 @@ +- while文とdo-while文を追加