From 8101167a21a580cda2bad30f3a9e1ef6c1a39b2e Mon Sep 17 00:00:00 2001 From: marihachi Date: Sun, 15 Oct 2023 16:52:40 +0900 Subject: [PATCH 01/62] =?UTF-8?q?enhance:=20=E5=86=8D=E5=B8=B0=E4=B8=8B?= =?UTF-8?q?=E9=99=8DLL=E3=83=91=E3=83=BC=E3=82=B5=E3=83=BC=E3=81=AE?= =?UTF-8?q?=E5=AE=9F=E8=A3=85=20(#360)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #360 --- CHANGELOG.md | 7 + docs/parser/scanner.md | 9 + etc/aiscript.api.md | 385 +-------------- package.json | 8 +- parse.js | 3 +- src/@types/parser.d.ts | 6 - src/index.ts | 2 - src/node.ts | 111 ++--- src/parser/index.ts | 36 +- src/parser/node.ts | 329 ------------- src/parser/parser.peggy | 596 ----------------------- src/parser/plugins/infix-to-fncall.ts | 146 ------ src/parser/plugins/set-attribute.ts | 48 -- src/parser/plugins/transform-chain.ts | 39 -- src/parser/plugins/validate-keyword.ts | 9 +- src/parser/plugins/validate-type.ts | 6 +- src/parser/scanner.ts | 608 ++++++++++++++++++++++++ src/parser/streams/char-stream.ts | 139 ++++++ src/parser/streams/token-stream.ts | 124 +++++ src/parser/syntaxes/common.ts | 142 ++++++ src/parser/syntaxes/expressions.ts | 623 +++++++++++++++++++++++++ src/parser/syntaxes/statements.ts | 408 ++++++++++++++++ src/parser/syntaxes/toplevel.ts | 115 +++++ src/parser/token.ts | 128 +++++ src/parser/utils.ts | 19 + src/parser/visit.ts | 96 ++-- test/index.ts | 64 ++- test/parser.ts | 146 ++++++ 28 files changed, 2620 insertions(+), 1732 deletions(-) create mode 100644 docs/parser/scanner.md delete mode 100644 src/@types/parser.d.ts delete mode 100644 src/parser/node.ts delete mode 100644 src/parser/parser.peggy delete mode 100644 src/parser/plugins/infix-to-fncall.ts delete mode 100644 src/parser/plugins/set-attribute.ts delete mode 100644 src/parser/plugins/transform-chain.ts create mode 100644 src/parser/scanner.ts create mode 100644 src/parser/streams/char-stream.ts create mode 100644 src/parser/streams/token-stream.ts create mode 100644 src/parser/syntaxes/common.ts create mode 100644 src/parser/syntaxes/expressions.ts create mode 100644 src/parser/syntaxes/statements.ts create mode 100644 src/parser/syntaxes/toplevel.ts create mode 100644 src/parser/token.ts create mode 100644 src/parser/utils.ts create mode 100644 test/parser.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index a280b091..61ec5f6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,13 @@ バージョン0.16.0に記録漏れがありました。 >- 関数`Str:from_codepoint` `Str#codepoint_at`を追加 +# Next +- 新しいAiScriptパーサーが実装されました。 +- スペースの厳密さが緩和されました。 +- 文字列リテラルやテンプレートで、`\`とそれに続く1文字は全てエスケープシーケンスとして扱われるようになりました。 +## Breaking changes +- 改行トークンを導入。改行の扱いが今までより厳密になりました。改行することができると決められた部分以外では文法エラーになります。 + # 0.17.0 - `package.json`を修正 diff --git a/docs/parser/scanner.md b/docs/parser/scanner.md new file mode 100644 index 00000000..6e78da1b --- /dev/null +++ b/docs/parser/scanner.md @@ -0,0 +1,9 @@ +# Scanner 設計資料 +作成者: marihachi + +## 現在のトークンと先読みされたトークン +_tokensの0番には現在のトークンが保持される。また、トークンが先読みされた場合は1番以降にそれらのトークンが保持されていくことになる。 +例えば、次のトークンを1つ先読みした場合は0番に現在のトークンが入り1番に先読みされたトークンが入る。 + +nextメソッドで現在位置が移動すると、それまで0番にあったトークン(現在のトークン)は配列から削除され、1番にあった要素は現在のトークンとなる。 +配列から全てのトークンが無くなった場合はトークンの読み取りが実行される。 diff --git a/etc/aiscript.api.md b/etc/aiscript.api.md index c430eb08..82532fc2 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) abstract class AiScriptError extends Error { constructor(message: string, info?: any); @@ -64,13 +55,6 @@ type And = NodeBase & { right: Expression; }; -// @public (undocumented) -type And_2 = NodeBase_2 & { - type: 'and'; - left: Expression_2; - right: Expression_2; -}; - // @public (undocumented) const ARR: (arr: VArr['value']) => VArr; @@ -80,14 +64,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; @@ -113,23 +89,15 @@ 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, Loc, Node_2 as Node, - Statement, - Expression, Namespace, Meta, + Statement, Definition, Attribute, Return, @@ -141,6 +109,7 @@ declare namespace Ast { AddAssign, SubAssign, Assign, + Expression, Not, And, Or, @@ -182,25 +151,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; @@ -210,12 +166,6 @@ type Bool = NodeBase & { value: boolean; }; -// @public (undocumented) -type Bool_2 = NodeBase_2 & ChainProp & { - type: 'bool'; - value: boolean; -}; - // @public (undocumented) const BREAK: () => Value; @@ -224,17 +174,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'; @@ -242,22 +181,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; @@ -266,67 +189,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'; @@ -337,16 +199,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'; @@ -355,14 +207,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,23 +231,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,17 +257,6 @@ type Fn = NodeBase & { children: (Statement | Expression)[]; }; -// @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; @@ -445,13 +267,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'; @@ -462,34 +277,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'; @@ -502,24 +298,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'; @@ -527,29 +305,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[]; -}; - -// @public (undocumented) -type InfixOperator = '||' | '&&' | '==' | '!=' | '<=' | '>=' | '<' | '>' | '+' | '-' | '*' | '^' | '/' | '%'; - // @public (undocumented) export class Interpreter { constructor(consts: Record, opts?: { @@ -586,9 +341,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; @@ -601,9 +353,6 @@ 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; @@ -612,8 +361,8 @@ function jsToVal(val: any): Value; // @public type Loc = { - start: number; - end: number; + line: number; + column: number; }; // @public (undocumented) @@ -622,12 +371,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'; @@ -639,17 +382,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'; @@ -657,13 +389,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'; @@ -671,13 +396,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'; @@ -686,17 +404,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 -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 { @@ -711,12 +419,6 @@ type Not = NodeBase & { expr: Expression; }; -// @public (undocumented) -type Not_2 = NodeBase_2 & { - type: 'not'; - expr: Expression_2; -}; - // @public (undocumented) const NULL: { type: "null"; @@ -727,11 +429,6 @@ type Null = NodeBase & { type: 'null'; }; -// @public (undocumented) -type Null_2 = NodeBase_2 & ChainProp & { - type: 'null'; -}; - // @public (undocumented) const NUM: (num: VNum['value']) => VNum; @@ -741,12 +438,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; @@ -756,12 +447,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'; @@ -769,13 +454,6 @@ type Or = NodeBase & { right: Expression; }; -// @public (undocumented) -type Or_2 = NodeBase_2 & { - type: 'or'; - left: Expression_2; - right: Expression_2; -}; - // @public (undocumented) export class Parser { constructor(); @@ -788,17 +466,11 @@ 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 (undocumented) type Prop = NodeBase & { type: 'prop'; @@ -806,19 +478,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; @@ -831,12 +490,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']); @@ -861,10 +514,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; @@ -874,12 +523,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'; @@ -887,25 +530,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"; @@ -915,9 +545,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; diff --git a/package.json b/package.json index 4e635669..b63d2a89 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": "npm run peg && npm run ts", - "build-debug": "npm run peg-debug && tsc", + "build": "npm run ts", "api": "npx api-extractor run --local --verbose", "api-prod": "npx api-extractor run --verbose", "lint": "eslint . --ext .js,.jsx,.ts,.tsx", @@ -44,11 +40,9 @@ "@typescript-eslint/eslint-plugin": "6.7.5", "@typescript-eslint/parser": "6.7.5", "chalk": "5.3.0", - "copyfiles": "2.4.1", "eslint": "8.51.0", "eslint-plugin-import": "2.28.1", "jest": "29.7.0", - "peggy": "3.0.2", "ts-jest": "29.1.1", "ts-jest-resolver": "2.0.1", "ts-node": "10.9.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/index.ts b/src/index.ts index a0811739..b524c404 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'; export { Interpreter }; @@ -17,6 +16,5 @@ export { values }; export { Parser }; export { ParserPlugin }; export { PluginType }; -export { Cst }; export { errors }; export { Ast }; diff --git a/src/node.ts b/src/node.ts index 79153c10..caa33196 100644 --- a/src/node.ts +++ b/src/node.ts @@ -1,15 +1,31 @@ /** * ASTノード - * - * ASTノードはCSTノードをインタプリタ等から操作しやすい構造に変形したものです。 */ export type Loc = { - start: number; - end: number; + line: number; + column: number; }; -export type Node = Namespace | Meta | Statement | Expression | TypeSource; +export type Node = Namespace | Meta | Statement | Expression | TypeSource | Attribute; + +type NodeBase = { + loc: Loc; // コード位置 +}; + +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 +46,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 +113,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; // 式 @@ -241,14 +240,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 b9a2dd77..00000000 --- a/src/parser/node.ts +++ /dev/null @@ -1,329 +0,0 @@ -/** - * CSTノード - * - * パーサーが生成する直接的な処理結果です。 - * パーサーが生成しやすい形式になっているため、インタプリタ等では操作しにくい構造になっていることがあります。 - * この処理結果がプラグインによって処理されるとASTノードとなります。 -*/ - -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?: { - 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; - 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[]; -}; - -export type Not = NodeBase & { - type: 'not'; - expr: Expression; -}; - -export type And = NodeBase & { - type: 'and'; - left: Expression; - right: Expression; -} - -export type Or = NodeBase & { - type: 'or'; - left: Expression; - right: Expression; -} - -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 2c55e520..00000000 --- a/src/parser/parser.peggy +++ /dev/null @@ -1,596 +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 ''; } - / "/*" (!"*/" .)* "*/" { return ''; } - - -// -// 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) - }); -} - -InfixSp - = "\\" LF - / __ - -Op - = ("||" / "&&" / "==" / "!=" / "<=" / ">=" / "<" / ">" / "+" / "-" / "*" / "^" / "/" / "%") -{ return text(); } - -Not - = "!" expr:Expr -{ - return createNode('not', { - expr: expr, - }); -} - - -// chain - -Chain - = e:Expr3 chain:(CallChain / IndexChain / PropChain)+ -{ 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 7d551320..00000000 --- a/src/parser/plugins/infix-to-fncall.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { visitNode } from '../visit.js'; -import { AiScriptSyntaxError } from '../../error.js'; -import type * as Cst from '../node.js'; - -/** - * 中置演算子式を表す木 - * 1 + 3 ならば次のようなイメージ - * ``` - * (+) - * (1) (3) - * ``` - */ -type InfixTree = { - type: 'infixTree'; - left: InfixTree | Cst.Node; - right: InfixTree | Cst.Node; - info: { - 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 { - return { - type: 'call', - target: { type: 'identifier', name: tree.info.func }, - args: [treeToNode(tree.left), treeToNode(tree.right)], - } 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 => ({ - type: 'and', - left: treeToNode(infix.left), - right: treeToNode(infix.right), - }) as Cst.And, - priority: 3, - }, - '||': { - mapFn: infix => ({ - type: 'or', - left: treeToNode(infix.left), - right: treeToNode(infix.right), - }) as Cst.Or, - priority: 3, - }, -}; - -/** - * NInfix を関数呼び出し形式に変換する - */ -function transform(node: Cst.Infix): Cst.Node { - const infos = node.operators.map(op => { - const info = infoTable[op]; - if (info == null) { - throw new AiScriptSyntaxError(`No such operator: ${op}.`); - } - return 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..1d7d5b10 100644 --- a/src/parser/plugins/validate-keyword.ts +++ b/src/parser/plugins/validate-keyword.ts @@ -1,6 +1,6 @@ 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', @@ -46,13 +46,12 @@ function throwReservedWordError(name: string): void { throw new AiScriptSyntaxError(`Reserved word "${name}" cannot be used as variable name.`); } -function validateNode(node: Cst.Node): Cst.Node { +function validateNode(node: Ast.Node): Ast.Node { switch (node.type) { case 'def': case 'attr': case 'ns': - case 'identifier': - case 'propChain': { + case 'identifier': { if (reservedWord.includes(node.name)) { throwReservedWordError(node.name); } @@ -77,7 +76,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..67a5d93e --- /dev/null +++ b/src/parser/scanner.ts @@ -0,0 +1,608 @@ +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 } 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 get kind(): TokenKind { + return this.token.kind; + } + + /** + * カーソル位置を次のトークンへ進めます。 + */ + 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.kind !== kind) { + throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[this.kind]}`); + } + } + + /** + * カーソル位置にあるトークンが指定したトークンの種類と一致することを確認し、 + * カーソル位置を次のトークンへ進めます。 + */ + 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 loc = this.stream.getPos(); + + if (lineBreakChars.includes(this.stream.char)) { + this.stream.next(); + token = TOKEN(TokenKind.NewLine, loc, { hasLeftSpacing }); + return token; + } + switch (this.stream.char) { + case '!': { + this.stream.next(); + if ((this.stream.char as string) === '=') { + this.stream.next(); + token = TOKEN(TokenKind.NotEq, loc, { hasLeftSpacing }); + } else { + token = TOKEN(TokenKind.Not, loc, { hasLeftSpacing }); + } + break; + } + case '"': + case '\'': { + token = this.readStringLiteral(hasLeftSpacing); + break; + } + case '#': { + this.stream.next(); + if ((this.stream.char as string) === '#') { + this.stream.next(); + if ((this.stream.char as string) === '#') { + this.stream.next(); + token = TOKEN(TokenKind.Sharp3, loc, { hasLeftSpacing }); + } + } else if ((this.stream.char as string) === '[') { + this.stream.next(); + token = TOKEN(TokenKind.OpenSharpBracket, loc, { hasLeftSpacing }); + } else { + throw new AiScriptSyntaxError('invalid character: "#"'); + } + break; + } + case '%': { + this.stream.next(); + token = TOKEN(TokenKind.Percent, loc, { hasLeftSpacing }); + break; + } + case '&': { + this.stream.next(); + if ((this.stream.char as string) === '&') { + this.stream.next(); + token = TOKEN(TokenKind.And2, loc, { hasLeftSpacing }); + } + break; + } + case '(': { + this.stream.next(); + token = TOKEN(TokenKind.OpenParen, loc, { hasLeftSpacing }); + break; + } + case ')': { + this.stream.next(); + token = TOKEN(TokenKind.CloseParen, loc, { hasLeftSpacing }); + break; + } + case '*': { + this.stream.next(); + token = TOKEN(TokenKind.Asterisk, loc, { hasLeftSpacing }); + break; + } + case '+': { + this.stream.next(); + if ((this.stream.char as string) === '=') { + this.stream.next(); + token = TOKEN(TokenKind.PlusEq, loc, { hasLeftSpacing }); + } else { + token = TOKEN(TokenKind.Plus, loc, { hasLeftSpacing }); + } + break; + } + case ',': { + this.stream.next(); + token = TOKEN(TokenKind.Comma, loc, { hasLeftSpacing }); + break; + } + case '-': { + this.stream.next(); + if ((this.stream.char as string) === '=') { + this.stream.next(); + token = TOKEN(TokenKind.MinusEq, loc, { hasLeftSpacing }); + } else { + token = TOKEN(TokenKind.Minus, loc, { hasLeftSpacing }); + } + break; + } + case '.': { + this.stream.next(); + token = TOKEN(TokenKind.Dot, loc, { hasLeftSpacing }); + break; + } + case '/': { + this.stream.next(); + if ((this.stream.char as string) === '*') { + this.stream.next(); + this.skipCommentRange(); + continue; + } else if ((this.stream.char as string) === '/') { + this.stream.next(); + this.skipCommentLine(); + continue; + } else { + token = TOKEN(TokenKind.Slash, loc, { hasLeftSpacing }); + } + break; + } + case ':': { + this.stream.next(); + if ((this.stream.char as string) === ':') { + this.stream.next(); + token = TOKEN(TokenKind.Colon2, loc, { hasLeftSpacing }); + } else { + token = TOKEN(TokenKind.Colon, loc, { hasLeftSpacing }); + } + break; + } + case ';': { + this.stream.next(); + token = TOKEN(TokenKind.SemiColon, loc, { hasLeftSpacing }); + break; + } + case '<': { + this.stream.next(); + if ((this.stream.char as string) === '=') { + this.stream.next(); + token = TOKEN(TokenKind.LtEq, loc, { hasLeftSpacing }); + } else if ((this.stream.char as string) === ':') { + this.stream.next(); + token = TOKEN(TokenKind.Out, loc, { hasLeftSpacing }); + } else { + token = TOKEN(TokenKind.Lt, loc, { hasLeftSpacing }); + } + break; + } + case '=': { + this.stream.next(); + if ((this.stream.char as string) === '=') { + this.stream.next(); + token = TOKEN(TokenKind.Eq2, loc, { hasLeftSpacing }); + } else if ((this.stream.char as string) === '>') { + this.stream.next(); + token = TOKEN(TokenKind.Arrow, loc, { hasLeftSpacing }); + } else { + token = TOKEN(TokenKind.Eq, loc, { hasLeftSpacing }); + } + break; + } + case '>': { + this.stream.next(); + if ((this.stream.char as string) === '=') { + this.stream.next(); + token = TOKEN(TokenKind.GtEq, loc, { hasLeftSpacing }); + } else { + token = TOKEN(TokenKind.Gt, loc, { hasLeftSpacing }); + } + break; + } + case '@': { + this.stream.next(); + token = TOKEN(TokenKind.At, loc, { hasLeftSpacing }); + break; + } + case '[': { + this.stream.next(); + token = TOKEN(TokenKind.OpenBracket, loc, { hasLeftSpacing }); + break; + } + case '\\': { + this.stream.next(); + token = TOKEN(TokenKind.BackSlash, loc, { hasLeftSpacing }); + break; + } + case ']': { + this.stream.next(); + token = TOKEN(TokenKind.CloseBracket, loc, { hasLeftSpacing }); + break; + } + case '^': { + this.stream.next(); + token = TOKEN(TokenKind.Hat, loc, { hasLeftSpacing }); + break; + } + case '`': { + token = this.readTemplate(hasLeftSpacing); + break; + } + case '{': { + this.stream.next(); + token = TOKEN(TokenKind.OpenBrace, loc, { hasLeftSpacing }); + break; + } + case '|': { + this.stream.next(); + if ((this.stream.char as string) === '|') { + this.stream.next(); + token = TOKEN(TokenKind.Or2, loc, { hasLeftSpacing }); + } + break; + } + case '}': { + this.stream.next(); + token = TOKEN(TokenKind.CloseBrace, loc, { 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}"`); + } + break; + } + return token; + } + + private tryReadWord(hasLeftSpacing: boolean): Token | undefined { + // read a word + let value = ''; + + const loc = 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, loc, { hasLeftSpacing }); + } + case 'true': { + return TOKEN(TokenKind.TrueKeyword, loc, { hasLeftSpacing }); + } + case 'false': { + return TOKEN(TokenKind.FalseKeyword, loc, { hasLeftSpacing }); + } + case 'each': { + return TOKEN(TokenKind.EachKeyword, loc, { hasLeftSpacing }); + } + case 'for': { + return TOKEN(TokenKind.ForKeyword, loc, { hasLeftSpacing }); + } + case 'loop': { + return TOKEN(TokenKind.LoopKeyword, loc, { hasLeftSpacing }); + } + case 'break': { + return TOKEN(TokenKind.BreakKeyword, loc, { hasLeftSpacing }); + } + case 'continue': { + return TOKEN(TokenKind.ContinueKeyword, loc, { hasLeftSpacing }); + } + case 'match': { + return TOKEN(TokenKind.MatchKeyword, loc, { hasLeftSpacing }); + } + case 'case': { + return TOKEN(TokenKind.CaseKeyword, loc, { hasLeftSpacing }); + } + case 'default': { + return TOKEN(TokenKind.DefaultKeyword, loc, { hasLeftSpacing }); + } + case 'if': { + return TOKEN(TokenKind.IfKeyword, loc, { hasLeftSpacing }); + } + case 'elif': { + return TOKEN(TokenKind.ElifKeyword, loc, { hasLeftSpacing }); + } + case 'else': { + return TOKEN(TokenKind.ElseKeyword, loc, { hasLeftSpacing }); + } + case 'return': { + return TOKEN(TokenKind.ReturnKeyword, loc, { hasLeftSpacing }); + } + case 'eval': { + return TOKEN(TokenKind.EvalKeyword, loc, { hasLeftSpacing }); + } + case 'var': { + return TOKEN(TokenKind.VarKeyword, loc, { hasLeftSpacing }); + } + case 'let': { + return TOKEN(TokenKind.LetKeyword, loc, { hasLeftSpacing }); + } + case 'exists': { + return TOKEN(TokenKind.ExistsKeyword, loc, { hasLeftSpacing }); + } + default: { + return TOKEN(TokenKind.Identifier, loc, { hasLeftSpacing, value }); + } + } + } + + private tryReadDigits(hasLeftSpacing: boolean): Token | undefined { + let wholeNumber = ''; + let fractional = ''; + + const loc = 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'); + } + } + let value; + if (fractional.length > 0) { + value = wholeNumber + '.' + fractional; + } else { + value = wholeNumber; + } + return TOKEN(TokenKind.NumberLiteral, loc, { hasLeftSpacing, value }); + } + + private readStringLiteral(hasLeftSpacing: boolean): Token { + let value = ''; + const literalMark = this.stream.char; + let state: 'string' | 'escape' | 'finish' = 'string'; + + const loc = this.stream.getPos(); + this.stream.next(); + + while (state !== 'finish') { + switch (state) { + case 'string': { + if (this.stream.eof) { + throw new AiScriptSyntaxError('unexpected EOF'); + } + 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'); + } + value += this.stream.char; + this.stream.next(); + state = 'string'; + break; + } + } + } + return TOKEN(TokenKind.StringLiteral, loc, { hasLeftSpacing, value }); + } + + private readTemplate(hasLeftSpacing: boolean): Token { + const elements: Token[] = []; + let buf = ''; + let tokenBuf: Token[] = []; + let state: 'string' | 'escape' | 'expr' | 'finish' = 'string'; + + const loc = this.stream.getPos(); + let elementLoc = loc; + this.stream.next(); + + while (state !== 'finish') { + switch (state) { + case 'string': { + // テンプレートの終了が無いままEOFに達した + if (this.stream.eof) { + throw new AiScriptSyntaxError('unexpected EOF'); + } + // エスケープ + 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, elementLoc, { hasLeftSpacing, value: buf })); + } + state = 'finish'; + break; + } + // 埋め込み式の開始 + if (this.stream.char === '{') { + this.stream.next(); + if (buf.length > 0) { + elements.push(TOKEN(TokenKind.TemplateStringElement, elementLoc, { hasLeftSpacing, value: buf })); + buf = ''; + } + // ここから式エレメントになるので位置を更新 + elementLoc = 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'); + } + // 普通の文字として取り込み + buf += this.stream.char; + this.stream.next(); + // 通常の文字列に戻る + state = 'string'; + break; + } + case 'expr': { + // 埋め込み式の終端記号が無いままEOFに達した + if (this.stream.eof) { + throw new AiScriptSyntaxError('unexpected EOF'); + } + // skip spasing + if (spaceChars.includes(this.stream.char)) { + this.stream.next(); + continue; + } + // 埋め込み式の終了 + if ((this.stream.char as string) === '}') { + this.stream.next(); + elements.push(TOKEN(TokenKind.TemplateExprElement, elementLoc, { hasLeftSpacing, children: tokenBuf })); + tokenBuf = []; + // ここから文字列エレメントになるので位置を更新 + elementLoc = this.stream.getPos(); + state = 'string'; + break; + } + const token = this.readToken(); + tokenBuf.push(token); + break; + } + } + } + + return TOKEN(TokenKind.Template, loc, { hasLeftSpacing, children: elements }); + } + + private skipCommentLine(): void { + while (true) { + if (this.stream.eof) { + break; + } + if (this.stream.char === '\n') { + this.stream.next(); + 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..3dae2a2d --- /dev/null +++ b/src/parser/streams/token-stream.ts @@ -0,0 +1,124 @@ +import { AiScriptSyntaxError } from '../../error.js'; +import { TOKEN, TokenKind } from '../token.js'; +import type { Token } from '../token.js'; + +/** + * トークンの読み取りに関するインターフェース +*/ +export interface ITokenStream { + /** + * カーソル位置にあるトークンを取得します。 + */ + get token(): Token; + + /** + * カーソル位置にあるトークンの種類を取得します。 + */ + get kind(): TokenKind; + + /** + * カーソル位置を次のトークンへ進めます。 + */ + 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 get kind(): TokenKind { + return this.token.kind; + } + + /** + * カーソル位置を次のトークンへ進めます。 + */ + 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.kind !== kind) { + throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[this.kind]}`); + } + } + + /** + * カーソル位置にあるトークンが指定したトークンの種類と一致することを確認し、 + * カーソル位置を次のトークンへ進めます。 + */ + 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..311b4f85 --- /dev/null +++ b/src/parser/syntaxes/common.ts @@ -0,0 +1,142 @@ +import { TokenKind } from '../token.js'; +import { AiScriptSyntaxError } from '../../error.js'; +import { NODE } from '../utils.js'; +import { parseStatement } from './statements.js'; + +import type { ITokenStream } from '../streams/token-stream.js'; +import type * as Ast from '../../node.js'; + +/** + * ```abnf + * Params = "(" [IDENT *(("," / SPACE) IDENT)] ")" + * ``` +*/ +export function parseParams(s: ITokenStream): { name: string, argType?: Ast.Node }[] { + const items: { name: string, argType?: Ast.Node }[] = []; + + s.nextWith(TokenKind.OpenParen); + + while (s.kind !== TokenKind.CloseParen) { + // separator + if (items.length > 0) { + if (s.kind === TokenKind.Comma) { + s.next(); + } else if (!s.token.hasLeftSpacing) { + throw new AiScriptSyntaxError('separator expected'); + } + } + + s.expect(TokenKind.Identifier); + const name = s.token.value!; + s.next(); + + let type; + if ((s.kind as TokenKind) === TokenKind.Colon) { + s.next(); + type = parseType(s); + } + + items.push({ name, argType: type }); + } + + s.nextWith(TokenKind.CloseParen); + + return items; +} + +/** + * ```abnf + * Block = "{" *Statement "}" + * ``` +*/ +export function parseBlock(s: ITokenStream): Ast.Node[] { + s.nextWith(TokenKind.OpenBrace); + + while (s.kind === TokenKind.NewLine) { + s.next(); + } + + const steps: Ast.Node[] = []; + while (s.kind !== TokenKind.CloseBrace) { + steps.push(parseStatement(s)); + + if ((s.kind as TokenKind) !== TokenKind.NewLine && (s.kind as TokenKind) !== TokenKind.CloseBrace) { + throw new AiScriptSyntaxError('Multiple statements cannot be placed on a single line.'); + } + while ((s.kind as TokenKind) === TokenKind.NewLine) { + s.next(); + } + } + + s.nextWith(TokenKind.CloseBrace); + + return steps; +} + +//#region Type + +export function parseType(s: ITokenStream): Ast.Node { + if (s.kind === TokenKind.At) { + return parseFnType(s); + } else { + return parseNamedType(s); + } +} + +/** + * ```abnf + * FnType = "@" "(" ParamTypes ")" "=>" Type + * ParamTypes = [Type *(("," / SPACE) Type)] + * ``` +*/ +function parseFnType(s: ITokenStream): Ast.Node { + const loc = s.token.loc; + + s.nextWith(TokenKind.At); + s.nextWith(TokenKind.OpenParen); + + const params: Ast.Node[] = []; + while (s.kind !== TokenKind.CloseParen) { + if (params.length > 0) { + if (s.kind === TokenKind.Comma) { + s.next(); + } else if (!s.token.hasLeftSpacing) { + throw new AiScriptSyntaxError('separator expected'); + } + } + 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 }, loc); +} + +/** + * ```abnf + * NamedType = IDENT ["<" Type ">"] + * ``` +*/ +function parseNamedType(s: ITokenStream): Ast.Node { + const loc = s.token.loc; + + s.expect(TokenKind.Identifier); + const name = s.token.value!; + s.next(); + + // inner type + let inner = null; + if (s.kind === TokenKind.Lt) { + s.next(); + inner = parseType(s); + s.nextWith(TokenKind.Gt); + } + + return NODE('namedTypeSource', { name, inner }, loc); +} + +//#endregion Type diff --git a/src/parser/syntaxes/expressions.ts b/src/parser/syntaxes/expressions.ts new file mode 100644 index 00000000..7aafacf7 --- /dev/null +++ b/src/parser/syntaxes/expressions.ts @@ -0,0 +1,623 @@ +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 loc = s.token.loc; + const op = s.kind; + s.next(); + + // 改行のエスケープ + if (s.kind === TokenKind.BackSlash) { + s.next(); + s.nextWith(TokenKind.NewLine); + } + + const expr = parsePratt(s, minBp); + + switch (op) { + case TokenKind.Plus: { + // 数値リテラル以外は非サポート + if (expr.type === 'num') { + return NODE('num', { value: expr.value }, loc); + } else { + throw new AiScriptSyntaxError('currently, sign is only supported for number literal.'); + } + // TODO: 将来的にサポートされる式を拡張 + // return NODE('plus', { expr }, loc); + } + case TokenKind.Minus: { + // 数値リテラル以外は非サポート + if (expr.type === 'num') { + return NODE('num', { value: -1 * expr.value }, loc); + } else { + throw new AiScriptSyntaxError('currently, sign is only supported for number literal.'); + } + // TODO: 将来的にサポートされる式を拡張 + // return NODE('minus', { expr }, loc); + } + case TokenKind.Not: { + return NODE('not', { expr }, loc); + } + default: { + throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[op]}`); + } + } +} + +function parseInfix(s: ITokenStream, left: Ast.Node, minBp: number): Ast.Node { + const loc = s.token.loc; + const op = s.kind; + s.next(); + + // 改行のエスケープ + if (s.kind === 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, + }, loc); + } else { + const right = parsePratt(s, minBp); + + switch (op) { + case TokenKind.Hat: { + return CALL_NODE('Core:pow', [left, right], loc); + } + case TokenKind.Asterisk: { + return CALL_NODE('Core:mul', [left, right], loc); + } + case TokenKind.Slash: { + return CALL_NODE('Core:div', [left, right], loc); + } + case TokenKind.Percent: { + return CALL_NODE('Core:mod', [left, right], loc); + } + case TokenKind.Plus: { + return CALL_NODE('Core:add', [left, right], loc); + } + case TokenKind.Minus: { + return CALL_NODE('Core:sub', [left, right], loc); + } + case TokenKind.Lt: { + return CALL_NODE('Core:lt', [left, right], loc); + } + case TokenKind.LtEq: { + return CALL_NODE('Core:lteq', [left, right], loc); + } + case TokenKind.Gt: { + return CALL_NODE('Core:gt', [left, right], loc); + } + case TokenKind.GtEq: { + return CALL_NODE('Core:gteq', [left, right], loc); + } + case TokenKind.Eq2: { + return CALL_NODE('Core:eq', [left, right], loc); + } + case TokenKind.NotEq: { + return CALL_NODE('Core:neq', [left, right], loc); + } + case TokenKind.And2: { + return NODE('and', { left, right }, loc); + } + case TokenKind.Or2: { + return NODE('or', { left, right }, loc); + } + default: { + throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[op]}`); + } + } + } +} + +function parsePostfix(s: ITokenStream, expr: Ast.Node): Ast.Node { + const loc = s.token.loc; + const op = s.kind; + + 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, + }, loc); + } + default: { + throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[op]}`); + } + } +} + +function parseAtom(s: ITokenStream, isStatic: boolean): Ast.Node { + const loc = s.token.loc; + + switch (s.kind) { + 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 element of s.token.children!) { + switch (element.kind) { + case TokenKind.TemplateStringElement: { + values.push(NODE('str', { value: element.value! }, element.loc)); + break; + } + case TokenKind.TemplateExprElement: { + // スキャナで埋め込み式として事前に読み取っておいたトークン列をパースする + const exprStream = new TokenStream(element.children!); + const expr = parseExpr(exprStream, false); + if (exprStream.kind !== TokenKind.EOF) { + throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[exprStream.token.kind]}`); + } + values.push(expr); + break; + } + default: { + throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[element.kind]}`); + } + } + } + + s.next(); + return NODE('tmpl', { tmpl: values }, loc); + } + case TokenKind.StringLiteral: { + const value = s.token.value!; + s.next(); + return NODE('str', { value }, loc); + } + case TokenKind.NumberLiteral: { + // TODO: validate number value + const value = Number(s.token.value!); + s.next(); + return NODE('num', { value }, loc); + } + case TokenKind.TrueKeyword: + case TokenKind.FalseKeyword: { + const value = (s.kind === TokenKind.TrueKeyword); + s.next(); + return NODE('bool', { value }, loc); + } + case TokenKind.NullKeyword: { + s.next(); + return NODE('null', { }, loc); + } + 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.kind]}`); +} + +/** + * Call = "(" [Expr *(("," / SPACE) Expr)] ")" +*/ +function parseCall(s: ITokenStream, target: Ast.Node): Ast.Node { + const loc = s.token.loc; + const items: Ast.Node[] = []; + + s.nextWith(TokenKind.OpenParen); + + while (s.kind !== TokenKind.CloseParen) { + // separator + if (items.length > 0) { + if (s.kind === TokenKind.Comma) { + s.next(); + } else if (!s.token.hasLeftSpacing) { + throw new AiScriptSyntaxError('separator expected'); + } + } + + items.push(parseExpr(s, false)); + } + + s.nextWith(TokenKind.CloseParen); + + return NODE('call', { + target, + args: items, + }, loc); +} + +/** + * ```abnf + * If = "if" Expr BlockOrStatement *("elif" Expr BlockOrStatement) ["else" BlockOrStatement] + * ``` +*/ +function parseIf(s: ITokenStream): Ast.Node { + const loc = s.token.loc; + + s.nextWith(TokenKind.IfKeyword); + const cond = parseExpr(s, false); + const then = parseBlockOrStatement(s); + + if (s.kind === TokenKind.NewLine && [TokenKind.ElifKeyword, TokenKind.ElseKeyword].includes(s.lookahead(1).kind)) { + s.next(); + } + + const elseif: { cond: Ast.Node, then: Ast.Node }[] = []; + while (s.kind === TokenKind.ElifKeyword) { + s.next(); + const elifCond = parseExpr(s, false); + const elifThen = parseBlockOrStatement(s); + if ((s.kind as TokenKind) === TokenKind.NewLine && [TokenKind.ElifKeyword, TokenKind.ElseKeyword].includes(s.lookahead(1).kind)) { + s.next(); + } + elseif.push({ cond: elifCond, then: elifThen }); + } + + let _else = undefined; + if (s.kind === TokenKind.ElseKeyword) { + s.next(); + _else = parseBlockOrStatement(s); + } + + return NODE('if', { cond, then, elseif, else: _else }, loc); +} + +/** + * ```abnf + * FnExpr = "@" Params [":" Type] Block + * ``` +*/ +function parseFnExpr(s: ITokenStream): Ast.Node { + const loc = s.token.loc; + + s.nextWith(TokenKind.At); + + const params = parseParams(s); + + let type; + if ((s.kind as TokenKind) === TokenKind.Colon) { + s.next(); + type = parseType(s); + } + + const body = parseBlock(s); + + return NODE('fn', { args: params, retType: type, children: body }, loc); +} + +/** + * ```abnf + * Match = "match" Expr "{" *("case" Expr "=>" BlockOrStatement) ["default" "=>" BlockOrStatement] "}" + * ``` +*/ +function parseMatch(s: ITokenStream): Ast.Node { + const loc = s.token.loc; + + s.nextWith(TokenKind.MatchKeyword); + const about = parseExpr(s, false); + + s.nextWith(TokenKind.OpenBrace); + s.nextWith(TokenKind.NewLine); + + const qs: { q: Ast.Node, a: Ast.Node }[] = []; + while (s.kind !== TokenKind.DefaultKeyword && s.kind !== TokenKind.CloseBrace) { + s.nextWith(TokenKind.CaseKeyword); + const q = parseExpr(s, false); + s.nextWith(TokenKind.Arrow); + const a = parseBlockOrStatement(s); + s.nextWith(TokenKind.NewLine); + qs.push({ q, a }); + } + + let x; + if (s.kind === TokenKind.DefaultKeyword) { + s.next(); + s.nextWith(TokenKind.Arrow); + x = parseBlockOrStatement(s); + s.nextWith(TokenKind.NewLine); + } + + s.nextWith(TokenKind.CloseBrace); + + return NODE('match', { about, qs, default: x }, loc); +} + +/** + * ```abnf + * Eval = "eval" Block + * ``` +*/ +function parseEval(s: ITokenStream): Ast.Node { + const loc = s.token.loc; + + s.nextWith(TokenKind.EvalKeyword); + const statements = parseBlock(s); + return NODE('block', { statements }, loc); +} + +/** + * ```abnf + * Exists = "exists" Reference + * ``` +*/ +function parseExists(s: ITokenStream): Ast.Node { + const loc = s.token.loc; + + s.nextWith(TokenKind.ExistsKeyword); + const identifier = parseReference(s); + return NODE('exists', { identifier }, loc); +} + +/** + * ```abnf + * Reference = IDENT *(":" IDENT) + * ``` +*/ +function parseReference(s: ITokenStream): Ast.Node { + const loc = s.token.loc; + + const segs: string[] = []; + while (true) { + if (segs.length > 0) { + if (s.kind === TokenKind.Colon) { + if (s.token.hasLeftSpacing) { + throw new AiScriptSyntaxError('Cannot use spaces in a reference.'); + } + s.next(); + if (s.token.hasLeftSpacing) { + throw new AiScriptSyntaxError('Cannot use spaces in a reference.'); + } + } else { + break; + } + } + s.expect(TokenKind.Identifier); + segs.push(s.token.value!); + s.next(); + } + return NODE('identifier', { name: segs.join(':') }, loc); +} + +/** + * ```abnf + * Object = "{" [IDENT ":" Expr *(("," / ";" / SPACE) IDENT ":" Expr) ["," / ";"]] "}" + * ``` +*/ +function parseObject(s: ITokenStream, isStatic: boolean): Ast.Node { + const loc = s.token.loc; + + s.nextWith(TokenKind.OpenBrace); + + if (s.kind === TokenKind.NewLine) { + s.next(); + } + + const map = new Map(); + while (s.kind !== 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 + if ((s.kind as TokenKind) === TokenKind.CloseBrace) { + break; + } else if (s.kind === TokenKind.Comma) { + s.next(); + } else if (s.kind === TokenKind.SemiColon) { + s.next(); + } else if (s.kind === TokenKind.NewLine) { + // noop + } else { + if (!s.token.hasLeftSpacing) { + throw new AiScriptSyntaxError('separator expected'); + } + } + + if (s.kind === TokenKind.NewLine) { + s.next(); + } + } + + s.nextWith(TokenKind.CloseBrace); + + return NODE('obj', { value: map }, loc); +} + +/** + * ```abnf + * Array = "[" [Expr *(("," / SPACE) Expr) [","]] "]" + * ``` +*/ +function parseArray(s: ITokenStream, isStatic: boolean): Ast.Node { + const loc = s.token.loc; + + s.nextWith(TokenKind.OpenBracket); + + if (s.kind === TokenKind.NewLine) { + s.next(); + } + + const value = []; + while (s.kind !== TokenKind.CloseBracket) { + value.push(parseExpr(s, isStatic)); + + // separator + if ((s.kind as TokenKind) === TokenKind.CloseBracket) { + break; + } else if (s.kind === TokenKind.Comma) { + s.next(); + } else if (s.kind === TokenKind.NewLine) { + // noop + } else { + if (!s.token.hasLeftSpacing) { + throw new AiScriptSyntaxError('separator expected'); + } + } + + if (s.kind === TokenKind.NewLine) { + s.next(); + } + } + + s.nextWith(TokenKind.CloseBracket); + + return NODE('arr', { value }, loc); +} + +//#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.kind; + 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.kind === TokenKind.BackSlash) { + s.next(); + s.nextWith(TokenKind.NewLine); + } + + const tokenKind = s.kind; + + 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..f36ba94d --- /dev/null +++ b/src/parser/syntaxes/statements.ts @@ -0,0 +1,408 @@ +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 loc = s.token.loc; + + switch (s.kind) { + 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.BreakKeyword: { + s.next(); + return NODE('break', {}, loc); + } + case TokenKind.ContinueKeyword: { + s.next(); + return NODE('continue', {}, loc); + } + } + 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.kind) { + case TokenKind.VarKeyword: + case TokenKind.LetKeyword: { + return parseVarDef(s); + } + case TokenKind.At: { + return parseFnDef(s); + } + default: { + throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[s.kind]}`); + } + } +} + +/** + * ```abnf + * BlockOrStatement = Block / Statement + * ``` +*/ +export function parseBlockOrStatement(s: ITokenStream): Ast.Node { + const loc = s.token.loc; + + if (s.kind === TokenKind.OpenBrace) { + const statements = parseBlock(s); + return NODE('block', { statements }, loc); + } else { + return parseStatement(s); + } +} + +/** + * ```abnf + * VarDef = ("let" / "var") IDENT [":" Type] "=" Expr + * ``` +*/ +function parseVarDef(s: ITokenStream): Ast.Node { + const loc = s.token.loc; + + let mut; + switch (s.kind) { + case TokenKind.LetKeyword: { + mut = false; + break; + } + case TokenKind.VarKeyword: { + mut = true; + break; + } + default: { + throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[s.kind]}`); + } + } + s.next(); + + s.expect(TokenKind.Identifier); + const name = s.token.value!; + s.next(); + + let type; + if ((s.kind as TokenKind) === TokenKind.Colon) { + s.next(); + type = parseType(s); + } + + s.nextWith(TokenKind.Eq); + + if ((s.kind as TokenKind) === TokenKind.NewLine) { + s.next(); + } + + const expr = parseExpr(s, false); + + return NODE('def', { name, varType: type, expr, mut, attr: [] }, loc); +} + +/** + * ```abnf + * FnDef = "@" IDENT Params [":" Type] Block + * ``` +*/ +function parseFnDef(s: ITokenStream): Ast.Node { + const loc = s.token.loc; + + s.nextWith(TokenKind.At); + + s.expect(TokenKind.Identifier); + const name = s.token.value; + s.next(); + + const params = parseParams(s); + + let type; + if ((s.kind as TokenKind) === TokenKind.Colon) { + s.next(); + type = parseType(s); + } + + const body = parseBlock(s); + + return NODE('def', { + name, + expr: NODE('fn', { + args: params, + retType: type, + children: body, + }, loc), + mut: false, + attr: [], + }, loc); +} + +/** + * ```abnf + * Out = "<:" Expr + * ``` +*/ +function parseOut(s: ITokenStream): Ast.Node { + const loc = s.token.loc; + + s.nextWith(TokenKind.Out); + const expr = parseExpr(s, false); + return CALL_NODE('print', [expr], loc); +} + +/** + * ```abnf + * Each = "each" "let" IDENT ("," / SPACE) Expr BlockOrStatement + * / "each" "(" "let" IDENT ("," / SPACE) Expr ")" BlockOrStatement + * ``` +*/ +function parseEach(s: ITokenStream): Ast.Node { + const loc = s.token.loc; + let hasParen = false; + + s.nextWith(TokenKind.EachKeyword); + + if (s.kind === TokenKind.OpenParen) { + hasParen = true; + s.next(); + } + + s.nextWith(TokenKind.LetKeyword); + + s.expect(TokenKind.Identifier); + const name = s.token.value!; + s.next(); + + if (s.kind === TokenKind.Comma) { + s.next(); + } else if (!s.token.hasLeftSpacing) { + throw new AiScriptSyntaxError('separator expected'); + } + + const items = parseExpr(s, false); + + if (hasParen) { + s.nextWith(TokenKind.CloseParen); + } + + const body = parseBlockOrStatement(s); + + return NODE('each', { + var: name, + items: items, + for: body, + }, loc); +} + +function parseFor(s: ITokenStream): Ast.Node { + const loc = s.token.loc; + let hasParen = false; + + s.nextWith(TokenKind.ForKeyword); + + if (s.kind === TokenKind.OpenParen) { + hasParen = true; + s.next(); + } + + if (s.kind === TokenKind.LetKeyword) { + // range syntax + s.next(); + + const identLoc = s.token.loc; + + s.expect(TokenKind.Identifier); + const name = s.token.value!; + s.next(); + + let _from; + if ((s.kind as TokenKind) === TokenKind.Eq) { + s.next(); + _from = parseExpr(s, false); + } else { + _from = NODE('num', { value: 0 }, identLoc); + } + + if ((s.kind as TokenKind) === TokenKind.Comma) { + s.next(); + } else if (!s.token.hasLeftSpacing) { + throw new AiScriptSyntaxError('separator expected'); + } + + 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, + }, loc); + } else { + // times syntax + + const times = parseExpr(s, false); + + if (hasParen) { + s.nextWith(TokenKind.CloseParen); + } + + const body = parseBlockOrStatement(s); + + return NODE('for', { + times, + for: body, + }, loc); + } +} + +/** + * ```abnf + * Return = "return" Expr + * ``` +*/ +function parseReturn(s: ITokenStream): Ast.Node { + const loc = s.token.loc; + + s.nextWith(TokenKind.ReturnKeyword); + const expr = parseExpr(s, false); + return NODE('return', { expr }, loc); +} + +/** + * ```abnf + * StatementWithAttr = *Attr Statement + * ``` +*/ +function parseStatementWithAttr(s: ITokenStream): Ast.Node { + const attrs: Ast.Attribute[] = []; + while (s.kind === 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.'); + } + 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 loc = s.token.loc; + + s.nextWith(TokenKind.OpenSharpBracket); + + s.expect(TokenKind.Identifier); + const name = s.token.value!; + s.next(); + + let value; + if (s.kind !== TokenKind.CloseBracket) { + value = parseExpr(s, true); + } else { + value = NODE('bool', { value: true }, loc); + } + + s.nextWith(TokenKind.CloseBracket); + + return NODE('attr', { name, value }, loc); +} + +/** + * ```abnf + * Loop = "loop" Block + * ``` +*/ +function parseLoop(s: ITokenStream): Ast.Node { + const loc = s.token.loc; + + s.nextWith(TokenKind.LoopKeyword); + const statements = parseBlock(s); + return NODE('loop', { statements }, loc); +} + +/** + * ```abnf + * Assign = Expr ("=" / "+=" / "-=") Expr + * ``` +*/ +function tryParseAssign(s: ITokenStream, dest: Ast.Node): Ast.Node | undefined { + const loc = s.token.loc; + + // Assign + switch (s.kind) { + case TokenKind.Eq: { + s.next(); + const expr = parseExpr(s, false); + return NODE('assign', { dest, expr }, loc); + } + case TokenKind.PlusEq: { + s.next(); + const expr = parseExpr(s, false); + return NODE('addAssign', { dest, expr }, loc); + } + case TokenKind.MinusEq: { + s.next(); + const expr = parseExpr(s, false); + return NODE('subAssign', { dest, expr }, loc); + } + default: { + return; + } + } +} diff --git a/src/parser/syntaxes/toplevel.ts b/src/parser/syntaxes/toplevel.ts new file mode 100644 index 00000000..cd6f8ba2 --- /dev/null +++ b/src/parser/syntaxes/toplevel.ts @@ -0,0 +1,115 @@ +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.kind === TokenKind.NewLine) { + s.next(); + } + + while (s.kind !== TokenKind.EOF) { + switch (s.kind) { + case TokenKind.Colon2: { + nodes.push(parseNamespace(s)); + break; + } + case TokenKind.Sharp3: { + nodes.push(parseMeta(s)); + break; + } + default: { + nodes.push(parseStatement(s)); + break; + } + } + + if ((s.kind as TokenKind) !== TokenKind.NewLine && (s.kind as TokenKind) !== TokenKind.EOF) { + throw new AiScriptSyntaxError('Multiple statements cannot be placed on a single line.'); + } + while ((s.kind as TokenKind) === TokenKind.NewLine) { + s.next(); + } + } + + return nodes; +} + +/** + * ```abnf + * Namespace = "::" IDENT "{" *(VarDef / FnDef / Namespace) "}" + * ``` +*/ +export function parseNamespace(s: ITokenStream): Ast.Node { + const loc = s.token.loc; + + 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.kind === TokenKind.NewLine) { + s.next(); + } + + while (s.kind !== TokenKind.CloseBrace) { + switch (s.kind) { + case TokenKind.VarKeyword: + case TokenKind.LetKeyword: + case TokenKind.At: { + members.push(parseDefStatement(s)); + break; + } + case TokenKind.Colon2: { + members.push(parseNamespace(s)); + break; + } + } + + if ((s.kind as TokenKind) !== TokenKind.NewLine && (s.kind as TokenKind) !== TokenKind.CloseBrace) { + throw new AiScriptSyntaxError('Multiple statements cannot be placed on a single line.'); + } + while ((s.kind as TokenKind) === TokenKind.NewLine) { + s.next(); + } + } + s.nextWith(TokenKind.CloseBrace); + + return NODE('ns', { name, members }, loc); +} + +/** + * ```abnf + * Meta = "###" [IDENT] StaticExpr + * ``` +*/ +export function parseMeta(s: ITokenStream): Ast.Node { + const loc = s.token.loc; + + s.nextWith(TokenKind.Sharp3); + + let name = null; + if (s.kind === TokenKind.Identifier) { + name = s.token.value!; + s.next(); + } + + const value = parseExpr(s, true); + + return NODE('meta', { name, value }, loc); +} diff --git a/src/parser/token.ts b/src/parser/token.ts new file mode 100644 index 00000000..67aca6b6 --- /dev/null +++ b/src/parser/token.ts @@ -0,0 +1,128 @@ +export enum TokenKind { + EOF, + NewLine, + Identifier, + + // literal + NumberLiteral, + StringLiteral, + + // template string + Template, + TemplateStringElement, + TemplateExprElement, + + // keyword + NullKeyword, + TrueKeyword, + FalseKeyword, + EachKeyword, + ForKeyword, + LoopKeyword, + 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, + /** "@" */ + At, + /** "[" */ + OpenBracket, + /** "\\" */ + BackSlash, + /** "]" */ + CloseBracket, + /** "^" */ + Hat, + /** "{" */ + OpenBrace, + /** "||" */ + Or2, + /** "}" */ + CloseBrace, +} + +export type TokenLocation = { column: number, line: number }; + +export class Token { + constructor( + public kind: TokenKind, + public loc: { column: number, line: number }, + 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, loc: TokenLocation, opts?: { hasLeftSpacing?: boolean, value?: Token['value'], children?: Token['children'] }): Token { + return new Token(kind, loc, opts?.hasLeftSpacing, opts?.value, opts?.children); +} diff --git a/src/parser/utils.ts b/src/parser/utils.ts new file mode 100644 index 00000000..456764e5 --- /dev/null +++ b/src/parser/utils.ts @@ -0,0 +1,19 @@ +import type * as Ast from '../node.js'; + +export function NODE(type: string, params: Record, loc: { column: number, line: number }): Ast.Node { + const node: Record = { type }; + for (const key of Object.keys(params)) { + if (params[key] !== undefined) { + node[key] = params[key]; + } + } + node.loc = loc; + return node as Ast.Node; +} + +export function CALL_NODE(name: string, args: Ast.Node[], loc: { column: number, line: number }): Ast.Node { + return NODE('call', { + target: NODE('identifier', { name }, loc), + args, + }, loc); +} diff --git a/src/parser/visit.ts b/src/parser/visit.ts index db617de2..29b5cbdf 100644 --- a/src/parser/visit.ts +++ b/src/parser/visit.ts @@ -1,143 +1,127 @@ -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 (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]; - } - 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]; + result.value[i] = visitNode(result.value[i]!, fn) as Ast.Arr['value'][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 +133,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/test/index.ts b/test/index.ts index b1b9aac9..48cb8a6b 100644 --- a/test/index.ts +++ b/test/index.ts @@ -16,9 +16,13 @@ const exe = (program: string): Promise => new Promise((ok, err) => { maxStep: 9999, }); - const parser = new Parser(); - const ast = parser.parse(program); - aiscript.exec(ast).catch(err); + try { + const parser = new Parser(); + const ast = parser.parse(program); + aiscript.exec(ast).catch(err); + } catch (e) { + err(e); + } }); const getMeta = (program: string) => { @@ -299,8 +303,8 @@ describe('Infix expression', () => { test.concurrent('syntax symbols vs infix operators', async () => { const res = await exe(` <: match true { - 1 == 1 => "true" - 1 < 1 => "false" + case 1 == 1 => "true" + case 1 < 1 => "false" } `); eq(res, STR('true')); @@ -313,8 +317,8 @@ describe('Infix expression', () => { test.concurrent('number + match expression', async () => { const res = await exe(` <: 1 + match 2 == 2 { - true => 3 - false => 4 + case true => 3 + case false => 4 } `); eq(res, NUM(4)); @@ -474,6 +478,20 @@ describe('Cannot put multiple statements in a line', () => { } 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(); + }); }); test.concurrent('empty function', async () => { @@ -1494,9 +1512,9 @@ describe('match', () => { test.concurrent('Basic', async () => { const res = await exe(` <: match 2 { - 1 => "a" - 2 => "b" - 3 => "c" + case 1 => "a" + case 2 => "b" + case 3 => "c" } `); eq(res, STR('b')); @@ -1505,9 +1523,9 @@ describe('match', () => { test.concurrent('When default not provided, returns null', async () => { const res = await exe(` <: match 42 { - 1 => "a" - 2 => "b" - 3 => "c" + case 1 => "a" + case 2 => "b" + case 3 => "c" } `); eq(res, NULL); @@ -1516,10 +1534,10 @@ describe('match', () => { test.concurrent('With default', async () => { const res = await exe(` <: match 42 { - 1 => "a" - 2 => "b" - 3 => "c" - * => "d" + case 1 => "a" + case 2 => "b" + case 3 => "c" + default => "d" } `); eq(res, STR('d')); @@ -1528,13 +1546,13 @@ describe('match', () => { test.concurrent('With block', async () => { const res = await exe(` <: match 2 { - 1 => 1 - 2 => { + case 1 => 1 + case 2 => { let a = 1 let b = 2 (a + b) } - 3 => 3 + case 3 => 3 } `); eq(res, NUM(3)); @@ -1544,7 +1562,7 @@ describe('match', () => { const res = await exe(` @f(x) { match x { - 1 => { + case 1 => { return "ai" } } @@ -2266,12 +2284,12 @@ describe('Location', () => { let node: Ast.Node; const parser = new Parser(); const nodes = parser.parse(` - @f(a) { a } + @f(a) { a } `); assert.equal(nodes.length, 1); node = nodes[0]; if (!node.loc) assert.fail(); - assert.deepEqual(node.loc, { start: 3, end: 13 }); + assert.deepEqual(node.loc, { line: 2, column: 4 }); }); }); diff --git a/test/parser.ts b/test/parser.ts new file mode 100644 index 00000000..893fff91 --- /dev/null +++ b/test/parser.ts @@ -0,0 +1,146 @@ +import * as assert from 'assert'; +import { Scanner } from '../src/parser/scanner'; +import { TOKEN, TokenKind, TokenLocation } 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, loc: TokenLocation, opts: { hasLeftSpacing?: boolean, value?: string }) { + assert.deepStrictEqual(stream.token, TOKEN(kind, loc, 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 }, { }); + }); +}); From 86eb9deeabf60931fb1f4169af5af890dcc1d11b Mon Sep 17 00:00:00 2001 From: marihachi Date: Fri, 20 Oct 2023 10:42:05 +0900 Subject: [PATCH 02/62] =?UTF-8?q?=E6=96=87=E6=B3=95=E3=82=A8=E3=83=A9?= =?UTF-8?q?=E3=83=BC=E3=81=AB=E4=BD=8D=E7=BD=AE=E6=83=85=E5=A0=B1=E3=82=92?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0=20(#413)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * error location for parser * type error location * fix error message --- etc/aiscript.api.md | 8 ++++++-- src/error.ts | 10 ++++++---- src/parser/plugins/validate-keyword.ts | 10 +++++----- src/parser/scanner.ts | 18 +++++++++--------- src/parser/streams/token-stream.ts | 2 +- src/parser/syntaxes/common.ts | 6 +++--- src/parser/syntaxes/expressions.ts | 26 +++++++++++++------------- src/parser/syntaxes/statements.ts | 10 +++++----- src/parser/syntaxes/toplevel.ts | 4 ++-- src/type.ts | 2 +- 10 files changed, 51 insertions(+), 45 deletions(-) diff --git a/etc/aiscript.api.md b/etc/aiscript.api.md index 82532fc2..92ec4507 100644 --- a/etc/aiscript.api.md +++ b/etc/aiscript.api.md @@ -36,14 +36,18 @@ class AiScriptRuntimeError extends AiScriptError { // @public class AiScriptSyntaxError extends AiScriptError { - constructor(message: string, info?: any); + constructor(message: string, loc: Loc, info?: any); + // (undocumented) + loc: Loc; // (undocumented) name: string; } // @public class AiScriptTypeError extends AiScriptError { - constructor(message: string, info?: any); + constructor(message: string, loc: Loc, info?: any); + // (undocumented) + loc: Loc; // (undocumented) name: string; } diff --git a/src/error.ts b/src/error.ts index 0a7a0f35..0c7a6b44 100644 --- a/src/error.ts +++ b/src/error.ts @@ -1,3 +1,5 @@ +import type { Loc } from './node.js'; + export abstract class AiScriptError extends Error { // name is read by Error.prototype.toString public name = 'AiScript'; @@ -30,8 +32,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 loc: Loc, info?: any) { + super(`${message} (Line ${loc.line}, Column ${loc.column})`, info); } } /** @@ -39,8 +41,8 @@ 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 loc: Loc, info?: any) { + super(`${message} (Line ${loc.line}, Column ${loc.column})`, info); } } diff --git a/src/parser/plugins/validate-keyword.ts b/src/parser/plugins/validate-keyword.ts index 1d7d5b10..6c1fdac1 100644 --- a/src/parser/plugins/validate-keyword.ts +++ b/src/parser/plugins/validate-keyword.ts @@ -42,8 +42,8 @@ const reservedWord = [ // 'out', ]; -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); } function validateNode(node: Ast.Node): Ast.Node { @@ -53,20 +53,20 @@ function validateNode(node: Ast.Node): Ast.Node { case 'ns': case 'identifier': { 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 'fn': { for (const arg of node.args) { if (reservedWord.includes(arg.name)) { - throwReservedWordError(arg.name); + throwReservedWordError(arg.name, node.loc); } } break; diff --git a/src/parser/scanner.ts b/src/parser/scanner.ts index 67a5d93e..3bd61a82 100644 --- a/src/parser/scanner.ts +++ b/src/parser/scanner.ts @@ -75,7 +75,7 @@ export class Scanner implements ITokenStream { */ public expect(kind: TokenKind): void { if (this.kind !== kind) { - throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[this.kind]}`); + throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[this.kind]}`, this.token.loc); } } @@ -140,7 +140,7 @@ export class Scanner implements ITokenStream { this.stream.next(); token = TOKEN(TokenKind.OpenSharpBracket, loc, { hasLeftSpacing }); } else { - throw new AiScriptSyntaxError('invalid character: "#"'); + throw new AiScriptSyntaxError('invalid character: "#"', loc); } break; } @@ -327,7 +327,7 @@ export class Scanner implements ITokenStream { token = wordToken; break; } - throw new AiScriptSyntaxError(`invalid character: "${this.stream.char}"`); + throw new AiScriptSyntaxError(`invalid character: "${this.stream.char}"`, loc); } break; } @@ -432,7 +432,7 @@ export class Scanner implements ITokenStream { this.stream.next(); } if (fractional.length === 0) { - throw new AiScriptSyntaxError('digit expected'); + throw new AiScriptSyntaxError('digit expected', loc); } } let value; @@ -456,7 +456,7 @@ export class Scanner implements ITokenStream { switch (state) { case 'string': { if (this.stream.eof) { - throw new AiScriptSyntaxError('unexpected EOF'); + throw new AiScriptSyntaxError('unexpected EOF', loc); } if (this.stream.char === '\\') { this.stream.next(); @@ -474,7 +474,7 @@ export class Scanner implements ITokenStream { } case 'escape': { if (this.stream.eof) { - throw new AiScriptSyntaxError('unexpected EOF'); + throw new AiScriptSyntaxError('unexpected EOF', loc); } value += this.stream.char; this.stream.next(); @@ -501,7 +501,7 @@ export class Scanner implements ITokenStream { case 'string': { // テンプレートの終了が無いままEOFに達した if (this.stream.eof) { - throw new AiScriptSyntaxError('unexpected EOF'); + throw new AiScriptSyntaxError('unexpected EOF', loc); } // エスケープ if (this.stream.char === '\\') { @@ -537,7 +537,7 @@ export class Scanner implements ITokenStream { case 'escape': { // エスケープ対象の文字が無いままEOFに達した if (this.stream.eof) { - throw new AiScriptSyntaxError('unexpected EOF'); + throw new AiScriptSyntaxError('unexpected EOF', loc); } // 普通の文字として取り込み buf += this.stream.char; @@ -549,7 +549,7 @@ export class Scanner implements ITokenStream { case 'expr': { // 埋め込み式の終端記号が無いままEOFに達した if (this.stream.eof) { - throw new AiScriptSyntaxError('unexpected EOF'); + throw new AiScriptSyntaxError('unexpected EOF', loc); } // skip spasing if (spaceChars.includes(this.stream.char)) { diff --git a/src/parser/streams/token-stream.ts b/src/parser/streams/token-stream.ts index 3dae2a2d..c0fadcaa 100644 --- a/src/parser/streams/token-stream.ts +++ b/src/parser/streams/token-stream.ts @@ -101,7 +101,7 @@ export class TokenStream implements ITokenStream { */ public expect(kind: TokenKind): void { if (this.kind !== kind) { - throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[this.kind]}`); + throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[this.kind]}`, this.token.loc); } } diff --git a/src/parser/syntaxes/common.ts b/src/parser/syntaxes/common.ts index 311b4f85..6c200ad5 100644 --- a/src/parser/syntaxes/common.ts +++ b/src/parser/syntaxes/common.ts @@ -22,7 +22,7 @@ export function parseParams(s: ITokenStream): { name: string, argType?: Ast.Node if (s.kind === TokenKind.Comma) { s.next(); } else if (!s.token.hasLeftSpacing) { - throw new AiScriptSyntaxError('separator expected'); + throw new AiScriptSyntaxError('separator expected', s.token.loc); } } @@ -61,7 +61,7 @@ export function parseBlock(s: ITokenStream): Ast.Node[] { steps.push(parseStatement(s)); if ((s.kind as TokenKind) !== TokenKind.NewLine && (s.kind as TokenKind) !== TokenKind.CloseBrace) { - throw new AiScriptSyntaxError('Multiple statements cannot be placed on a single line.'); + throw new AiScriptSyntaxError('Multiple statements cannot be placed on a single line.', s.token.loc); } while ((s.kind as TokenKind) === TokenKind.NewLine) { s.next(); @@ -101,7 +101,7 @@ function parseFnType(s: ITokenStream): Ast.Node { if (s.kind === TokenKind.Comma) { s.next(); } else if (!s.token.hasLeftSpacing) { - throw new AiScriptSyntaxError('separator expected'); + throw new AiScriptSyntaxError('separator expected', s.token.loc); } } const type = parseType(s); diff --git a/src/parser/syntaxes/expressions.ts b/src/parser/syntaxes/expressions.ts index 7aafacf7..eecbc66f 100644 --- a/src/parser/syntaxes/expressions.ts +++ b/src/parser/syntaxes/expressions.ts @@ -70,7 +70,7 @@ function parsePrefix(s: ITokenStream, minBp: number): Ast.Node { if (expr.type === 'num') { return NODE('num', { value: expr.value }, loc); } else { - throw new AiScriptSyntaxError('currently, sign is only supported for number literal.'); + throw new AiScriptSyntaxError('currently, sign is only supported for number literal.', loc); } // TODO: 将来的にサポートされる式を拡張 // return NODE('plus', { expr }, loc); @@ -80,7 +80,7 @@ function parsePrefix(s: ITokenStream, minBp: number): Ast.Node { if (expr.type === 'num') { return NODE('num', { value: -1 * expr.value }, loc); } else { - throw new AiScriptSyntaxError('currently, sign is only supported for number literal.'); + throw new AiScriptSyntaxError('currently, sign is only supported for number literal.', loc); } // TODO: 将来的にサポートされる式を拡張 // return NODE('minus', { expr }, loc); @@ -89,7 +89,7 @@ function parsePrefix(s: ITokenStream, minBp: number): Ast.Node { return NODE('not', { expr }, loc); } default: { - throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[op]}`); + throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[op]}`, loc); } } } @@ -161,7 +161,7 @@ function parseInfix(s: ITokenStream, left: Ast.Node, minBp: number): Ast.Node { return NODE('or', { left, right }, loc); } default: { - throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[op]}`); + throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[op]}`, loc); } } } @@ -186,7 +186,7 @@ function parsePostfix(s: ITokenStream, expr: Ast.Node): Ast.Node { }, loc); } default: { - throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[op]}`); + throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[op]}`, loc); } } } @@ -231,13 +231,13 @@ function parseAtom(s: ITokenStream, isStatic: boolean): Ast.Node { const exprStream = new TokenStream(element.children!); const expr = parseExpr(exprStream, false); if (exprStream.kind !== TokenKind.EOF) { - throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[exprStream.token.kind]}`); + throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[exprStream.token.kind]}`, exprStream.token.loc); } values.push(expr); break; } default: { - throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[element.kind]}`); + throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[element.kind]}`, element.loc); } } } @@ -283,7 +283,7 @@ function parseAtom(s: ITokenStream, isStatic: boolean): Ast.Node { return expr; } } - throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[s.kind]}`); + throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[s.kind]}`, loc); } /** @@ -301,7 +301,7 @@ function parseCall(s: ITokenStream, target: Ast.Node): Ast.Node { if (s.kind === TokenKind.Comma) { s.next(); } else if (!s.token.hasLeftSpacing) { - throw new AiScriptSyntaxError('separator expected'); + throw new AiScriptSyntaxError('separator expected', s.token.loc); } } @@ -451,11 +451,11 @@ function parseReference(s: ITokenStream): Ast.Node { if (segs.length > 0) { if (s.kind === TokenKind.Colon) { if (s.token.hasLeftSpacing) { - throw new AiScriptSyntaxError('Cannot use spaces in a reference.'); + throw new AiScriptSyntaxError('Cannot use spaces in a reference.', s.token.loc); } s.next(); if (s.token.hasLeftSpacing) { - throw new AiScriptSyntaxError('Cannot use spaces in a reference.'); + throw new AiScriptSyntaxError('Cannot use spaces in a reference.', s.token.loc); } } else { break; @@ -505,7 +505,7 @@ function parseObject(s: ITokenStream, isStatic: boolean): Ast.Node { // noop } else { if (!s.token.hasLeftSpacing) { - throw new AiScriptSyntaxError('separator expected'); + throw new AiScriptSyntaxError('separator expected', s.token.loc); } } @@ -546,7 +546,7 @@ function parseArray(s: ITokenStream, isStatic: boolean): Ast.Node { // noop } else { if (!s.token.hasLeftSpacing) { - throw new AiScriptSyntaxError('separator expected'); + throw new AiScriptSyntaxError('separator expected', s.token.loc); } } diff --git a/src/parser/syntaxes/statements.ts b/src/parser/syntaxes/statements.ts index f36ba94d..c5655175 100644 --- a/src/parser/syntaxes/statements.ts +++ b/src/parser/syntaxes/statements.ts @@ -72,7 +72,7 @@ export function parseDefStatement(s: ITokenStream): Ast.Node { return parseFnDef(s); } default: { - throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[s.kind]}`); + throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[s.kind]}`, s.token.loc); } } } @@ -112,7 +112,7 @@ function parseVarDef(s: ITokenStream): Ast.Node { break; } default: { - throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[s.kind]}`); + throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[s.kind]}`, s.token.loc); } } s.next(); @@ -213,7 +213,7 @@ function parseEach(s: ITokenStream): Ast.Node { if (s.kind === TokenKind.Comma) { s.next(); } else if (!s.token.hasLeftSpacing) { - throw new AiScriptSyntaxError('separator expected'); + throw new AiScriptSyntaxError('separator expected', s.token.loc); } const items = parseExpr(s, false); @@ -263,7 +263,7 @@ function parseFor(s: ITokenStream): Ast.Node { if ((s.kind as TokenKind) === TokenKind.Comma) { s.next(); } else if (!s.token.hasLeftSpacing) { - throw new AiScriptSyntaxError('separator expected'); + throw new AiScriptSyntaxError('separator expected', s.token.loc); } const to = parseExpr(s, false); @@ -326,7 +326,7 @@ function parseStatementWithAttr(s: ITokenStream): Ast.Node { const statement = parseStatement(s); if (statement.type !== 'def') { - throw new AiScriptSyntaxError('invalid attribute.'); + throw new AiScriptSyntaxError('invalid attribute.', statement.loc); } if (statement.attr != null) { statement.attr.push(...attrs); diff --git a/src/parser/syntaxes/toplevel.ts b/src/parser/syntaxes/toplevel.ts index cd6f8ba2..a84e60de 100644 --- a/src/parser/syntaxes/toplevel.ts +++ b/src/parser/syntaxes/toplevel.ts @@ -36,7 +36,7 @@ export function parseTopLevel(s: ITokenStream): Ast.Node[] { } if ((s.kind as TokenKind) !== TokenKind.NewLine && (s.kind as TokenKind) !== TokenKind.EOF) { - throw new AiScriptSyntaxError('Multiple statements cannot be placed on a single line.'); + throw new AiScriptSyntaxError('Multiple statements cannot be placed on a single line.', s.token.loc); } while ((s.kind as TokenKind) === TokenKind.NewLine) { s.next(); @@ -82,7 +82,7 @@ export function parseNamespace(s: ITokenStream): Ast.Node { } if ((s.kind as TokenKind) !== TokenKind.NewLine && (s.kind as TokenKind) !== TokenKind.CloseBrace) { - throw new AiScriptSyntaxError('Multiple statements cannot be placed on a single line.'); + throw new AiScriptSyntaxError('Multiple statements cannot be placed on a single line.', s.token.loc); } while ((s.kind as TokenKind) === TokenKind.NewLine) { s.next(); diff --git a/src/type.ts b/src/type.ts index 5e93acf0..54cd9f0f 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); } else { const argTypes = typeSource.args.map(arg => getTypeBySource(arg)); return T_FN(argTypes, getTypeBySource(typeSource.result)); From 9eb8929ee2e2d45e022b2414a19750fcc29245d4 Mon Sep 17 00:00:00 2001 From: marihachi Date: Fri, 20 Oct 2023 10:42:45 +0900 Subject: [PATCH 03/62] fix unexpected eof (#414) --- src/parser/scanner.ts | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/parser/scanner.ts b/src/parser/scanner.ts index 3bd61a82..21d42f28 100644 --- a/src/parser/scanner.ts +++ b/src/parser/scanner.ts @@ -115,7 +115,7 @@ export class Scanner implements ITokenStream { switch (this.stream.char) { case '!': { this.stream.next(); - if ((this.stream.char as string) === '=') { + if (!this.stream.eof && (this.stream.char as string) === '=') { this.stream.next(); token = TOKEN(TokenKind.NotEq, loc, { hasLeftSpacing }); } else { @@ -130,13 +130,13 @@ export class Scanner implements ITokenStream { } case '#': { this.stream.next(); - if ((this.stream.char as string) === '#') { + if (!this.stream.eof && (this.stream.char as string) === '#') { this.stream.next(); - if ((this.stream.char as string) === '#') { + if (!this.stream.eof && (this.stream.char as string) === '#') { this.stream.next(); token = TOKEN(TokenKind.Sharp3, loc, { hasLeftSpacing }); } - } else if ((this.stream.char as string) === '[') { + } else if (!this.stream.eof && (this.stream.char as string) === '[') { this.stream.next(); token = TOKEN(TokenKind.OpenSharpBracket, loc, { hasLeftSpacing }); } else { @@ -151,7 +151,7 @@ export class Scanner implements ITokenStream { } case '&': { this.stream.next(); - if ((this.stream.char as string) === '&') { + if (!this.stream.eof && (this.stream.char as string) === '&') { this.stream.next(); token = TOKEN(TokenKind.And2, loc, { hasLeftSpacing }); } @@ -174,7 +174,7 @@ export class Scanner implements ITokenStream { } case '+': { this.stream.next(); - if ((this.stream.char as string) === '=') { + if (!this.stream.eof && (this.stream.char as string) === '=') { this.stream.next(); token = TOKEN(TokenKind.PlusEq, loc, { hasLeftSpacing }); } else { @@ -189,7 +189,7 @@ export class Scanner implements ITokenStream { } case '-': { this.stream.next(); - if ((this.stream.char as string) === '=') { + if (!this.stream.eof && (this.stream.char as string) === '=') { this.stream.next(); token = TOKEN(TokenKind.MinusEq, loc, { hasLeftSpacing }); } else { @@ -204,11 +204,11 @@ export class Scanner implements ITokenStream { } case '/': { this.stream.next(); - if ((this.stream.char as string) === '*') { + if (!this.stream.eof && (this.stream.char as string) === '*') { this.stream.next(); this.skipCommentRange(); continue; - } else if ((this.stream.char as string) === '/') { + } else if (!this.stream.eof && (this.stream.char as string) === '/') { this.stream.next(); this.skipCommentLine(); continue; @@ -219,7 +219,7 @@ export class Scanner implements ITokenStream { } case ':': { this.stream.next(); - if ((this.stream.char as string) === ':') { + if (!this.stream.eof && (this.stream.char as string) === ':') { this.stream.next(); token = TOKEN(TokenKind.Colon2, loc, { hasLeftSpacing }); } else { @@ -234,10 +234,10 @@ export class Scanner implements ITokenStream { } case '<': { this.stream.next(); - if ((this.stream.char as string) === '=') { + if (!this.stream.eof && (this.stream.char as string) === '=') { this.stream.next(); token = TOKEN(TokenKind.LtEq, loc, { hasLeftSpacing }); - } else if ((this.stream.char as string) === ':') { + } else if (!this.stream.eof && (this.stream.char as string) === ':') { this.stream.next(); token = TOKEN(TokenKind.Out, loc, { hasLeftSpacing }); } else { @@ -247,10 +247,10 @@ export class Scanner implements ITokenStream { } case '=': { this.stream.next(); - if ((this.stream.char as string) === '=') { + if (!this.stream.eof && (this.stream.char as string) === '=') { this.stream.next(); token = TOKEN(TokenKind.Eq2, loc, { hasLeftSpacing }); - } else if ((this.stream.char as string) === '>') { + } else if (!this.stream.eof && (this.stream.char as string) === '>') { this.stream.next(); token = TOKEN(TokenKind.Arrow, loc, { hasLeftSpacing }); } else { @@ -260,7 +260,7 @@ export class Scanner implements ITokenStream { } case '>': { this.stream.next(); - if ((this.stream.char as string) === '=') { + if (!this.stream.eof && (this.stream.char as string) === '=') { this.stream.next(); token = TOKEN(TokenKind.GtEq, loc, { hasLeftSpacing }); } else { @@ -304,7 +304,7 @@ export class Scanner implements ITokenStream { } case '|': { this.stream.next(); - if ((this.stream.char as string) === '|') { + if (!this.stream.eof && (this.stream.char as string) === '|') { this.stream.next(); token = TOKEN(TokenKind.Or2, loc, { hasLeftSpacing }); } From aaf9538ce233c9d3110ef878e38f3cdc7fc0e687 Mon Sep 17 00:00:00 2001 From: marihachi Date: Wed, 25 Oct 2023 02:45:34 +0900 Subject: [PATCH 04/62] Add test from #441 --- test/index.ts | 73 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/test/index.ts b/test/index.ts index 48cb8a6b..100f2ce8 100644 --- a/test/index.ts +++ b/test/index.ts @@ -1112,6 +1112,79 @@ describe('chain', () => { `); eq(res, STR('kawaii')); }); + + test.concurrent('property chain with parenthesis', async () => { + let ast = Parser.parse(` + (a.b).c + `); + const line = ast[0]; + if ( + line.type !== 'prop' || + line.target.type !== 'prop' || + line.target.target.type !== 'identifier' + ) + assert.fail(); + assert.equal(line.target.target.name, 'a'); + assert.equal(line.target.name, 'b'); + assert.equal(line.name, 'c'); + }); + + test.concurrent('index chain with parenthesis', async () => { + let ast = Parser.parse(` + (a[42]).b + `); + const line = ast[0]; + if ( + line.type !== 'prop' || + line.target.type !== 'index' || + line.target.target.type !== 'identifier' || + line.target.index.type !== 'num' + ) + assert.fail(); + assert.equal(line.target.target.name, 'a'); + assert.equal(line.target.index.value, 42); + assert.equal(line.name, 'b'); + }); + + test.concurrent('call chain with parenthesis', async () => { + let ast = Parser.parse(` + (foo(42, 57)).bar + `); + const line = ast[0]; + if ( + line.type !== 'prop' || + line.target.type !== 'call' || + line.target.target.type !== 'identifier' || + line.target.args.length !== 2 || + line.target.args[0].type !== 'num' || + line.target.args[1].type !== 'num' + ) + assert.fail(); + assert.equal(line.target.target.name, 'foo'); + assert.equal(line.target.args[0].value, 42); + assert.equal(line.target.args[1].value, 57); + assert.equal(line.name, 'bar'); + }); + + test.concurrent('longer chain with parenthesis', async () => { + let ast = Parser.parse(` + (a.b.c).d.e + `); + const line = ast[0]; + if ( + line.type !== 'prop' || + line.target.type !== 'prop' || + line.target.target.type !== 'prop' || + line.target.target.target.type !== 'prop' || + line.target.target.target.target.type !== 'identifier' + ) + assert.fail(); + assert.equal(line.target.target.target.target.name, 'a'); + assert.equal(line.target.target.target.name, 'b'); + assert.equal(line.target.target.name, 'c'); + assert.equal(line.target.name, 'd'); + assert.equal(line.name, 'e'); + }); }); describe('Template syntax', () => { From 5966ceec4a6d33438676a62eae175b03ee7f6cd7 Mon Sep 17 00:00:00 2001 From: marihachi Date: Wed, 25 Oct 2023 02:48:40 +0900 Subject: [PATCH 05/62] =?UTF-8?q?=E3=83=91=E3=83=BC=E3=82=B5=E3=83=BC?= =?UTF-8?q?=E3=81=AE=E3=83=89=E3=82=AD=E3=83=A5=E3=83=A1=E3=83=B3=E3=83=88?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0=20(#439)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add parser docs * fix doc --- docs/parser/overview.md | 16 ++++++++++++++++ docs/parser/scanner.md | 5 ++++- docs/parser/token-streams.md | 11 +++++++++++ 3 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 docs/parser/overview.md create mode 100644 docs/parser/token-streams.md 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 index 6e78da1b..3f226e03 100644 --- a/docs/parser/scanner.md +++ b/docs/parser/scanner.md @@ -1,5 +1,4 @@ # Scanner 設計資料 -作成者: marihachi ## 現在のトークンと先読みされたトークン _tokensの0番には現在のトークンが保持される。また、トークンが先読みされた場合は1番以降にそれらのトークンが保持されていくことになる。 @@ -7,3 +6,7 @@ _tokensの0番には現在のトークンが保持される。また、トーク 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としてパース関数に渡される。 From dfe67fd927dfc3623636cd70805a8f390e8a0922 Mon Sep 17 00:00:00 2001 From: FineArchs <133759614+FineArchs@users.noreply.github.com> Date: Wed, 1 Nov 2023 20:02:28 +0900 Subject: [PATCH 06/62] =?UTF-8?q?=E3=83=A9=E3=83=B3=E3=82=BF=E3=82=A4?= =?UTF-8?q?=E3=83=A0=E3=82=A8=E3=83=A9=E3=83=BC=E3=81=AE=E4=BD=8D=E7=BD=AE?= =?UTF-8?q?=E3=82=92=E8=A1=A8=E7=A4=BA=20(#415)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add location to runtime errors * fix Non-aiscript error loc & add test * api --------- Co-authored-by: marihachi --- etc/aiscript.api.md | 38 ++++++++++--- src/error.ts | 11 ++++ src/interpreter/index.ts | 32 +++++++---- src/interpreter/value.ts | 22 +++++--- test/index.ts | 62 --------------------- test/interpreter.ts | 115 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 190 insertions(+), 90 deletions(-) create mode 100644 test/interpreter.ts diff --git a/etc/aiscript.api.md b/etc/aiscript.api.md index 92ec4507..d89418c6 100644 --- a/etc/aiscript.api.md +++ b/etc/aiscript.api.md @@ -19,6 +19,8 @@ abstract class AiScriptError extends Error { // (undocumented) info?: any; // (undocumented) + loc?: Loc; + // (undocumented) name: string; } @@ -27,6 +29,15 @@ class AiScriptIndexOutOfRangeError extends AiScriptRuntimeError { constructor(message: string, info?: any); } +// @public +class AiScriptNamespaceError extends AiScriptError { + constructor(message: string, loc: Loc, info?: any); + // (undocumented) + loc: Loc; + // (undocumented) + name: string; +} + // @public class AiScriptRuntimeError extends AiScriptError { constructor(message: string, info?: any); @@ -223,6 +234,7 @@ declare namespace errors { NonAiScriptError, AiScriptSyntaxError, AiScriptTypeError, + AiScriptNamespaceError, AiScriptRuntimeError, AiScriptIndexOutOfRangeError } @@ -248,7 +260,7 @@ 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 & { @@ -262,7 +274,7 @@ type Fn = NodeBase & { }; // @public (undocumented) -const FN_NATIVE: (fn: VFn['native']) => VFn; +const FN_NATIVE: (fn: VNativeFn['native']) => VNativeFn; // @public (undocumented) type FnTypeSource = NodeBase & { @@ -595,6 +607,8 @@ declare namespace values { VArr, VObj, VFn, + VUserFn, + VNativeFn, VReturn, VBreak, VContinue, @@ -651,18 +665,17 @@ type VError = { info?: Value; }; +// @public (undocumented) +type VFn = VUserFn | VNativeFn; + // @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) @@ -694,6 +707,15 @@ 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; + statements: Node_2[]; + scope: Scope; +}; + // (No @packageDocumentation comment for this package) ``` diff --git a/src/error.ts b/src/error.ts index 0c7a6b44..da6e68af 100644 --- a/src/error.ts +++ b/src/error.ts @@ -4,6 +4,7 @@ export abstract class AiScriptError extends Error { // name is read by Error.prototype.toString public name = 'AiScript'; public info?: any; + public loc?: Loc; constructor(message: string, info?: any) { super(message); @@ -46,6 +47,16 @@ export class AiScriptTypeError extends AiScriptError { } } +/** + * Namespace collection errors. + */ +export class AiScriptNamespaceError extends AiScriptError { + public name = 'Namespace'; + constructor(message: string, public loc: Loc, info?: any) { + super(`${message} (Line ${loc.line}, Column ${loc.column})`, info); + } +} + /** * Interpret-time errors. */ diff --git a/src/interpreter/index.ts b/src/interpreter/index.ts index 6b5524ae..3b5a5538 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'; @@ -83,10 +83,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 { @@ -101,10 +97,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 { @@ -198,7 +190,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); } 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); } } } @@ -247,7 +242,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.loc) throw e; + else { + const e2 = (e instanceof AiScriptError) ? e : new NonAiScriptError(e); + e2.loc = node.loc; + e2.message = `${e2.message} (Line ${node.loc.line}, Column ${node.loc.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++; diff --git a/src/interpreter/value.ts b/src/interpreter/value.ts index 0933bc18..90c531b0 100644 --- a/src/interpreter/value.ts +++ b/src/interpreter/value.ts @@ -30,20 +30,26 @@ export type VObj = { value: Map; }; +export type VFn = VUserFn | VNativeFn; +type VFnBase = { + type: 'fn'; + args?: string[]; +}; +export type VUserFn = VFnBase & { + native?: undefined; // if (vfn.native) で型アサーション出来るように + statements: Node[]; + scope: Scope; +}; /** * 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 +121,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/test/index.ts b/test/index.ts index 3f0d634b..7f766525 100644 --- a/test/index.ts +++ b/test/index.ts @@ -50,68 +50,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)); diff --git a/test/interpreter.ts b/test/interpreter.ts new file mode 100644 index 00000000..f9953705 --- /dev/null +++ b/test/interpreter.ts @@ -0,0 +1,115 @@ +import * as assert from 'assert'; +import { expect, test } from '@jest/globals'; +import { Parser, Interpreter, values, errors, utils } 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 exeAndGetErrLoc = (src: string): Promise => new Promise((ok, ng) => { + const aiscript = new Interpreter({ + emitError: FN_NATIVE((_args, _opts) => { + throw Error('emitError'); + }), + }, { + err(e) { ok(e.loc) }, + }); + aiscript.exec(Parser.parse(src)).then(() => ng('error has not occured.')); + }); + + test.concurrent('Non-aiscript Error', async () => { + return expect(exeAndGetErrLoc(`/* (の位置 + */ + emitError() + `)).resolves.toEqual({ line: 3, column: 13}); + }); + + test.concurrent('No "var" in namespace declaration', async () => { + return expect(exeAndGetErrLoc(`// vの位置 + :: Ai { + let chan = 'kawaii' + var kun = '!?' + } + `)).resolves.toEqual({ line: 4, column: 5}); + }); + + test.concurrent('Index out of range', async () => { + return expect(exeAndGetErrLoc(`// [の位置 + let arr = [] + arr[0] + `)).resolves.toEqual({ line: 3, column: 7}); + }); + + test.concurrent('Error in passed function', async () => { + return expect(exeAndGetErrLoc(`// /の位置 + [0, 1, 2].map(@(v){ + 0/v + }) + `)).resolves.toEqual({ line: 3, column: 6}); + }); + + test.concurrent('No such prop', async () => { + return expect(exeAndGetErrLoc(`// .の位置 + [].ai + `)).resolves.toEqual({ line: 2, column: 6}); + }); +}); From 75a6c288a85ee2759dc56c90dfc62c2d982e5a34 Mon Sep 17 00:00:00 2001 From: marihachi Date: Thu, 2 Nov 2023 16:47:05 +0900 Subject: [PATCH 07/62] =?UTF-8?q?=E4=BA=88=E7=B4=84=E8=AA=9E=E3=81=AE?= =?UTF-8?q?=E3=83=90=E3=83=AA=E3=83=87=E3=83=BC=E3=82=B7=E3=83=A7=E3=83=B3?= =?UTF-8?q?=E5=A4=89=E6=9B=B4=20(#421)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * reserved words * Update validate-keyword.ts --- src/parser/plugins/validate-keyword.ts | 97 +++++++++++++++++--------- test/index.ts | 12 +--- 2 files changed, 68 insertions(+), 41 deletions(-) diff --git a/src/parser/plugins/validate-keyword.ts b/src/parser/plugins/validate-keyword.ts index 6c1fdac1..a179aba7 100644 --- a/src/parser/plugins/validate-keyword.ts +++ b/src/parser/plugins/validate-keyword.ts @@ -2,44 +2,56 @@ import { AiScriptSyntaxError } from '../../error.js'; import { visitNode } from '../visit.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', + 'do', + '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', + 'while', + 'yield', + 'import', + 'is', + 'meta', + 'module', + 'namespace', + 'new', ]; function throwReservedWordError(name: string, loc: Ast.Loc): void { @@ -48,10 +60,11 @@ function throwReservedWordError(name: string, loc: Ast.Loc): void { function validateNode(node: Ast.Node): Ast.Node { switch (node.type) { + case 'ns': case 'def': case 'attr': - case 'ns': - case 'identifier': { + case 'identifier': + case 'prop': { if (reservedWord.includes(node.name)) { throwReservedWordError(node.name, node.loc); } @@ -63,6 +76,18 @@ function validateNode(node: Ast.Node): Ast.Node { } 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)) { @@ -71,6 +96,14 @@ function validateNode(node: Ast.Node): Ast.Node { } break; } + case 'obj': { + for (const name of node.value.keys()) { + if (reservedWord.includes(name)) { + throwReservedWordError(name, node.loc); + } + } + break; + } } return node; diff --git a/test/index.ts b/test/index.ts index 7f766525..8bb13ab5 100644 --- a/test/index.ts +++ b/test/index.ts @@ -6,6 +6,7 @@ import * as assert from 'assert'; import { Parser, Interpreter, utils, errors, Ast } from '../src'; import { NUM, STR, NULL, ARR, OBJ, BOOL, TRUE, FALSE, ERROR ,FN_NATIVE } from '../src/interpreter/value'; +import { AiScriptSyntaxError } from '../src/error'; let { AiScriptRuntimeError, AiScriptIndexOutOfRangeError } = errors; const exe = (program: string): Promise => new Promise((ok, err) => { @@ -2918,7 +2919,7 @@ describe('Security', () => { `); assert.fail(); } catch (e) { - assert.ok(e instanceof AiScriptRuntimeError); + assert.ok(e instanceof AiScriptSyntaxError); } try { @@ -2941,13 +2942,6 @@ describe('Security', () => { }); 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 = {} @@ -2970,7 +2964,7 @@ describe('Security', () => { `); assert.fail(); } catch (e) { - assert.ok(e instanceof AiScriptRuntimeError); + assert.ok(e instanceof AiScriptSyntaxError); } try { From c9823578587e78c6a8681024000b505d9df27762 Mon Sep 17 00:00:00 2001 From: marihachi Date: Sat, 4 Nov 2023 10:16:03 +0900 Subject: [PATCH 08/62] update changelog --- CHANGELOG.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2514a989..e45e3130 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,12 @@ [Read translated version (en)](./translations/en/CHANGELOG.md) # Next -- 新しいAiScriptパーサーが実装されました。 -- スペースの厳密さが緩和されました。 -- 文字列リテラルやテンプレートで、`\`とそれに続く1文字は全てエスケープシーケンスとして扱われるようになりました。 -## Breaking changes -- 改行トークンを導入。改行の扱いが今までより厳密になりました。改行することができると決められた部分以外では文法エラーになります。 +- 新しいAiScriptパーサーを実装 + - スペースの厳密さが緩和 + - **Breaking Change** 改行トークンを導入。改行の扱いが今までより厳密になりました。改行することができる部分以外では文法エラーになります。 +- 文字列リテラルやテンプレートで、`\`とそれに続く1文字は全てエスケープシーケンスとして扱われるように +- 文法エラーやラインタイムエラーの発生位置が表示されるように +- **Breaking Change** 多くの予約語を追加。これまで変数名等に使えていた名前に影響が出る可能性があります。 # 0.17.0 - `package.json`を修正 From 3ef0b227840cee96431c8b66ad17ec9bbf473e28 Mon Sep 17 00:00:00 2001 From: marihachi Date: Sat, 4 Nov 2023 12:19:40 +0900 Subject: [PATCH 09/62] =?UTF-8?q?=E5=8C=BA=E5=88=87=E3=82=8A=E6=96=87?= =?UTF-8?q?=E5=AD=97=E3=81=AE=E5=A4=89=E6=9B=B4=20(#432)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * terminators * separator * add test * test * update test --- src/parser/syntaxes/common.ts | 70 +++-- src/parser/syntaxes/expressions.ts | 143 ++++++--- src/parser/syntaxes/statements.ts | 4 +- src/parser/syntaxes/toplevel.ts | 40 ++- test/index.ts | 482 ++++++++++++++++++++++++----- 5 files changed, 586 insertions(+), 153 deletions(-) diff --git a/src/parser/syntaxes/common.ts b/src/parser/syntaxes/common.ts index 6c200ad5..b4bd9dc0 100644 --- a/src/parser/syntaxes/common.ts +++ b/src/parser/syntaxes/common.ts @@ -8,7 +8,7 @@ import type * as Ast from '../../node.js'; /** * ```abnf - * Params = "(" [IDENT *(("," / SPACE) IDENT)] ")" + * Params = "(" [IDENT [":" Type] *(SEP IDENT [":" Type])] ")" * ``` */ export function parseParams(s: ITokenStream): { name: string, argType?: Ast.Node }[] { @@ -16,16 +16,11 @@ export function parseParams(s: ITokenStream): { name: string, argType?: Ast.Node s.nextWith(TokenKind.OpenParen); - while (s.kind !== TokenKind.CloseParen) { - // separator - if (items.length > 0) { - if (s.kind === TokenKind.Comma) { - s.next(); - } else if (!s.token.hasLeftSpacing) { - throw new AiScriptSyntaxError('separator expected', s.token.loc); - } - } + if (s.kind === TokenKind.NewLine) { + s.next(); + } + while (s.kind !== TokenKind.CloseParen) { s.expect(TokenKind.Identifier); const name = s.token.value!; s.next(); @@ -37,6 +32,27 @@ export function parseParams(s: ITokenStream): { name: string, argType?: Ast.Node } items.push({ name, argType: type }); + + // separator + switch (s.kind as TokenKind) { + case TokenKind.NewLine: { + s.next(); + break; + } + case TokenKind.Comma: { + s.next(); + if (s.kind === TokenKind.NewLine) { + s.next(); + } + break; + } + case TokenKind.CloseParen: { + break; + } + default: { + throw new AiScriptSyntaxError('separator expected', s.token.loc); + } + } } s.nextWith(TokenKind.CloseParen); @@ -60,11 +76,21 @@ export function parseBlock(s: ITokenStream): Ast.Node[] { while (s.kind !== TokenKind.CloseBrace) { steps.push(parseStatement(s)); - if ((s.kind as TokenKind) !== TokenKind.NewLine && (s.kind as TokenKind) !== TokenKind.CloseBrace) { - throw new AiScriptSyntaxError('Multiple statements cannot be placed on a single line.', s.token.loc); - } - while ((s.kind as TokenKind) === TokenKind.NewLine) { - s.next(); + // terminator + switch (s.kind as TokenKind) { + case TokenKind.NewLine: + case TokenKind.SemiColon: { + while ([TokenKind.NewLine, TokenKind.SemiColon].includes(s.kind)) { + s.next(); + } + break; + } + case TokenKind.CloseBrace: { + break; + } + default: { + throw new AiScriptSyntaxError('Multiple statements cannot be placed on a single line.', s.token.loc); + } } } @@ -86,7 +112,7 @@ export function parseType(s: ITokenStream): Ast.Node { /** * ```abnf * FnType = "@" "(" ParamTypes ")" "=>" Type - * ParamTypes = [Type *(("," / SPACE) Type)] + * ParamTypes = [Type *(SEP Type)] * ``` */ function parseFnType(s: ITokenStream): Ast.Node { @@ -98,10 +124,14 @@ function parseFnType(s: ITokenStream): Ast.Node { const params: Ast.Node[] = []; while (s.kind !== TokenKind.CloseParen) { if (params.length > 0) { - if (s.kind === TokenKind.Comma) { - s.next(); - } else if (!s.token.hasLeftSpacing) { - throw new AiScriptSyntaxError('separator expected', s.token.loc); + switch (s.kind as TokenKind) { + case TokenKind.Comma: { + s.next(); + break; + } + default: { + throw new AiScriptSyntaxError('separator expected', s.token.loc); + } } } const type = parseType(s); diff --git a/src/parser/syntaxes/expressions.ts b/src/parser/syntaxes/expressions.ts index eecbc66f..3adda1bd 100644 --- a/src/parser/syntaxes/expressions.ts +++ b/src/parser/syntaxes/expressions.ts @@ -287,7 +287,7 @@ function parseAtom(s: ITokenStream, isStatic: boolean): Ast.Node { } /** - * Call = "(" [Expr *(("," / SPACE) Expr)] ")" + * Call = "(" [Expr *(SEP Expr) [SEP]] ")" */ function parseCall(s: ITokenStream, target: Ast.Node): Ast.Node { const loc = s.token.loc; @@ -295,17 +295,33 @@ function parseCall(s: ITokenStream, target: Ast.Node): Ast.Node { s.nextWith(TokenKind.OpenParen); + if (s.kind === TokenKind.NewLine) { + s.next(); + } + while (s.kind !== TokenKind.CloseParen) { + items.push(parseExpr(s, false)); + // separator - if (items.length > 0) { - if (s.kind === TokenKind.Comma) { + switch (s.kind as TokenKind) { + case TokenKind.NewLine: { s.next(); - } else if (!s.token.hasLeftSpacing) { + break; + } + case TokenKind.Comma: { + s.next(); + if (s.kind === TokenKind.NewLine) { + s.next(); + } + break; + } + case TokenKind.CloseParen: { + break; + } + default: { throw new AiScriptSyntaxError('separator expected', s.token.loc); } } - - items.push(parseExpr(s, false)); } s.nextWith(TokenKind.CloseParen); @@ -377,7 +393,8 @@ function parseFnExpr(s: ITokenStream): Ast.Node { /** * ```abnf - * Match = "match" Expr "{" *("case" Expr "=>" BlockOrStatement) ["default" "=>" BlockOrStatement] "}" + * Match = "match" Expr "{" [MatchCases] ["default" "=>" BlockOrStatement [SEP]] "}" + * MatchCases = "case" Expr "=>" BlockOrStatement *(SEP "case" Expr "=>" BlockOrStatement) [SEP] * ``` */ function parseMatch(s: ITokenStream): Ast.Node { @@ -387,7 +404,10 @@ function parseMatch(s: ITokenStream): Ast.Node { const about = parseExpr(s, false); s.nextWith(TokenKind.OpenBrace); - s.nextWith(TokenKind.NewLine); + + if (s.kind === TokenKind.NewLine) { + s.next(); + } const qs: { q: Ast.Node, a: Ast.Node }[] = []; while (s.kind !== TokenKind.DefaultKeyword && s.kind !== TokenKind.CloseBrace) { @@ -395,8 +415,29 @@ function parseMatch(s: ITokenStream): Ast.Node { const q = parseExpr(s, false); s.nextWith(TokenKind.Arrow); const a = parseBlockOrStatement(s); - s.nextWith(TokenKind.NewLine); qs.push({ q, a }); + + // separator + switch (s.kind as TokenKind) { + case TokenKind.NewLine: { + s.next(); + break; + } + case TokenKind.Comma: { + s.next(); + if (s.kind === TokenKind.NewLine) { + s.next(); + } + break; + } + case TokenKind.DefaultKeyword: + case TokenKind.CloseBrace: { + break; + } + default: { + throw new AiScriptSyntaxError('separator expected', s.token.loc); + } + } } let x; @@ -404,7 +445,27 @@ function parseMatch(s: ITokenStream): Ast.Node { s.next(); s.nextWith(TokenKind.Arrow); x = parseBlockOrStatement(s); - s.nextWith(TokenKind.NewLine); + + // separator + switch (s.kind as TokenKind) { + case TokenKind.NewLine: { + s.next(); + break; + } + case TokenKind.Comma: { + s.next(); + if ((s.kind as TokenKind) === TokenKind.NewLine) { + s.next(); + } + break; + } + case TokenKind.CloseBrace: { + break; + } + default: { + throw new AiScriptSyntaxError('separator expected', s.token.loc); + } + } } s.nextWith(TokenKind.CloseBrace); @@ -470,7 +531,7 @@ function parseReference(s: ITokenStream): Ast.Node { /** * ```abnf - * Object = "{" [IDENT ":" Expr *(("," / ";" / SPACE) IDENT ":" Expr) ["," / ";"]] "}" + * Object = "{" [IDENT ":" Expr *(SEP IDENT ":" Expr) [SEP]] "}" * ``` */ function parseObject(s: ITokenStream, isStatic: boolean): Ast.Node { @@ -495,23 +556,25 @@ function parseObject(s: ITokenStream, isStatic: boolean): Ast.Node { map.set(k, v); // separator - if ((s.kind as TokenKind) === TokenKind.CloseBrace) { - break; - } else if (s.kind === TokenKind.Comma) { - s.next(); - } else if (s.kind === TokenKind.SemiColon) { - s.next(); - } else if (s.kind === TokenKind.NewLine) { - // noop - } else { - if (!s.token.hasLeftSpacing) { + switch (s.kind as TokenKind) { + case TokenKind.NewLine: { + s.next(); + break; + } + case TokenKind.Comma: { + s.next(); + if (s.kind === TokenKind.NewLine) { + s.next(); + } + break; + } + case TokenKind.CloseBrace: { + break; + } + default: { throw new AiScriptSyntaxError('separator expected', s.token.loc); } } - - if (s.kind === TokenKind.NewLine) { - s.next(); - } } s.nextWith(TokenKind.CloseBrace); @@ -521,7 +584,7 @@ function parseObject(s: ITokenStream, isStatic: boolean): Ast.Node { /** * ```abnf - * Array = "[" [Expr *(("," / SPACE) Expr) [","]] "]" + * Array = "[" [Expr *(SEP Expr) [SEP]] "]" * ``` */ function parseArray(s: ITokenStream, isStatic: boolean): Ast.Node { @@ -538,21 +601,25 @@ function parseArray(s: ITokenStream, isStatic: boolean): Ast.Node { value.push(parseExpr(s, isStatic)); // separator - if ((s.kind as TokenKind) === TokenKind.CloseBracket) { - break; - } else if (s.kind === TokenKind.Comma) { - s.next(); - } else if (s.kind === TokenKind.NewLine) { - // noop - } else { - if (!s.token.hasLeftSpacing) { + switch (s.kind as TokenKind) { + case TokenKind.NewLine: { + s.next(); + break; + } + case TokenKind.Comma: { + s.next(); + if (s.kind === TokenKind.NewLine) { + s.next(); + } + break; + } + case TokenKind.CloseBracket: { + break; + } + default: { throw new AiScriptSyntaxError('separator expected', s.token.loc); } } - - if (s.kind === TokenKind.NewLine) { - s.next(); - } } s.nextWith(TokenKind.CloseBracket); diff --git a/src/parser/syntaxes/statements.ts b/src/parser/syntaxes/statements.ts index c5655175..4c816a5b 100644 --- a/src/parser/syntaxes/statements.ts +++ b/src/parser/syntaxes/statements.ts @@ -212,7 +212,7 @@ function parseEach(s: ITokenStream): Ast.Node { if (s.kind === TokenKind.Comma) { s.next(); - } else if (!s.token.hasLeftSpacing) { + } else { throw new AiScriptSyntaxError('separator expected', s.token.loc); } @@ -262,7 +262,7 @@ function parseFor(s: ITokenStream): Ast.Node { if ((s.kind as TokenKind) === TokenKind.Comma) { s.next(); - } else if (!s.token.hasLeftSpacing) { + } else { throw new AiScriptSyntaxError('separator expected', s.token.loc); } diff --git a/src/parser/syntaxes/toplevel.ts b/src/parser/syntaxes/toplevel.ts index a84e60de..c0969e65 100644 --- a/src/parser/syntaxes/toplevel.ts +++ b/src/parser/syntaxes/toplevel.ts @@ -35,11 +35,21 @@ export function parseTopLevel(s: ITokenStream): Ast.Node[] { } } - if ((s.kind as TokenKind) !== TokenKind.NewLine && (s.kind as TokenKind) !== TokenKind.EOF) { - throw new AiScriptSyntaxError('Multiple statements cannot be placed on a single line.', s.token.loc); - } - while ((s.kind as TokenKind) === TokenKind.NewLine) { - s.next(); + // terminator + switch (s.kind as TokenKind) { + case TokenKind.NewLine: + case TokenKind.SemiColon: { + while ([TokenKind.NewLine, TokenKind.SemiColon].includes(s.kind)) { + s.next(); + } + break; + } + case TokenKind.EOF: { + break; + } + default: { + throw new AiScriptSyntaxError('Multiple statements cannot be placed on a single line.', s.token.loc); + } } } @@ -81,11 +91,21 @@ export function parseNamespace(s: ITokenStream): Ast.Node { } } - if ((s.kind as TokenKind) !== TokenKind.NewLine && (s.kind as TokenKind) !== TokenKind.CloseBrace) { - throw new AiScriptSyntaxError('Multiple statements cannot be placed on a single line.', s.token.loc); - } - while ((s.kind as TokenKind) === TokenKind.NewLine) { - s.next(); + // terminator + switch (s.kind as TokenKind) { + case TokenKind.NewLine: + case TokenKind.SemiColon: { + while ([TokenKind.NewLine, TokenKind.SemiColon].includes(s.kind)) { + s.next(); + } + break; + } + case TokenKind.CloseBrace: { + break; + } + default: { + throw new AiScriptSyntaxError('Multiple statements cannot be placed on a single line.', s.token.loc); + } } } s.nextWith(TokenKind.CloseBrace); diff --git a/test/index.ts b/test/index.ts index 8bb13ab5..e75a23dc 100644 --- a/test/index.ts +++ b/test/index.ts @@ -433,6 +433,368 @@ describe('Cannot put multiple statements in a line', () => { }); }); +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 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 with comma', 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)); + }); + }); +}); + test.concurrent('empty function', async () => { const res = await exe(` @hoge() { } @@ -476,8 +838,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) }, } } @@ -746,9 +1108,9 @@ describe('Object', () => { let obj = { a: { b: { - c: 42; - }; - }; + c: 42, + }, + }, } <: obj.a.b.c @@ -763,9 +1125,9 @@ describe('Object', () => { let obj = { a: { b: { - c: f; - }; - }; + c: f, + }, + }, } <: obj.a.b.c() @@ -805,7 +1167,7 @@ describe('Object', () => { test.concurrent('string key', async () => { const res = await exe(` let obj = { - "藍": 42; + "藍": 42, } <: obj."藍" @@ -816,7 +1178,7 @@ describe('Object', () => { test.concurrent('string key including colon and period', async () => { const res = await exe(` let obj = { - ":.:": 42; + ":.:": 42, } <: obj.":.:" @@ -829,7 +1191,7 @@ describe('Object', () => { let key = "藍" let obj = { - : 42; + : 42, } <: obj @@ -904,8 +1266,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") @@ -917,8 +1279,8 @@ describe('chain', () => { const res = await exe(` let obj = { a: { - b: ["ai", "chan", "kawaii"]; - }; + b: ["ai", "chan", "kawaii"], + }, } obj.a.b[1] = "taso" @@ -936,8 +1298,8 @@ describe('chain', () => { const res = await exe(` let obj = { a: { - b: ["ai", "chan", "kawaii"]; - }; + b: ["ai", "chan", "kawaii"], + }, } var x = null @@ -952,8 +1314,8 @@ describe('chain', () => { const res = await exe(` let arr = [ { - a: 1; - b: 2; + a: 1, + b: 2, } ] @@ -974,8 +1336,8 @@ describe('chain', () => { const res = await exe(` let obj = { a: { - b: [1, 2, 3]; - }; + b: [1, 2, 3], + }, } obj.a.b[1] += 1 @@ -1206,16 +1568,6 @@ describe('Function call', () => { eq(res, NUM(2)); }); - test.concurrent('with args (separated by space)', async () => { - const res = await exe(` - @f(x y) { - (x + y) - } - <: f(1 1) - `); - eq(res, NUM(2)); - }); - test.concurrent('std: throw AiScript error when required arg missing', async () => { try { await exe(` @@ -1404,7 +1756,7 @@ describe('exists', () => { test.concurrent('Basic', async () => { const res = await exe(` let foo = null - <: [(exists foo) (exists bar)] + <: [(exists foo), (exists bar)] `); eq(res, ARR([BOOL(true), BOOL(false)])); }); @@ -1601,7 +1953,7 @@ describe('loop', () => { test.concurrent('with continue', async () => { const res = await exe(` - var a = ["ai" "chan" "kawaii" "yo" "!"] + var a = ["ai", "chan", "kawaii", "yo", "!"] var b = [] loop { var x = a.shift() @@ -1723,7 +2075,7 @@ describe('for of', () => { test.concurrent('Break', async () => { const res = await exe(` let msgs = [] - each let item, ["ai", "chan", "kawaii" "yo"] { + each let item, ["ai", "chan", "kawaii", "yo"] { if (item == "kawaii") break msgs.push([item, "!"].join()) } @@ -1918,20 +2270,6 @@ describe('literal', () => { 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(` <: { @@ -1943,28 +2281,6 @@ describe('literal', () => { 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(` <: { @@ -1990,7 +2306,7 @@ describe('type declaration', () => { const res = await exe(` let abc: num = 1 var xyz: str = "abc" - <: [abc xyz] + <: [abc, xyz] `); eq(res, ARR([NUM(1), STR('abc')])); }); @@ -2014,7 +2330,7 @@ describe('type declaration', () => { describe('meta', () => { test.concurrent('default meta', async () => { const res = getMeta(` - ### { a: 1; b: 2; c: 3; } + ### { a: 1, b: 2, c: 3, } `); eq(res, new Map([ [null, { @@ -2077,7 +2393,7 @@ describe('meta', () => { describe('Array', () => { test.concurrent('valid', async () => { const res = getMeta(` - ### x [1 2 3] + ### x [1, 2, 3] `); eq(res, new Map([ ['x', [1, 2, 3]] @@ -2087,7 +2403,7 @@ describe('meta', () => { test.concurrent('invalid', async () => { try { getMeta(` - ### x [1 (2 + 2) 3] + ### x [1, (2 + 2), 3] `); } catch (e) { assert.ok(true); @@ -2100,7 +2416,7 @@ describe('meta', () => { describe('Object', () => { test.concurrent('valid', async () => { const res = getMeta(` - ### x { a: 1; b: 2; c: 3; } + ### x { a: 1, b: 2, c: 3, } `); eq(res, new Map([ ['x', { @@ -2114,7 +2430,7 @@ describe('meta', () => { test.concurrent('invalid', async () => { try { getMeta(` - ### x { a: 1; b: (2 + 2); c: 3; } + ### x { a: 1, b: (2 + 2), c: 3, } `); } catch (e) { assert.ok(true); @@ -2225,7 +2541,7 @@ describe('Attribute', () => { let attr: Ast.Attribute; const parser = new Parser(); const nodes = parser.parse(` - #[Endpoint { path: "/notes/create"; }] + #[Endpoint { path: "/notes/create" }] #[Desc "Create a note."] #[Cat true] @createNote(text) { @@ -2537,7 +2853,7 @@ describe('primitive props', () => { 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) + <: arr.reduce(@(accumulator, currentValue, index) { (accumulator + (currentValue * index)) }, 0) `); eq(res, NUM(20)); }); @@ -2745,13 +3061,13 @@ describe('std', () => { const res = await exe(` @test(seed) { let random = Math:gen_rng(seed) - return random(0 100) + return random(0, 100) } let seed1 = \`{Util:uuid()}\` let seed2 = \`{Date:year()}\` let test1 = if (test(seed1) == test(seed1)) {true} else {false} let test2 = if (test(seed1) == test(seed2)) {true} else {false} - <: [test1 test2] + <: [test1, test2] `) eq(res, ARR([BOOL(true), BOOL(false)])); }); @@ -2760,7 +3076,7 @@ describe('std', () => { describe('Obj', () => { test.concurrent('keys', async () => { const res = await exe(` - let o = { a: 1; b: 2; c: 3; } + let o = { a: 1, b: 2, c: 3, } <: Obj:keys(o) `); @@ -2769,7 +3085,7 @@ describe('std', () => { test.concurrent('vals', async () => { const res = await exe(` - let o = { _nul: null; _num: 24; _str: 'hoge'; _arr: []; _obj: {}; } + let o = { _nul: null, _num: 24, _str: 'hoge', _arr: [], _obj: {}, } <: Obj:vals(o) `); @@ -2778,7 +3094,7 @@ describe('std', () => { test.concurrent('kvs', async () => { const res = await exe(` - let o = { a: 1; b: 2; c: 3; } + let o = { a: 1, b: 2, c: 3, } <: Obj:kvs(o) `); From b9df8a456724c124527be30be58578888e4250b8 Mon Sep 17 00:00:00 2001 From: FineArchs <133759614+FineArchs@users.noreply.github.com> Date: Sat, 4 Nov 2023 17:32:09 +0900 Subject: [PATCH 10/62] Update index.ts (#454) --- test/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/index.ts b/test/index.ts index e75a23dc..7eb82f9b 100644 --- a/test/index.ts +++ b/test/index.ts @@ -3107,8 +3107,8 @@ describe('std', () => { test.concurrent('merge', async () => { const res = await exe(` - let o1 = { a: 1; b: 2; } - let o2 = { b: 3; c: 4; } + let o1 = { a: 1, b: 2 } + let o2 = { b: 3, c: 4 } <: Obj:merge(o1, o2) `); From 70954a6c71356c9797600b91d166b784128a835a Mon Sep 17 00:00:00 2001 From: FineArchs <133759614+FineArchs@users.noreply.github.com> Date: Sat, 4 Nov 2023 21:17:11 +0900 Subject: [PATCH 11/62] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e45e3130..271d0dc9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - **Breaking Change** 改行トークンを導入。改行の扱いが今までより厳密になりました。改行することができる部分以外では文法エラーになります。 - 文字列リテラルやテンプレートで、`\`とそれに続く1文字は全てエスケープシーケンスとして扱われるように - 文法エラーやラインタイムエラーの発生位置が表示されるように +- **Breaking Change** パースの都合によりmatch文の構文を変更。パターンの前に`case`キーワードが必要となり、`*`は`default`に変更。 - **Breaking Change** 多くの予約語を追加。これまで変数名等に使えていた名前に影響が出る可能性があります。 # 0.17.0 From a507ebe650ef221b8191e0b301f00f358604dec5 Mon Sep 17 00:00:00 2001 From: FineArchs <133759614+FineArchs@users.noreply.github.com> Date: Sat, 4 Nov 2023 21:41:50 +0900 Subject: [PATCH 12/62] Update syntax.md --- docs/syntax.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/syntax.md b/docs/syntax.md index ed21880a..7d6b8097 100644 --- a/docs/syntax.md +++ b/docs/syntax.md @@ -85,11 +85,14 @@ let foo = eval { ``` 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 From d1d0892c372aae364039c873248c0fddabef9df8 Mon Sep 17 00:00:00 2001 From: FineArchs <133759614+FineArchs@users.noreply.github.com> Date: Sat, 4 Nov 2023 21:51:11 +0900 Subject: [PATCH 13/62] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 271d0dc9..21914201 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ - スペースの厳密さが緩和 - **Breaking Change** 改行トークンを導入。改行の扱いが今までより厳密になりました。改行することができる部分以外では文法エラーになります。 - 文字列リテラルやテンプレートで、`\`とそれに続く1文字は全てエスケープシーケンスとして扱われるように -- 文法エラーやラインタイムエラーの発生位置が表示されるように +- 文法エラーに加えランタイムエラーの発生位置が表示されるように。行・列の数値は1始まりになりました - **Breaking Change** パースの都合によりmatch文の構文を変更。パターンの前に`case`キーワードが必要となり、`*`は`default`に変更。 - **Breaking Change** 多くの予約語を追加。これまで変数名等に使えていた名前に影響が出る可能性があります。 From 1fc890b531410599a6f36c3d08173ca3897b4fc1 Mon Sep 17 00:00:00 2001 From: FineArchs <133759614+FineArchs@users.noreply.github.com> Date: Sat, 4 Nov 2023 23:35:59 +0900 Subject: [PATCH 14/62] Update get-started.md --- docs/get-started.md | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/docs/get-started.md b/docs/get-started.md index f23eadea..c17fddc5 100644 --- a/docs/get-started.md +++ b/docs/get-started.md @@ -261,8 +261,8 @@ AiScriptファイルにメタデータを埋め込める機能です。 ``` ## エラー型 -一部の標準関数は実行失敗時にエラー型の値を返します。 -これによりエラー処理を行うことができます。 +一部の標準関数は実行失敗時にエラー型の値を返します。 +これによりエラー処理を行うことができます。 ``` @validate(str){ let v=Json:parse(str) @@ -270,3 +270,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始まりです。 From 53e2f54a3ce5ad9840c1baf7cc8bcb967ea9c63d Mon Sep 17 00:00:00 2001 From: FineArchs <133759614+FineArchs@users.noreply.github.com> Date: Sun, 12 Nov 2023 03:54:54 +0900 Subject: [PATCH 15/62] Update CHANGELOG.md --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 21914201..d8c7f60f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,9 @@ - スペースの厳密さが緩和 - **Breaking Change** 改行トークンを導入。改行の扱いが今までより厳密になりました。改行することができる部分以外では文法エラーになります。 - 文字列リテラルやテンプレートで、`\`とそれに続く1文字は全てエスケープシーケンスとして扱われるように -- 文法エラーに加えランタイムエラーの発生位置が表示されるように。行・列の数値は1始まりになりました +- 文法エラーの表示を改善。理由を詳細に表示するように。 +- 複数行のコメントがある時に文法エラーの表示行数がずれる問題を解消しました。 +- 実行時エラーの発生位置が表示されるように。 - **Breaking Change** パースの都合によりmatch文の構文を変更。パターンの前に`case`キーワードが必要となり、`*`は`default`に変更。 - **Breaking Change** 多くの予約語を追加。これまで変数名等に使えていた名前に影響が出る可能性があります。 From e53b2b05040ef0fe1650574caf078fdfd806e1a2 Mon Sep 17 00:00:00 2001 From: FineArchs <133759614+FineArchs@users.noreply.github.com> Date: Thu, 16 Nov 2023 18:22:24 +0900 Subject: [PATCH 16/62] Update keywords.md --- docs/keywords.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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` From 95fa6fdeacc9597d193f79cc80f32e310a79ff61 Mon Sep 17 00:00:00 2001 From: FineArchs <133759614+FineArchs@users.noreply.github.com> Date: Thu, 16 Nov 2023 18:33:13 +0900 Subject: [PATCH 17/62] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8c7f60f..f6815bf3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - 実行時エラーの発生位置が表示されるように。 - **Breaking Change** パースの都合によりmatch文の構文を変更。パターンの前に`case`キーワードが必要となり、`*`は`default`に変更。 - **Breaking Change** 多くの予約語を追加。これまで変数名等に使えていた名前に影響が出る可能性があります。 +- **Breaking Change** 配列及び関数の引数において、空白区切りが使用できなくなりました。`,`または改行が必要です。 # 0.17.0 - `package.json`を修正 From 7fa8ec0f21b552c22b0ba36cf4a5b3b9c75ec07d Mon Sep 17 00:00:00 2001 From: FineArchs <133759614+FineArchs@users.noreply.github.com> Date: Thu, 16 Nov 2023 19:24:22 +0900 Subject: [PATCH 18/62] Update get-started.md --- docs/get-started.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/get-started.md b/docs/get-started.md index 507aa36e..a629c0ec 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" ``` @@ -216,7 +216,7 @@ for (100) { ## 繰り返し(配列) `each`を使うと、配列の各アイテムに対し処理を繰り返すことができます: ``` -let items = ["a" "b" "c"] +let items = ["a", "b", "c"] each (let item, items) { <: item } @@ -260,7 +260,7 @@ AiScriptファイルにメタデータを埋め込める機能です。 ### { name: "example" version: 42 - keywords: ["foo" "bar" "baz"] + keywords: ["foo", "bar", "baz"] } ``` From 81ac81af9b5389b305f0a14df4c757e7ec5b8be8 Mon Sep 17 00:00:00 2001 From: marihachi Date: Thu, 16 Nov 2023 20:46:16 +0900 Subject: [PATCH 19/62] fix tail comment (#468) --- src/parser/scanner.ts | 1 - test/index.ts | 9 +++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/parser/scanner.ts b/src/parser/scanner.ts index 21d42f28..c1b07983 100644 --- a/src/parser/scanner.ts +++ b/src/parser/scanner.ts @@ -582,7 +582,6 @@ export class Scanner implements ITokenStream { break; } if (this.stream.char === '\n') { - this.stream.next(); break; } this.stream.next(); diff --git a/test/index.ts b/test/index.ts index 7eb82f9b..2c28d4ac 100644 --- a/test/index.ts +++ b/test/index.ts @@ -341,6 +341,15 @@ describe('Comment', () => { 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')); + }); }); test.concurrent('式にコロンがあってもオブジェクトと判定されない', async () => { From 6d0e268a36a86317a5b846453a1c84c462803247 Mon Sep 17 00:00:00 2001 From: Fine Archs Date: Fri, 19 Jan 2024 04:08:37 +0900 Subject: [PATCH 20/62] optional argument --- etc/aiscript.api.md | 14 ++++++++++++++ src/interpreter/index.ts | 19 +++++++++++++++---- src/interpreter/util.ts | 7 ++++++- src/interpreter/value.ts | 8 +++++++- src/node.ts | 1 + src/parser/scanner.ts | 5 +++++ src/parser/syntaxes/common.ts | 9 +++++++-- src/parser/token.ts | 2 ++ test/index.ts | 23 +++++++++++++++++++++++ 9 files changed, 80 insertions(+), 8 deletions(-) diff --git a/etc/aiscript.api.md b/etc/aiscript.api.md index d89418c6..8277f128 100644 --- a/etc/aiscript.api.md +++ b/etc/aiscript.api.md @@ -267,6 +267,7 @@ type Fn = NodeBase & { type: 'fn'; args: { name: string; + optional?: boolean; argType?: TypeSource; }[]; retType?: TypeSource; @@ -608,6 +609,7 @@ declare namespace values { VObj, VFn, VUserFn, + VFnArg, VNativeFn, VReturn, VBreak, @@ -668,6 +670,13 @@ type VError = { // @public (undocumented) type VFn = VUserFn | VNativeFn; +// @public (undocumented) +type VFnArg = { + name: string; + optional?: boolean; + type?: Type; +}; + // @public type VNativeFn = VFnBase & { native: (args: (Value | undefined)[], opts: { @@ -712,10 +721,15 @@ type VStr = { // @public (undocumented) type VUserFn = VFnBase & { native?: undefined; + args: VFnArg[]; statements: Node_2[]; scope: Scope; }; +// Warnings were encountered during analysis: +// +// src/interpreter/value.ts:47: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/src/interpreter/index.ts b/src/interpreter/index.ts index 3b5a5538..242c1cde 100644 --- a/src/interpreter/index.ts +++ b/src/interpreter/index.ts @@ -230,10 +230,11 @@ 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 in fn.args) { + if (!fn.args[i]!.optional) expectAny(args[i]); + _args.set(fn.args[i]!.name, { isMutable: true, - value: args[i]!, + value: args[i] ?? NULL, }); } const fnScope = fn.scope!.createChildScope(_args); @@ -486,7 +487,17 @@ export class Interpreter { } case 'fn': { - return FN(node.args.map(arg => arg.name), node.children, scope); + return FN( + node.args.map(arg => { + return { + name: arg.name, + optional: arg.optional, + // type: (TODO) + }; + }), + node.children, + scope, + ); } case 'block': { diff --git a/src/interpreter/util.ts b/src/interpreter/util.ts index 1d60c0f3..6c5d55f7 100644 --- a/src/interpreter/util.ts +++ b/src/interpreter/util.ts @@ -188,7 +188,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 90c531b0..f0616554 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 = { @@ -33,13 +34,18 @@ export type VObj = { export type VFn = VUserFn | VNativeFn; type VFnBase = { type: 'fn'; - args?: string[]; }; export type VUserFn = VFnBase & { native?: undefined; // if (vfn.native) で型アサーション出来るように + args: VFnArg[]; statements: Node[]; scope: Scope; }; +export type VFnArg = { + name: string; + optional?: boolean; + type?: Type; +} /** * 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. */ diff --git a/src/node.ts b/src/node.ts index caa33196..09f80e71 100644 --- a/src/node.ts +++ b/src/node.ts @@ -175,6 +175,7 @@ export type Fn = NodeBase & { type: 'fn'; // 関数 args: { name: string; // 引数名 + optional?: boolean; // 引数に?がついているか argType?: TypeSource; // 引数の型 }[]; retType?: TypeSource; // 戻り値の型 diff --git a/src/parser/scanner.ts b/src/parser/scanner.ts index 21d42f28..89412772 100644 --- a/src/parser/scanner.ts +++ b/src/parser/scanner.ts @@ -268,6 +268,11 @@ export class Scanner implements ITokenStream { } break; } + case '?': { + this.stream.next(); + token = TOKEN(TokenKind.Question, loc, { hasLeftSpacing }); + break; + } case '@': { this.stream.next(); token = TOKEN(TokenKind.At, loc, { hasLeftSpacing }); diff --git a/src/parser/syntaxes/common.ts b/src/parser/syntaxes/common.ts index b4bd9dc0..c50707ec 100644 --- a/src/parser/syntaxes/common.ts +++ b/src/parser/syntaxes/common.ts @@ -12,7 +12,7 @@ import type * as Ast from '../../node.js'; * ``` */ export function parseParams(s: ITokenStream): { name: string, argType?: Ast.Node }[] { - const items: { name: string, argType?: Ast.Node }[] = []; + const items: { name: string, optional?: boolean, argType?: Ast.Node }[] = []; s.nextWith(TokenKind.OpenParen); @@ -25,13 +25,18 @@ export function parseParams(s: ITokenStream): { name: string, argType?: Ast.Node const name = s.token.value!; s.next(); + let optional; + if ((s.kind as TokenKind) === TokenKind.Question) { + s.next(); + optional = true; + } let type; if ((s.kind as TokenKind) === TokenKind.Colon) { s.next(); type = parseType(s); } - items.push({ name, argType: type }); + items.push({ name, optional, argType: type }); // separator switch (s.kind as TokenKind) { diff --git a/src/parser/token.ts b/src/parser/token.ts index 67aca6b6..455ce450 100644 --- a/src/parser/token.ts +++ b/src/parser/token.ts @@ -87,6 +87,8 @@ export enum TokenKind { Gt, /** ">=" */ GtEq, + /** "?" */ + Question, /** "@" */ At, /** "[" */ diff --git a/test/index.ts b/test/index.ts index 7eb82f9b..05380c63 100644 --- a/test/index.ts +++ b/test/index.ts @@ -1568,6 +1568,29 @@ describe('Function call', () => { eq(res, NUM(2)); }); + test.concurrent('optional args', async () => { + const res = await exe(` + @f(x, y?, z?) { + [x, y, z] + } + <: f(true) + `); + eq(res, ARR([TRUE, NULL, NULL])); + }); + + 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 () => { try { await exe(` From 46f0da92aefcb076a6b71d41c2112109fa6b67aa Mon Sep 17 00:00:00 2001 From: FineArchs <133759614+FineArchs@users.noreply.github.com> Date: Sun, 19 Nov 2023 00:52:31 +0900 Subject: [PATCH 21/62] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 063a94c2..dfff1246 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - 文字列リテラルやテンプレートで、`\`とそれに続く1文字は全てエスケープシーケンスとして扱われるように - 文法エラーやラインタイムエラーの発生位置が表示されるように - **Breaking Change** 多くの予約語を追加。これまで変数名等に使えていた名前に影響が出る可能性があります。 +- 省略可能引数を追加。引数名に`?`を後置することでその引数は省略可能となります。(逆にそうでない引数が省略されると即時エラーとなります) # 0.17.0 - `package.json`を修正 From 5bfc73e64121ebd6aa58dc89817fe78612e07f20 Mon Sep 17 00:00:00 2001 From: Fine Archs Date: Sun, 19 Nov 2023 02:38:08 +0900 Subject: [PATCH 22/62] args with default value --- etc/aiscript.api.md | 6 +++--- src/interpreter/index.ts | 13 +++++++------ src/interpreter/value.ts | 2 +- src/node.ts | 2 +- src/parser/syntaxes/common.ts | 13 +++++++++---- src/parser/visit.ts | 5 +++++ test/index.ts | 10 ++++++++++ 7 files changed, 36 insertions(+), 15 deletions(-) diff --git a/etc/aiscript.api.md b/etc/aiscript.api.md index aa3df4d2..347b7dee 100644 --- a/etc/aiscript.api.md +++ b/etc/aiscript.api.md @@ -270,7 +270,7 @@ type Fn = NodeBase & { type: 'fn'; args: { name: string; - optional?: boolean; + default?: Expression; argType?: TypeSource; }[]; retType?: TypeSource; @@ -676,8 +676,8 @@ type VFn = VUserFn | VNativeFn; // @public (undocumented) type VFnArg = { name: string; - optional?: boolean; type?: Type; + default?: Value; }; // @public @@ -731,7 +731,7 @@ type VUserFn = VFnBase & { // Warnings were encountered during analysis: // -// src/interpreter/value.ts:47:2 - (ae-forgotten-export) The symbol "Type" needs to be exported by the entry point index.d.ts +// 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/src/interpreter/index.ts b/src/interpreter/index.ts index 88994e5b..3a3a95de 100644 --- a/src/interpreter/index.ts +++ b/src/interpreter/index.ts @@ -231,10 +231,11 @@ export class Interpreter { } else { const _args = new Map(); for (const i in fn.args) { - if (!fn.args[i]!.optional) expectAny(args[i]); - _args.set(fn.args[i]!.name, { + 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); @@ -488,13 +489,13 @@ export class Interpreter { case 'fn': { return FN( - node.args.map(arg => { + await Promise.all(node.args.map(async (arg) => { return { name: arg.name, - optional: arg.optional, + default: arg.default ? await this._eval(arg.default, scope) : undefined, // type: (TODO) }; - }), + })), node.children, scope, ); diff --git a/src/interpreter/value.ts b/src/interpreter/value.ts index f0616554..3250afd7 100644 --- a/src/interpreter/value.ts +++ b/src/interpreter/value.ts @@ -43,8 +43,8 @@ export type VUserFn = VFnBase & { }; export type VFnArg = { name: string; - optional?: boolean; 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. diff --git a/src/node.ts b/src/node.ts index 09f80e71..1994b25a 100644 --- a/src/node.ts +++ b/src/node.ts @@ -175,7 +175,7 @@ export type Fn = NodeBase & { type: 'fn'; // 関数 args: { name: string; // 引数名 - optional?: boolean; // 引数に?がついているか + default?: Expression; // 引数の初期値 argType?: TypeSource; // 引数の型 }[]; retType?: TypeSource; // 戻り値の型 diff --git a/src/parser/syntaxes/common.ts b/src/parser/syntaxes/common.ts index c50707ec..a0519ceb 100644 --- a/src/parser/syntaxes/common.ts +++ b/src/parser/syntaxes/common.ts @@ -2,6 +2,7 @@ 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'; @@ -12,7 +13,7 @@ import type * as Ast from '../../node.js'; * ``` */ export function parseParams(s: ITokenStream): { name: string, argType?: Ast.Node }[] { - const items: { name: string, optional?: boolean, argType?: Ast.Node }[] = []; + const items: { name: string, default?: Ast.Node, argType?: Ast.Node }[] = []; s.nextWith(TokenKind.OpenParen); @@ -25,18 +26,22 @@ export function parseParams(s: ITokenStream): { name: string, argType?: Ast.Node const name = s.token.value!; s.next(); - let optional; + let defaultExpr; if ((s.kind as TokenKind) === TokenKind.Question) { s.next(); - optional = true; + defaultExpr = { type: 'null' } as Ast.Null; } let type; if ((s.kind as TokenKind) === TokenKind.Colon) { s.next(); type = parseType(s); } + if ((s.kind as TokenKind) === TokenKind.Eq) { + s.next(); + defaultExpr = parseExpr(s, false); + } - items.push({ name, optional, argType: type }); + items.push({ name, default: defaultExpr, argType: type }); // separator switch (s.kind as TokenKind) { diff --git a/src/parser/visit.ts b/src/parser/visit.ts index 29b5cbdf..861e8caf 100644 --- a/src/parser/visit.ts +++ b/src/parser/visit.ts @@ -61,6 +61,11 @@ export function visitNode(node: Ast.Node, fn: (node: Ast.Node) => Ast.Node): Ast break; } case 'fn': { + for (const i in result.args) { + 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 Ast.Fn['children'][number]; } diff --git a/test/index.ts b/test/index.ts index 1c4cff5e..cc7c49f1 100644 --- a/test/index.ts +++ b/test/index.ts @@ -1613,6 +1613,16 @@ describe('Function call', () => { 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('missing arg', async () => { try { await exe(` From edcf51cf4cae079c380b6e10548b158243139b88 Mon Sep 17 00:00:00 2001 From: Fine Archs Date: Sun, 19 Nov 2023 02:44:40 +0900 Subject: [PATCH 23/62] update CHANGELOG --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dfff1246..eecb12a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ - 文字列リテラルやテンプレートで、`\`とそれに続く1文字は全てエスケープシーケンスとして扱われるように - 文法エラーやラインタイムエラーの発生位置が表示されるように - **Breaking Change** 多くの予約語を追加。これまで変数名等に使えていた名前に影響が出る可能性があります。 -- 省略可能引数を追加。引数名に`?`を後置することでその引数は省略可能となります。(逆にそうでない引数が省略されると即時エラーとなります) +- 省略可能引数と初期値付き引数を追加。引数名に`?`を後置することでその引数は省略可能となります。引数に`=<式>`を後置すると引数に初期値を設定できます。省略可能引数は初期値`null`の引数と同等です。いずれでもない引数が省略されると即時エラーとなります。 # 0.17.0 - `package.json`を修正 From 6335dbfd05fe3e17380eeb291409b7d4ad879247 Mon Sep 17 00:00:00 2001 From: FineArchs <133759614+FineArchs@users.noreply.github.com> Date: Sun, 19 Nov 2023 21:01:02 +0900 Subject: [PATCH 24/62] Update syntax.md --- docs/syntax.md | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/docs/syntax.md b/docs/syntax.md index 1cc63213..bb4562f4 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等の予約語は関数名として使用できない From 3bb168561a0caa08edc8f116383e309899181e3e Mon Sep 17 00:00:00 2001 From: Fine Archs Date: Sun, 19 Nov 2023 22:38:25 +0900 Subject: [PATCH 25/62] move optionality process --- etc/aiscript.api.md | 1 + src/interpreter/index.ts | 2 +- src/node.ts | 1 + src/parser/syntaxes/common.ts | 17 +++++++++-------- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/etc/aiscript.api.md b/etc/aiscript.api.md index 347b7dee..64a7eda2 100644 --- a/etc/aiscript.api.md +++ b/etc/aiscript.api.md @@ -270,6 +270,7 @@ type Fn = NodeBase & { type: 'fn'; args: { name: string; + optional?: boolean; default?: Expression; argType?: TypeSource; }[]; diff --git a/src/interpreter/index.ts b/src/interpreter/index.ts index 3a3a95de..2513be3b 100644 --- a/src/interpreter/index.ts +++ b/src/interpreter/index.ts @@ -492,7 +492,7 @@ export class Interpreter { await Promise.all(node.args.map(async (arg) => { return { name: arg.name, - default: arg.default ? await this._eval(arg.default, scope) : undefined, + default: arg.default ? await this._eval(arg.default, scope) : arg.optional ? NULL : undefined, // type: (TODO) }; })), diff --git a/src/node.ts b/src/node.ts index 1994b25a..537928c4 100644 --- a/src/node.ts +++ b/src/node.ts @@ -175,6 +175,7 @@ export type Fn = NodeBase & { type: 'fn'; // 関数 args: { name: string; // 引数名 + optional?: boolean; default?: Expression; // 引数の初期値 argType?: TypeSource; // 引数の型 }[]; diff --git a/src/parser/syntaxes/common.ts b/src/parser/syntaxes/common.ts index a0519ceb..209b101a 100644 --- a/src/parser/syntaxes/common.ts +++ b/src/parser/syntaxes/common.ts @@ -13,7 +13,7 @@ import type * as Ast from '../../node.js'; * ``` */ export function parseParams(s: ITokenStream): { name: string, argType?: Ast.Node }[] { - const items: { name: string, default?: Ast.Node, argType?: Ast.Node }[] = []; + const items: { name: string, optional?: boolean, default?: Ast.Node, argType?: Ast.Node }[] = []; s.nextWith(TokenKind.OpenParen); @@ -26,22 +26,23 @@ export function parseParams(s: ITokenStream): { name: string, argType?: Ast.Node const name = s.token.value!; s.next(); - let defaultExpr; + let optional; if ((s.kind as TokenKind) === TokenKind.Question) { s.next(); - defaultExpr = { type: 'null' } as Ast.Null; + optional = true; + } + let defaultExpr; + if ((s.kind as TokenKind) === TokenKind.Eq) { + s.next(); + defaultExpr = parseExpr(s, false); } let type; if ((s.kind as TokenKind) === TokenKind.Colon) { s.next(); type = parseType(s); } - if ((s.kind as TokenKind) === TokenKind.Eq) { - s.next(); - defaultExpr = parseExpr(s, false); - } - items.push({ name, default: defaultExpr, argType: type }); + items.push({ name, optional, default: defaultExpr, argType: type }); // separator switch (s.kind as TokenKind) { From c03fdaed28850b072104e988568913508210290d Mon Sep 17 00:00:00 2001 From: Fine Archs Date: Mon, 20 Nov 2023 08:48:36 +0900 Subject: [PATCH 26/62] change type --- etc/aiscript.api.md | 2 +- src/node.ts | 2 +- src/parser/syntaxes/common.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/etc/aiscript.api.md b/etc/aiscript.api.md index 64a7eda2..2a2ffca8 100644 --- a/etc/aiscript.api.md +++ b/etc/aiscript.api.md @@ -270,7 +270,7 @@ type Fn = NodeBase & { type: 'fn'; args: { name: string; - optional?: boolean; + optional: boolean; default?: Expression; argType?: TypeSource; }[]; diff --git a/src/node.ts b/src/node.ts index 537928c4..27df81ff 100644 --- a/src/node.ts +++ b/src/node.ts @@ -175,7 +175,7 @@ export type Fn = NodeBase & { type: 'fn'; // 関数 args: { name: string; // 引数名 - optional?: boolean; + optional: boolean; default?: Expression; // 引数の初期値 argType?: TypeSource; // 引数の型 }[]; diff --git a/src/parser/syntaxes/common.ts b/src/parser/syntaxes/common.ts index 209b101a..262ab151 100644 --- a/src/parser/syntaxes/common.ts +++ b/src/parser/syntaxes/common.ts @@ -26,7 +26,7 @@ export function parseParams(s: ITokenStream): { name: string, argType?: Ast.Node const name = s.token.value!; s.next(); - let optional; + let optional = false; if ((s.kind as TokenKind) === TokenKind.Question) { s.next(); optional = true; From eaf8f6d022c9af5225070f5cb610a273f9ada89a Mon Sep 17 00:00:00 2001 From: FineArchs <133759614+FineArchs@users.noreply.github.com> Date: Sat, 20 Jan 2024 12:34:42 +0900 Subject: [PATCH 27/62] =?UTF-8?q?=E6=BC=94=E7=AE=97=E5=AD=90=E3=81=AE?= =?UTF-8?q?=E9=A0=85=E7=9B=AE=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/syntax.md | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/docs/syntax.md b/docs/syntax.md index d929747d..7e4c204c 100644 --- a/docs/syntax.md +++ b/docs/syntax.md @@ -190,6 +190,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に評価されるかで条件分岐を行います。 式として扱うことができ、最後の文の値を返します。 From 9155dd07c5a31f51c4874b2cc3de499f8b88ba46 Mon Sep 17 00:00:00 2001 From: FineArchs <133759614+FineArchs@users.noreply.github.com> Date: Sat, 20 Jan 2024 12:36:09 +0900 Subject: [PATCH 28/62] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 253ffc39..13574f29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - **Breaking Change** パースの都合によりmatch文の構文を変更。パターンの前に`case`キーワードが必要となり、`*`は`default`に変更。 - **Breaking Change** 多くの予約語を追加。これまで変数名等に使えていた名前に影響が出る可能性があります。 - **Breaking Change** 配列及び関数の引数において、空白区切りが使用できなくなりました。`,`または改行が必要です。 +- **Breaking Change** `+`や`!`などの演算子の優先順位に変更があります。新しい順序は[syntax.md](docs/syntax.md#%E6%BC%94%E7%AE%97%E5%AD%90)を参照して下さい。 # 0.17.0 - `package.json`を修正 From f605bff4abc4bacf3023cf2ebcf201b579a20d74 Mon Sep 17 00:00:00 2001 From: FineArchs <133759614+FineArchs@users.noreply.github.com> Date: Sat, 20 Jan 2024 12:40:39 +0900 Subject: [PATCH 29/62] Update get-started.md --- docs/get-started.md | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/docs/get-started.md b/docs/get-started.md index a629c0ec..cf2f975d 100644 --- a/docs/get-started.md +++ b/docs/get-started.md @@ -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 { ~ }` を使うと、ブロック内で最後に書かれた式が値として返されます。 From 928bc9679ade55e3b959f5d0f92a0e50656c4eef Mon Sep 17 00:00:00 2001 From: ikasoba <57828948+ikasoba@users.noreply.github.com> Date: Sat, 20 Jan 2024 17:09:53 +0900 Subject: [PATCH 30/62] =?UTF-8?q?=E3=81=AA=E3=82=93=E3=81=8B=E3=81=A7?= =?UTF-8?q?=E3=81=8D=E3=81=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/interpreter/util.ts | 1 + test/index.ts | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/src/interpreter/util.ts b/src/interpreter/util.ts index 1d60c0f3..f3397dfc 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; diff --git a/test/index.ts b/test/index.ts index f5bdde2e..a4ed4cfe 100644 --- a/test/index.ts +++ b/test/index.ts @@ -55,6 +55,14 @@ describe('ops', () => { 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 = @(){} + <: (f == f) + `), BOOL(true)); }); test.concurrent('!=', async () => { From 3d86171475d10cc6b79da36e25909d0c875a1e6c Mon Sep 17 00:00:00 2001 From: ikasoba <57828948+ikasoba@users.noreply.github.com> Date: Sat, 20 Jan 2024 17:14:01 +0900 Subject: [PATCH 31/62] =?UTF-8?q?changelog=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 253ffc39..1c09c481 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - **Breaking Change** パースの都合によりmatch文の構文を変更。パターンの前に`case`キーワードが必要となり、`*`は`default`に変更。 - **Breaking Change** 多くの予約語を追加。これまで変数名等に使えていた名前に影響が出る可能性があります。 - **Breaking Change** 配列及び関数の引数において、空白区切りが使用できなくなりました。`,`または改行が必要です。 +- **Breaking Change** 関数同士の比較の実装 # 0.17.0 - `package.json`を修正 From f692bb14bc74a5158925894a130f3f2dd0b637ad Mon Sep 17 00:00:00 2001 From: ikasoba <57828948+ikasoba@users.noreply.github.com> Date: Sun, 21 Jan 2024 17:17:55 +0900 Subject: [PATCH 32/62] =?UTF-8?q?=E3=83=86=E3=82=B9=E3=83=88=E3=82=B1?= =?UTF-8?q?=E3=83=BC=E3=82=B9=E3=82=92=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/index.ts b/test/index.ts index a4ed4cfe..35db5629 100644 --- a/test/index.ts +++ b/test/index.ts @@ -61,7 +61,9 @@ describe('ops', () => { eq(await exe('<: (Core:eq == @(){})'), BOOL(false)); eq(await exe(` let f = @(){} - <: (f == f) + let g = f + + <: (f == g) `), BOOL(true)); }); From df2988674f6d90b2813200dc15e7e3ff8ec6fa4c Mon Sep 17 00:00:00 2001 From: FineArchs <133759614+FineArchs@users.noreply.github.com> Date: Sun, 21 Jan 2024 18:08:36 +0900 Subject: [PATCH 33/62] Update syntax.md --- docs/syntax.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/syntax.md b/docs/syntax.md index 7e4c204c..ff1f6482 100644 --- a/docs/syntax.md +++ b/docs/syntax.md @@ -221,7 +221,7 @@ Ai:kun() // kawaii <Core:lt小さい <=Core:lteq以下 ==Core:eq等しい - !=Core:neq不等 + !=Core:neq等しくない &&Core:andかつ ||Core:orまたは From 149b34dd4e8188afaba89286cf896610a00448a2 Mon Sep 17 00:00:00 2001 From: marihachi Date: Mon, 11 Mar 2024 23:03:08 +0900 Subject: [PATCH 34/62] =?UTF-8?q?Next:=20hex=20API=E3=81=AE=E5=A4=89?= =?UTF-8?q?=E6=9B=B4=20(#585)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * move to_hex() api * add changelog --- CHANGELOG.md | 1 + docs/primitive-props.md | 2 ++ docs/std.md | 3 --- src/interpreter/lib/std.ts | 5 ----- src/interpreter/primitive-props.ts | 4 ++++ 5 files changed, 7 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f314be8c..fafe4e93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ - **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`に移動しました。 # 未リリース分 - `Core:abort`でプログラムを緊急停止できるように diff --git a/docs/primitive-props.md b/docs/primitive-props.md index 0c9c2919..f7b9b2de 100644 --- a/docs/primitive-props.md +++ b/docs/primitive-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 diff --git a/docs/std.md b/docs/std.md index 4e888add..68b23502 100644 --- a/docs/std.md +++ b/docs/std.md @@ -99,9 +99,6 @@ _date_ を渡した場合、_date_に対応するミリ秒、 数が多いため専用のページになっています。→[std-math.md](std-math.md) ## :: Num -### @Num:to_hex(_x_: num): str -数値から16進数の文字列を生成します。 - ### @Num:from_hex(_hex_: str): num 16進数の文字列から数値を生成します。 diff --git a/src/interpreter/lib/std.ts b/src/interpreter/lib/std.ts index 469cba07..93d34aee 100644 --- a/src/interpreter/lib/std.ts +++ b/src/interpreter/lib/std.ts @@ -439,11 +439,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 911313ac..395c8e27 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: { From 7744b347bb16579dd67a18909f370af74239d09e Mon Sep 17 00:00:00 2001 From: FineArchs <133759614+FineArchs@users.noreply.github.com> Date: Tue, 12 Mar 2024 21:16:02 +0900 Subject: [PATCH 35/62] =?UTF-8?q?#585=20=E3=81=AB=E3=83=86=E3=82=B9?= =?UTF-8?q?=E3=83=88=E8=BF=BD=E5=8A=A0=20(#586)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * move to_hex() api * add changelog * add test * fix test * Update index.ts --------- Co-authored-by: marihachi --- test/index.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/test/index.ts b/test/index.ts index 34e3c457..20ac3998 100644 --- a/test/index.ts +++ b/test/index.ts @@ -2676,6 +2676,21 @@ describe('primitive props', () => { `); 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', () => { From cb51d24369d370755585fd592432e3ea479feeb4 Mon Sep 17 00:00:00 2001 From: FineArchs <133759614+FineArchs@users.noreply.github.com> Date: Wed, 13 Mar 2024 14:29:33 +0900 Subject: [PATCH 36/62] fix test (#594) --- test/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/index.ts b/test/index.ts index 20ac3998..00704bb3 100644 --- a/test/index.ts +++ b/test/index.ts @@ -3163,7 +3163,7 @@ describe('std', () => { test.concurrent('abort', async () => { assert.rejects( exe('Core:abort("hoge")'), - { name: '', message: 'hoge' }, + e => e.message.includes('hoge'), ); }); }); From eafd83c59e7f8b9995ab94c8f7cbc9cf2018f116 Mon Sep 17 00:00:00 2001 From: marihachi Date: Thu, 14 Mar 2024 21:45:53 +0900 Subject: [PATCH 37/62] =?UTF-8?q?Next:=20=E6=94=B9=E8=A1=8C=E3=81=A7?= =?UTF-8?q?=E3=81=8D=E3=82=8B=E7=AE=87=E6=89=80=E3=81=AE=E8=BF=BD=E5=8A=A0?= =?UTF-8?q?=E5=AE=9F=E8=A3=85=20(#590)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * improve way to get token kind. * improve: object literal, array literal * add tests * fix getKind --- src/parser/scanner.ts | 6 +-- src/parser/streams/token-stream.ts | 8 +-- src/parser/syntaxes/common.ts | 26 ++++----- src/parser/syntaxes/expressions.ts | 84 ++++++++++++++---------------- src/parser/syntaxes/statements.ts | 36 ++++++------- src/parser/syntaxes/toplevel.ts | 22 ++++---- test/index.ts | 67 ++++++++++++++++++++++++ 7 files changed, 155 insertions(+), 94 deletions(-) diff --git a/src/parser/scanner.ts b/src/parser/scanner.ts index c1b07983..3b3aa6cd 100644 --- a/src/parser/scanner.ts +++ b/src/parser/scanner.ts @@ -38,7 +38,7 @@ export class Scanner implements ITokenStream { /** * カーソル位置にあるトークンの種類を取得します。 */ - public get kind(): TokenKind { + public getKind(): TokenKind { return this.token.kind; } @@ -74,8 +74,8 @@ export class Scanner implements ITokenStream { * 一致しなかった場合には文法エラーを発生させます。 */ public expect(kind: TokenKind): void { - if (this.kind !== kind) { - throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[this.kind]}`, this.token.loc); + if (this.getKind() !== kind) { + throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[this.getKind()]}`, this.token.loc); } } diff --git a/src/parser/streams/token-stream.ts b/src/parser/streams/token-stream.ts index c0fadcaa..0ef2fe23 100644 --- a/src/parser/streams/token-stream.ts +++ b/src/parser/streams/token-stream.ts @@ -14,7 +14,7 @@ export interface ITokenStream { /** * カーソル位置にあるトークンの種類を取得します。 */ - get kind(): TokenKind; + getKind(): TokenKind; /** * カーソル位置を次のトークンへ進めます。 @@ -70,7 +70,7 @@ export class TokenStream implements ITokenStream { /** * カーソル位置にあるトークンの種類を取得します。 */ - public get kind(): TokenKind { + public getKind(): TokenKind { return this.token.kind; } @@ -100,8 +100,8 @@ export class TokenStream implements ITokenStream { * 一致しなかった場合には文法エラーを発生させます。 */ public expect(kind: TokenKind): void { - if (this.kind !== kind) { - throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[this.kind]}`, this.token.loc); + if (this.getKind() !== kind) { + throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[this.getKind()]}`, this.token.loc); } } diff --git a/src/parser/syntaxes/common.ts b/src/parser/syntaxes/common.ts index b4bd9dc0..9f32c970 100644 --- a/src/parser/syntaxes/common.ts +++ b/src/parser/syntaxes/common.ts @@ -16,17 +16,17 @@ export function parseParams(s: ITokenStream): { name: string, argType?: Ast.Node s.nextWith(TokenKind.OpenParen); - if (s.kind === TokenKind.NewLine) { + if (s.getKind() === TokenKind.NewLine) { s.next(); } - while (s.kind !== TokenKind.CloseParen) { + while (s.getKind() !== TokenKind.CloseParen) { s.expect(TokenKind.Identifier); const name = s.token.value!; s.next(); let type; - if ((s.kind as TokenKind) === TokenKind.Colon) { + if (s.getKind() === TokenKind.Colon) { s.next(); type = parseType(s); } @@ -34,14 +34,14 @@ export function parseParams(s: ITokenStream): { name: string, argType?: Ast.Node items.push({ name, argType: type }); // separator - switch (s.kind as TokenKind) { + switch (s.getKind()) { case TokenKind.NewLine: { s.next(); break; } case TokenKind.Comma: { s.next(); - if (s.kind === TokenKind.NewLine) { + if (s.getKind() === TokenKind.NewLine) { s.next(); } break; @@ -68,19 +68,19 @@ export function parseParams(s: ITokenStream): { name: string, argType?: Ast.Node export function parseBlock(s: ITokenStream): Ast.Node[] { s.nextWith(TokenKind.OpenBrace); - while (s.kind === TokenKind.NewLine) { + while (s.getKind() === TokenKind.NewLine) { s.next(); } const steps: Ast.Node[] = []; - while (s.kind !== TokenKind.CloseBrace) { + while (s.getKind() !== TokenKind.CloseBrace) { steps.push(parseStatement(s)); // terminator - switch (s.kind as TokenKind) { + switch (s.getKind()) { case TokenKind.NewLine: case TokenKind.SemiColon: { - while ([TokenKind.NewLine, TokenKind.SemiColon].includes(s.kind)) { + while ([TokenKind.NewLine, TokenKind.SemiColon].includes(s.getKind())) { s.next(); } break; @@ -102,7 +102,7 @@ export function parseBlock(s: ITokenStream): Ast.Node[] { //#region Type export function parseType(s: ITokenStream): Ast.Node { - if (s.kind === TokenKind.At) { + if (s.getKind() === TokenKind.At) { return parseFnType(s); } else { return parseNamedType(s); @@ -122,9 +122,9 @@ function parseFnType(s: ITokenStream): Ast.Node { s.nextWith(TokenKind.OpenParen); const params: Ast.Node[] = []; - while (s.kind !== TokenKind.CloseParen) { + while (s.getKind() !== TokenKind.CloseParen) { if (params.length > 0) { - switch (s.kind as TokenKind) { + switch (s.getKind()) { case TokenKind.Comma: { s.next(); break; @@ -160,7 +160,7 @@ function parseNamedType(s: ITokenStream): Ast.Node { // inner type let inner = null; - if (s.kind === TokenKind.Lt) { + if (s.getKind() === TokenKind.Lt) { s.next(); inner = parseType(s); s.nextWith(TokenKind.Gt); diff --git a/src/parser/syntaxes/expressions.ts b/src/parser/syntaxes/expressions.ts index 3adda1bd..439b8fc1 100644 --- a/src/parser/syntaxes/expressions.ts +++ b/src/parser/syntaxes/expressions.ts @@ -53,11 +53,11 @@ const operators: OpInfo[] = [ function parsePrefix(s: ITokenStream, minBp: number): Ast.Node { const loc = s.token.loc; - const op = s.kind; + const op = s.getKind(); s.next(); // 改行のエスケープ - if (s.kind === TokenKind.BackSlash) { + if (s.getKind() === TokenKind.BackSlash) { s.next(); s.nextWith(TokenKind.NewLine); } @@ -96,11 +96,11 @@ function parsePrefix(s: ITokenStream, minBp: number): Ast.Node { function parseInfix(s: ITokenStream, left: Ast.Node, minBp: number): Ast.Node { const loc = s.token.loc; - const op = s.kind; + const op = s.getKind(); s.next(); // 改行のエスケープ - if (s.kind === TokenKind.BackSlash) { + if (s.getKind() === TokenKind.BackSlash) { s.next(); s.nextWith(TokenKind.NewLine); } @@ -169,7 +169,7 @@ function parseInfix(s: ITokenStream, left: Ast.Node, minBp: number): Ast.Node { function parsePostfix(s: ITokenStream, expr: Ast.Node): Ast.Node { const loc = s.token.loc; - const op = s.kind; + const op = s.getKind(); switch (op) { case TokenKind.OpenParen: { @@ -194,7 +194,7 @@ function parsePostfix(s: ITokenStream, expr: Ast.Node): Ast.Node { function parseAtom(s: ITokenStream, isStatic: boolean): Ast.Node { const loc = s.token.loc; - switch (s.kind) { + switch (s.getKind()) { case TokenKind.IfKeyword: { if (isStatic) break; return parseIf(s); @@ -230,7 +230,7 @@ function parseAtom(s: ITokenStream, isStatic: boolean): Ast.Node { // スキャナで埋め込み式として事前に読み取っておいたトークン列をパースする const exprStream = new TokenStream(element.children!); const expr = parseExpr(exprStream, false); - if (exprStream.kind !== TokenKind.EOF) { + if (exprStream.getKind() !== TokenKind.EOF) { throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[exprStream.token.kind]}`, exprStream.token.loc); } values.push(expr); @@ -258,7 +258,7 @@ function parseAtom(s: ITokenStream, isStatic: boolean): Ast.Node { } case TokenKind.TrueKeyword: case TokenKind.FalseKeyword: { - const value = (s.kind === TokenKind.TrueKeyword); + const value = (s.getKind() === TokenKind.TrueKeyword); s.next(); return NODE('bool', { value }, loc); } @@ -283,7 +283,7 @@ function parseAtom(s: ITokenStream, isStatic: boolean): Ast.Node { return expr; } } - throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[s.kind]}`, loc); + throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[s.getKind()]}`, loc); } /** @@ -295,22 +295,22 @@ function parseCall(s: ITokenStream, target: Ast.Node): Ast.Node { s.nextWith(TokenKind.OpenParen); - if (s.kind === TokenKind.NewLine) { + if (s.getKind() === TokenKind.NewLine) { s.next(); } - while (s.kind !== TokenKind.CloseParen) { + while (s.getKind() !== TokenKind.CloseParen) { items.push(parseExpr(s, false)); // separator - switch (s.kind as TokenKind) { + switch (s.getKind()) { case TokenKind.NewLine: { s.next(); break; } case TokenKind.Comma: { s.next(); - if (s.kind === TokenKind.NewLine) { + if (s.getKind() === TokenKind.NewLine) { s.next(); } break; @@ -344,23 +344,23 @@ function parseIf(s: ITokenStream): Ast.Node { const cond = parseExpr(s, false); const then = parseBlockOrStatement(s); - if (s.kind === TokenKind.NewLine && [TokenKind.ElifKeyword, TokenKind.ElseKeyword].includes(s.lookahead(1).kind)) { + 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.kind === TokenKind.ElifKeyword) { + while (s.getKind() === TokenKind.ElifKeyword) { s.next(); const elifCond = parseExpr(s, false); const elifThen = parseBlockOrStatement(s); - if ((s.kind as TokenKind) === TokenKind.NewLine && [TokenKind.ElifKeyword, TokenKind.ElseKeyword].includes(s.lookahead(1).kind)) { + 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.kind === TokenKind.ElseKeyword) { + if (s.getKind() === TokenKind.ElseKeyword) { s.next(); _else = parseBlockOrStatement(s); } @@ -381,7 +381,7 @@ function parseFnExpr(s: ITokenStream): Ast.Node { const params = parseParams(s); let type; - if ((s.kind as TokenKind) === TokenKind.Colon) { + if ((s.getKind()) === TokenKind.Colon) { s.next(); type = parseType(s); } @@ -405,12 +405,12 @@ function parseMatch(s: ITokenStream): Ast.Node { s.nextWith(TokenKind.OpenBrace); - if (s.kind === TokenKind.NewLine) { + if (s.getKind() === TokenKind.NewLine) { s.next(); } const qs: { q: Ast.Node, a: Ast.Node }[] = []; - while (s.kind !== TokenKind.DefaultKeyword && s.kind !== TokenKind.CloseBrace) { + while (s.getKind() !== TokenKind.DefaultKeyword && s.getKind() !== TokenKind.CloseBrace) { s.nextWith(TokenKind.CaseKeyword); const q = parseExpr(s, false); s.nextWith(TokenKind.Arrow); @@ -418,14 +418,14 @@ function parseMatch(s: ITokenStream): Ast.Node { qs.push({ q, a }); // separator - switch (s.kind as TokenKind) { + switch (s.getKind()) { case TokenKind.NewLine: { s.next(); break; } case TokenKind.Comma: { s.next(); - if (s.kind === TokenKind.NewLine) { + if (s.getKind() === TokenKind.NewLine) { s.next(); } break; @@ -441,20 +441,20 @@ function parseMatch(s: ITokenStream): Ast.Node { } let x; - if (s.kind === TokenKind.DefaultKeyword) { + if (s.getKind() === TokenKind.DefaultKeyword) { s.next(); s.nextWith(TokenKind.Arrow); x = parseBlockOrStatement(s); // separator - switch (s.kind as TokenKind) { + switch (s.getKind()) { case TokenKind.NewLine: { s.next(); break; } case TokenKind.Comma: { s.next(); - if ((s.kind as TokenKind) === TokenKind.NewLine) { + if ((s.getKind()) === TokenKind.NewLine) { s.next(); } break; @@ -510,7 +510,7 @@ function parseReference(s: ITokenStream): Ast.Node { const segs: string[] = []; while (true) { if (segs.length > 0) { - if (s.kind === TokenKind.Colon) { + if (s.getKind() === TokenKind.Colon) { if (s.token.hasLeftSpacing) { throw new AiScriptSyntaxError('Cannot use spaces in a reference.', s.token.loc); } @@ -539,12 +539,12 @@ function parseObject(s: ITokenStream, isStatic: boolean): Ast.Node { s.nextWith(TokenKind.OpenBrace); - if (s.kind === TokenKind.NewLine) { + while (s.getKind() === TokenKind.NewLine) { s.next(); } const map = new Map(); - while (s.kind !== TokenKind.CloseBrace) { + while (s.getKind() !== TokenKind.CloseBrace) { s.expect(TokenKind.Identifier); const k = s.token.value!; s.next(); @@ -556,14 +556,11 @@ function parseObject(s: ITokenStream, isStatic: boolean): Ast.Node { map.set(k, v); // separator - switch (s.kind as TokenKind) { - case TokenKind.NewLine: { - s.next(); - break; - } + switch (s.getKind()) { + case TokenKind.NewLine: case TokenKind.Comma: { s.next(); - if (s.kind === TokenKind.NewLine) { + while (s.getKind() === TokenKind.NewLine) { s.next(); } break; @@ -592,23 +589,20 @@ function parseArray(s: ITokenStream, isStatic: boolean): Ast.Node { s.nextWith(TokenKind.OpenBracket); - if (s.kind === TokenKind.NewLine) { + while (s.getKind() === TokenKind.NewLine) { s.next(); } const value = []; - while (s.kind !== TokenKind.CloseBracket) { + while (s.getKind() !== TokenKind.CloseBracket) { value.push(parseExpr(s, isStatic)); // separator - switch (s.kind as TokenKind) { - case TokenKind.NewLine: { - s.next(); - break; - } + switch (s.getKind()) { + case TokenKind.NewLine: case TokenKind.Comma: { s.next(); - if (s.kind === TokenKind.NewLine) { + while (s.getKind() === TokenKind.NewLine) { s.next(); } break; @@ -640,7 +634,7 @@ function parsePratt(s: ITokenStream, minBp: number): Ast.Node { let left: Ast.Node; - const tokenKind = s.kind; + 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); @@ -650,12 +644,12 @@ function parsePratt(s: ITokenStream, minBp: number): Ast.Node { while (true) { // 改行のエスケープ - if (s.kind === TokenKind.BackSlash) { + if (s.getKind() === TokenKind.BackSlash) { s.next(); s.nextWith(TokenKind.NewLine); } - const tokenKind = s.kind; + const tokenKind = s.getKind(); const postfix = operators.find((x): x is PostfixInfo => x.opKind === 'postfix' && x.kind === tokenKind); if (postfix != null) { diff --git a/src/parser/syntaxes/statements.ts b/src/parser/syntaxes/statements.ts index 4c816a5b..717ad579 100644 --- a/src/parser/syntaxes/statements.ts +++ b/src/parser/syntaxes/statements.ts @@ -16,7 +16,7 @@ import type { ITokenStream } from '../streams/token-stream.js'; export function parseStatement(s: ITokenStream): Ast.Node { const loc = s.token.loc; - switch (s.kind) { + switch (s.getKind()) { case TokenKind.VarKeyword: case TokenKind.LetKeyword: { return parseVarDef(s); @@ -63,7 +63,7 @@ export function parseStatement(s: ITokenStream): Ast.Node { } export function parseDefStatement(s: ITokenStream): Ast.Node { - switch (s.kind) { + switch (s.getKind()) { case TokenKind.VarKeyword: case TokenKind.LetKeyword: { return parseVarDef(s); @@ -72,7 +72,7 @@ export function parseDefStatement(s: ITokenStream): Ast.Node { return parseFnDef(s); } default: { - throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[s.kind]}`, s.token.loc); + throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[s.getKind()]}`, s.token.loc); } } } @@ -85,7 +85,7 @@ export function parseDefStatement(s: ITokenStream): Ast.Node { export function parseBlockOrStatement(s: ITokenStream): Ast.Node { const loc = s.token.loc; - if (s.kind === TokenKind.OpenBrace) { + if (s.getKind() === TokenKind.OpenBrace) { const statements = parseBlock(s); return NODE('block', { statements }, loc); } else { @@ -102,7 +102,7 @@ function parseVarDef(s: ITokenStream): Ast.Node { const loc = s.token.loc; let mut; - switch (s.kind) { + switch (s.getKind()) { case TokenKind.LetKeyword: { mut = false; break; @@ -112,7 +112,7 @@ function parseVarDef(s: ITokenStream): Ast.Node { break; } default: { - throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[s.kind]}`, s.token.loc); + throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[s.getKind()]}`, s.token.loc); } } s.next(); @@ -122,14 +122,14 @@ function parseVarDef(s: ITokenStream): Ast.Node { s.next(); let type; - if ((s.kind as TokenKind) === TokenKind.Colon) { + if (s.getKind() === TokenKind.Colon) { s.next(); type = parseType(s); } s.nextWith(TokenKind.Eq); - if ((s.kind as TokenKind) === TokenKind.NewLine) { + if (s.getKind() === TokenKind.NewLine) { s.next(); } @@ -155,7 +155,7 @@ function parseFnDef(s: ITokenStream): Ast.Node { const params = parseParams(s); let type; - if ((s.kind as TokenKind) === TokenKind.Colon) { + if (s.getKind() === TokenKind.Colon) { s.next(); type = parseType(s); } @@ -199,7 +199,7 @@ function parseEach(s: ITokenStream): Ast.Node { s.nextWith(TokenKind.EachKeyword); - if (s.kind === TokenKind.OpenParen) { + if (s.getKind() === TokenKind.OpenParen) { hasParen = true; s.next(); } @@ -210,7 +210,7 @@ function parseEach(s: ITokenStream): Ast.Node { const name = s.token.value!; s.next(); - if (s.kind === TokenKind.Comma) { + if (s.getKind() === TokenKind.Comma) { s.next(); } else { throw new AiScriptSyntaxError('separator expected', s.token.loc); @@ -237,12 +237,12 @@ function parseFor(s: ITokenStream): Ast.Node { s.nextWith(TokenKind.ForKeyword); - if (s.kind === TokenKind.OpenParen) { + if (s.getKind() === TokenKind.OpenParen) { hasParen = true; s.next(); } - if (s.kind === TokenKind.LetKeyword) { + if (s.getKind() === TokenKind.LetKeyword) { // range syntax s.next(); @@ -253,14 +253,14 @@ function parseFor(s: ITokenStream): Ast.Node { s.next(); let _from; - if ((s.kind as TokenKind) === TokenKind.Eq) { + if (s.getKind() === TokenKind.Eq) { s.next(); _from = parseExpr(s, false); } else { _from = NODE('num', { value: 0 }, identLoc); } - if ((s.kind as TokenKind) === TokenKind.Comma) { + if (s.getKind() === TokenKind.Comma) { s.next(); } else { throw new AiScriptSyntaxError('separator expected', s.token.loc); @@ -318,7 +318,7 @@ function parseReturn(s: ITokenStream): Ast.Node { */ function parseStatementWithAttr(s: ITokenStream): Ast.Node { const attrs: Ast.Attribute[] = []; - while (s.kind === TokenKind.OpenSharpBracket) { + while (s.getKind() === TokenKind.OpenSharpBracket) { attrs.push(parseAttr(s) as Ast.Attribute); s.nextWith(TokenKind.NewLine); } @@ -352,7 +352,7 @@ function parseAttr(s: ITokenStream): Ast.Node { s.next(); let value; - if (s.kind !== TokenKind.CloseBracket) { + if (s.getKind() !== TokenKind.CloseBracket) { value = parseExpr(s, true); } else { value = NODE('bool', { value: true }, loc); @@ -385,7 +385,7 @@ function tryParseAssign(s: ITokenStream, dest: Ast.Node): Ast.Node | undefined { const loc = s.token.loc; // Assign - switch (s.kind) { + switch (s.getKind()) { case TokenKind.Eq: { s.next(); const expr = parseExpr(s, false); diff --git a/src/parser/syntaxes/toplevel.ts b/src/parser/syntaxes/toplevel.ts index c0969e65..ebd32d28 100644 --- a/src/parser/syntaxes/toplevel.ts +++ b/src/parser/syntaxes/toplevel.ts @@ -15,12 +15,12 @@ import type { ITokenStream } from '../streams/token-stream.js'; export function parseTopLevel(s: ITokenStream): Ast.Node[] { const nodes: Ast.Node[] = []; - while (s.kind === TokenKind.NewLine) { + while (s.getKind() === TokenKind.NewLine) { s.next(); } - while (s.kind !== TokenKind.EOF) { - switch (s.kind) { + while (s.getKind() !== TokenKind.EOF) { + switch (s.getKind()) { case TokenKind.Colon2: { nodes.push(parseNamespace(s)); break; @@ -36,10 +36,10 @@ export function parseTopLevel(s: ITokenStream): Ast.Node[] { } // terminator - switch (s.kind as TokenKind) { + switch (s.getKind()) { case TokenKind.NewLine: case TokenKind.SemiColon: { - while ([TokenKind.NewLine, TokenKind.SemiColon].includes(s.kind)) { + while ([TokenKind.NewLine, TokenKind.SemiColon].includes(s.getKind())) { s.next(); } break; @@ -73,12 +73,12 @@ export function parseNamespace(s: ITokenStream): Ast.Node { const members: Ast.Node[] = []; s.nextWith(TokenKind.OpenBrace); - while (s.kind === TokenKind.NewLine) { + while (s.getKind() === TokenKind.NewLine) { s.next(); } - while (s.kind !== TokenKind.CloseBrace) { - switch (s.kind) { + while (s.getKind() !== TokenKind.CloseBrace) { + switch (s.getKind()) { case TokenKind.VarKeyword: case TokenKind.LetKeyword: case TokenKind.At: { @@ -92,10 +92,10 @@ export function parseNamespace(s: ITokenStream): Ast.Node { } // terminator - switch (s.kind as TokenKind) { + switch (s.getKind()) { case TokenKind.NewLine: case TokenKind.SemiColon: { - while ([TokenKind.NewLine, TokenKind.SemiColon].includes(s.kind)) { + while ([TokenKind.NewLine, TokenKind.SemiColon].includes(s.getKind())) { s.next(); } break; @@ -124,7 +124,7 @@ export function parseMeta(s: ITokenStream): Ast.Node { s.nextWith(TokenKind.Sharp3); let name = null; - if (s.kind === TokenKind.Identifier) { + if (s.getKind() === TokenKind.Identifier) { name = s.token.value!; s.next(); } diff --git a/test/index.ts b/test/index.ts index 00704bb3..910a5673 100644 --- a/test/index.ts +++ b/test/index.ts @@ -674,6 +674,20 @@ describe('separator', () => { 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 = { @@ -714,6 +728,20 @@ describe('separator', () => { 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 = [ @@ -725,6 +753,45 @@ describe('separator', () => { 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] From 76343b37d86356bf2765379c44feea775e93234d Mon Sep 17 00:00:00 2001 From: FineArchs <133759614+FineArchs@users.noreply.github.com> Date: Mon, 1 Apr 2024 16:52:31 +0900 Subject: [PATCH 38/62] use for...of .keys() --- src/parser/visit.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/parser/visit.ts b/src/parser/visit.ts index 861e8caf..17d11f0f 100644 --- a/src/parser/visit.ts +++ b/src/parser/visit.ts @@ -61,7 +61,7 @@ export function visitNode(node: Ast.Node, fn: (node: Ast.Node) => Ast.Node): Ast break; } case 'fn': { - for (const i in result.args) { + 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']; } From c8ad88fa2524c9f98725db7b4469a871048f8d7b Mon Sep 17 00:00:00 2001 From: FineArchs <133759614+FineArchs@users.noreply.github.com> Date: Mon, 1 Apr 2024 16:55:02 +0900 Subject: [PATCH 39/62] use for...of .keys() --- src/interpreter/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/interpreter/index.ts b/src/interpreter/index.ts index 2513be3b..a8748c12 100644 --- a/src/interpreter/index.ts +++ b/src/interpreter/index.ts @@ -230,7 +230,7 @@ export class Interpreter { return result ?? NULL; } else { const _args = new Map(); - for (const i in fn.args) { + for (const i of fn.args.keys()) { const argdef = fn.args[i]!; if (!argdef.default) expectAny(args[i]); _args.set(argdef.name, { From 1391781523b8e68ab4611634ff4905bc6d603bad Mon Sep 17 00:00:00 2001 From: FineArchs <133759614+FineArchs@users.noreply.github.com> Date: Mon, 1 Apr 2024 17:17:20 +0900 Subject: [PATCH 40/62] Update common.ts --- src/parser/syntaxes/common.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/parser/syntaxes/common.ts b/src/parser/syntaxes/common.ts index b851194d..fe8ddf39 100644 --- a/src/parser/syntaxes/common.ts +++ b/src/parser/syntaxes/common.ts @@ -27,12 +27,12 @@ export function parseParams(s: ITokenStream): { name: string, argType?: Ast.Node s.next(); let optional = false; - if ((s.kind as TokenKind) === TokenKind.Question) { + if ((s.getKind() as TokenKind) === TokenKind.Question) { s.next(); optional = true; } let defaultExpr; - if ((s.kind as TokenKind) === TokenKind.Eq) { + if ((s.getKind() as TokenKind) === TokenKind.Eq) { s.next(); defaultExpr = parseExpr(s, false); } From f4e0a4b6b67d919e35fb75515a124df37bc9eddc Mon Sep 17 00:00:00 2001 From: salano_ym <53254905+salano-ym@users.noreply.github.com> Date: Fri, 10 May 2024 15:14:33 +0000 Subject: [PATCH 41/62] =?UTF-8?q?arr.sort=E3=82=92=E5=AE=89=E5=AE=9A?= =?UTF-8?q?=E3=82=BD=E3=83=BC=E3=83=88=E3=81=AB=E5=A4=89=E6=9B=B4=20#656?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 1 + docs/primitive-props.md | 1 + src/interpreter/primitive-props.ts | 2 +- test/index.ts | 17 +++++++++++++++++ 4 files changed, 20 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1dfc7315..c1fd9af2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ - **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/docs/primitive-props.md b/docs/primitive-props.md index 8bc7de86..147f1fbf 100644 --- a/docs/primitive-props.md +++ b/docs/primitive-props.md @@ -168,6 +168,7 @@ _fromIndex_が負値の時は末尾からの位置(配列の長さ+_fromIndex_ ### @(_v_: arr).sort(_comp_: @(_a_: value, _b_: value)): arr **【この操作は配列を書き換えます】** 配列の並べ替えをします。第1引数 _comp_ として次のような比較関数を渡します。 +安定ソートです。 * _a_ が _b_ より順番的に前の時、負の値を返す * _a_ が _b_ より順番的に後の時、正の値を返す * _a_ が _b_ と順番的に同等の時、0を返す diff --git a/src/interpreter/primitive-props.ts b/src/interpreter/primitive-props.ts index c374dfd8..13827fc2 100644 --- a/src/interpreter/primitive-props.ts +++ b/src/interpreter/primitive-props.ts @@ -248,7 +248,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/test/index.ts b/test/index.ts index 6f82e04f..f12de651 100644 --- a/test/index.ts +++ b/test/index.ts @@ -3214,6 +3214,23 @@ describe('primitive props', () => { `); 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(` From ed279f1a578bba72b0d523a60cc5b72866d914bb Mon Sep 17 00:00:00 2001 From: salano_ym <53254905+salano-ym@users.noreply.github.com> Date: Fri, 10 May 2024 15:21:34 +0000 Subject: [PATCH 42/62] fix CHANGELOG.md --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c1fd9af2..ededec23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,7 +39,6 @@ - 関数`Str#charcode_at` `Str#to_arr` `Str#to_char_arr` `Str#to_charcode_arr` `Str#to_utf8_byte_arr` `Str#to_unicode_codepoint_arr` `Str:from_unicode_codepoints` `Str:from_utf8_bytes`を追加 - Fix: `Str#codepoint_at`がサロゲートペアに対応していないのを修正 - 配列の範囲外および非整数のインデックスへの代入でエラーを出すように -- JavaScriptのように分割代入ができるように(現段階では機能は最小限) ## Note バージョン0.16.0に記録漏れがありました。 >- 関数`Str:from_codepoint` `Str#codepoint_at`を追加 From cab1bf6ecb396279b7c0197bdc647d3e573b5850 Mon Sep 17 00:00:00 2001 From: Fine Archs Date: Fri, 17 May 2024 12:24:10 +0900 Subject: [PATCH 43/62] add while --- src/parser/plugins/validate-keyword.ts | 1 - src/parser/scanner.ts | 6 +++ src/parser/syntaxes/statements.ts | 54 ++++++++++++++++++++++++++ src/parser/token.ts | 2 + 4 files changed, 62 insertions(+), 1 deletion(-) diff --git a/src/parser/plugins/validate-keyword.ts b/src/parser/plugins/validate-keyword.ts index a179aba7..7a16aaac 100644 --- a/src/parser/plugins/validate-keyword.ts +++ b/src/parser/plugins/validate-keyword.ts @@ -44,7 +44,6 @@ const reservedWord = [ 'use', 'using', 'when', - 'while', 'yield', 'import', 'is', diff --git a/src/parser/scanner.ts b/src/parser/scanner.ts index 3b3aa6cd..1bba9fa7 100644 --- a/src/parser/scanner.ts +++ b/src/parser/scanner.ts @@ -367,6 +367,12 @@ export class Scanner implements ITokenStream { case 'loop': { return TOKEN(TokenKind.LoopKeyword, loc, { hasLeftSpacing }); } + case 'do': { + return TOKEN(TokenKind.DoKeyword, loc, { hasLeftSpacing }); + } + case 'while': { + return TOKEN(TokenKind.WhileKeyword, loc, { hasLeftSpacing }); + } case 'break': { return TOKEN(TokenKind.BreakKeyword, loc, { hasLeftSpacing }); } diff --git a/src/parser/syntaxes/statements.ts b/src/parser/syntaxes/statements.ts index 717ad579..d60b8d5b 100644 --- a/src/parser/syntaxes/statements.ts +++ b/src/parser/syntaxes/statements.ts @@ -45,6 +45,12 @@ export function parseStatement(s: ITokenStream): Ast.Node { 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', {}, loc); @@ -376,6 +382,54 @@ function parseLoop(s: ITokenStream): Ast.Node { return NODE('loop', { statements }, loc); } +/** + * ```abnf + * Loop = "do" BlockOrStatement "while" Expr + * ``` +*/ +function parseDoWhile(s: ITokenStream): Ast.Node { + const doLoc = s.token.loc; + s.nextWith(TokenKind.DoKeyword); + const body = parseBlockOrStatement(s); + const whileLoc = s.token.loc; + s.nextWith(TokenKind.WhileKeyword); + const cond = parseExpr(s, false); + + return NODE('loop', { + statements: [ + body, + NODE('if', { + cond: NODE('not', { expr: cond }, whileLoc), + then: NODE('break', {}, whileLoc), + elseif: [], + }, whileLoc), + ], + }, doLoc); +} + +/** + * ```abnf + * Loop = "while" Expr BlockOrStatement + * ``` +*/ +function parseWhile(s: ITokenStream): Ast.Node { + const loc = s.token.loc; + s.nextWith(TokenKind.WhileKeyword); + const cond = parseExpr(s, false); + const body = parseBlockOrStatement(s); + + return NODE('loop', { + statements: [ + NODE('if', { + cond: NODE('not', { expr: cond }, loc), + then: NODE('break', {}, loc), + elseif: [], + }, loc), + body, + ], + }, loc); +} + /** * ```abnf * Assign = Expr ("=" / "+=" / "-=") Expr diff --git a/src/parser/token.ts b/src/parser/token.ts index 67aca6b6..c07030c7 100644 --- a/src/parser/token.ts +++ b/src/parser/token.ts @@ -19,6 +19,8 @@ export enum TokenKind { EachKeyword, ForKeyword, LoopKeyword, + DoKeyword, + WhileKeyword, BreakKeyword, ContinueKeyword, MatchKeyword, From 8f38f9bfae1f02fff3a17b5a2f116a54fe716619 Mon Sep 17 00:00:00 2001 From: FineArchs <133759614+FineArchs@users.noreply.github.com> Date: Fri, 17 May 2024 12:29:29 +0900 Subject: [PATCH 44/62] =?UTF-8?q?=E6=B6=88=E3=81=97=E5=BF=98=E3=82=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/parser/plugins/validate-keyword.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/parser/plugins/validate-keyword.ts b/src/parser/plugins/validate-keyword.ts index 7a16aaac..97b1a3cf 100644 --- a/src/parser/plugins/validate-keyword.ts +++ b/src/parser/plugins/validate-keyword.ts @@ -19,7 +19,6 @@ const reservedWord = [ 'constructor', // 'def', 'dictionary', - 'do', 'enum', 'export', 'finally', From 8fe342e3e8050777a36f983855e756a7eb299ec6 Mon Sep 17 00:00:00 2001 From: FineArchs <133759614+FineArchs@users.noreply.github.com> Date: Sat, 18 May 2024 15:57:41 +0900 Subject: [PATCH 45/62] Create while.md --- unreleased/while.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 unreleased/while.md 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文を追加 From bff6cc7da00595dc5186af4bbc1021c09f90ab8b Mon Sep 17 00:00:00 2001 From: FineArchs <133759614+FineArchs@users.noreply.github.com> Date: Mon, 20 May 2024 08:05:29 +0900 Subject: [PATCH 46/62] Update doc --- docs/syntax.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/docs/syntax.md b/docs/syntax.md index 15b7a419..f15479ab 100644 --- a/docs/syntax.md +++ b/docs/syntax.md @@ -182,6 +182,35 @@ each let v, arr{ // Syntax Error } ``` +### while + +```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の場合 +while false { + <: 'hoge' +} // hoge +``` + ### loop `break`されるまで無制限にループを行います。 ```js From aa250a9b455bb69c8be73dfc117a343907fa464f Mon Sep 17 00:00:00 2001 From: FineArchs <133759614+FineArchs@users.noreply.github.com> Date: Mon, 20 May 2024 17:50:25 +0900 Subject: [PATCH 47/62] fix doc --- docs/syntax.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/syntax.md b/docs/syntax.md index f15479ab..e0b0ac7d 100644 --- a/docs/syntax.md +++ b/docs/syntax.md @@ -183,7 +183,8 @@ each let v, arr{ // Syntax Error ``` ### while - +条件がtrueの間ループを続けます。 +条件が最初からfalseの場合はループは実行されません。 ```js var count = 0 while count < 42 { From f5779a5f24037d514d89dcfd79f07445f79a9fec Mon Sep 17 00:00:00 2001 From: FineArchs <133759614+FineArchs@users.noreply.github.com> Date: Mon, 27 May 2024 09:45:58 +0900 Subject: [PATCH 48/62] divide test (#678) --- jest.config.cjs | 10 +- test/index.ts | 2873 ++++++++------------------------------------- test/keywords.ts | 87 ++ test/literals.ts | 189 +++ test/syntax.ts | 1471 +++++++++++++++++++++++ test/testutils.ts | 36 + 6 files changed, 2277 insertions(+), 2389 deletions(-) create mode 100644 test/keywords.ts create mode 100644 test/literals.ts create mode 100644 test/syntax.ts create mode 100644 test/testutils.ts 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/test/index.ts b/test/index.ts index 621533fe..22baee6d 100644 --- a/test/index.ts +++ b/test/index.ts @@ -7,39 +7,9 @@ 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 } from '../src/interpreter/value'; -import { AiScriptSyntaxError } from '../src/error'; -let { AiScriptRuntimeError, AiScriptIndexOutOfRangeError } = errors; - -const exe = (program: string): Promise => new Promise((ok, err) => { - const aiscript = new Interpreter({}, { - out(value) { - ok(value); - }, - maxStep: 9999, - }); - - try { - const parser = new Parser(); - const ast = parser.parse(program); - aiscript.exec(ast).catch(err); - } catch (e) { - err(e); - } -}); - -const getMeta = (program: string) => { - const parser = new Parser(); - const ast = parser.parse(program); - - const metadata = Interpreter.collectMetadata(ast); +import { AiScriptSyntaxError, AiScriptRuntimeError, AiScriptIndexOutOfRangeError } from '../src/error'; +import { exe, eq } from './testutils'; - return metadata; -}; - -const eq = (a, b) => { - assert.deepEqual(a.type, b.type); - assert.deepEqual(a.value, b.value); -}; test.concurrent('Hello, world!', async () => { const res = await exe('<: "Hello, world!"'); @@ -52,317 +22,6 @@ test.concurrent('empty script', async () => { assert.deepEqual(ast, []); }); -describe('ops', () => { - 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('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('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')); - }); -}); - test.concurrent('式にコロンがあってもオブジェクトと判定されない', async () => { const res = await exe(` <: eval { @@ -394,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" } @@ -413,2041 +64,709 @@ 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('var def in block', async () => { - try { - await exe(` - eval { - let a = 42 let b = 11 - } - `); - } catch (e) { - assert.ok(true); - return; - } - assert.fail(); - }); +test.concurrent('empty function', async () => { + const res = await exe(` + @hoge() { } + <: hoge() + `); + eq(res, NULL); }); -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)); - }); - }); +test.concurrent('empty lambda', async () => { + const res = await exe(` + let hoge = @() { } + <: hoge() + `); + eq(res, NULL); +}); - 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('lambda that returns an object', async () => { + const res = await exe(` + let hoge = @() {{}} + <: hoge() + `); + eq(res, OBJ(new Map())); +}); - test.concurrent('semi colon', async () => { - const res = await exe(` - eval{let x=1;let y=2;<:x+y} - `); - eq(res, NUM(3)); - }); +test.concurrent('Closure', async () => { + const res = await exe(` + @store(v) { + let state = v + @() { + state + } + } + let s = store("ai") + <: s() + `); + eq(res, STR('ai')); +}); - 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)); - }); - }); -}); - -test.concurrent('empty function', async () => { - const res = await exe(` - @hoge() { } - <: hoge() - `); - eq(res, NULL); -}); - -test.concurrent('empty lambda', async () => { - const res = await exe(` - let hoge = @() { } - <: hoge() - `); - eq(res, NULL); -}); - -test.concurrent('lambda that returns an object', async () => { - const res = await exe(` - let hoge = @() {{}} - <: hoge() - `); - eq(res, OBJ(new Map())); -}); - -test.concurrent('Closure', async () => { - const res = await exe(` - @store(v) { - let state = v - @() { - state - } - } - let s = store("ai") - <: s() - `); - eq(res, STR('ai')); -}); - -test.concurrent('Closure (counter)', async () => { - const res = await exe(` - @create_counter() { - var count = 0 - { - get_count: @() { count }, - count: @() { count = (count + 1) }, - } - } +test.concurrent('Closure (counter)', async () => { + const res = await exe(` + @create_counter() { + var count = 0 + { + get_count: @() { count }, + count: @() { count = (count + 1) }, + } + } let counter = create_counter() let get_count = counter.get_count - let count = counter.count - - count() - count() - count() - - <: get_count() - `); - eq(res, NUM(3)); -}); - -test.concurrent('Recursion', async () => { - const res = await exe(` - @fact(n) { - if (n == 0) { 1 } else { (fact((n - 1)) * n) } - } - - <: fact(5) - `); - eq(res, NUM(120)); -}); - -describe('Var name starts with reserved word', () => { - test.concurrent('let', async () => { - const res = await exe(` - @f() { - let letcat = "ai" - letcat - } - <: f() - `); - eq(res, STR('ai')); - }); - - test.concurrent('var', async () => { - const res = await exe(` - @f() { - let varcat = "ai" - varcat - } - <: f() - `); - eq(res, STR('ai')); - }); - - test.concurrent('return', async () => { - const res = await exe(` - @f() { - let returncat = "ai" - returncat - } - <: f() - `); - eq(res, STR('ai')); - }); - - test.concurrent('each', async () => { - const res = await exe(` - @f() { - let eachcat = "ai" - eachcat - } - <: f() - `); - eq(res, STR('ai')); - }); - - test.concurrent('for', async () => { - const res = await exe(` - @f() { - let forcat = "ai" - forcat - } - <: f() - `); - eq(res, STR('ai')); - }); - - test.concurrent('loop', 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, - } - - <: obj - `); - eq(res, NUM(42)); - }); - */ -}); - -describe('Array', () => { - test.concurrent('Array item access', async () => { - const res = await exe(` - let arr = ["ai", "chan", "kawaii"] - - <: arr[1] - `); - eq(res, STR('chan')); - }); - - test.concurrent('Array item assign', async () => { - const res = await exe(` - let arr = ["ai", "chan", "kawaii"] - - arr[1] = "taso" - - <: arr - `); - eq(res, ARR([STR('ai'), STR('taso'), STR('kawaii')])); - }); - - test.concurrent('Assign array item to out of range', async () => { - try { - await exe(` - let arr = [1, 2, 3] - - arr[3] = 4 - - <: null - `) - } catch (e) { - eq(e instanceof AiScriptIndexOutOfRangeError, false); - } - - try { - await exe(` - let arr = [1, 2, 3] - - arr[9] = 10 - - <: null - `) - } catch (e) { - eq(e instanceof AiScriptIndexOutOfRangeError, true); - } - }); - - test.concurrent('index out of range error', async () => { - try { - await exe(` - <: [42][1] - `); - } catch (e) { - assert.equal(e instanceof AiScriptIndexOutOfRangeError, true); - return; - } - assert.fail(); - }); - - test.concurrent('index out of range on assignment', async () => { - try { - await exe(` - var a = [] - a[2] = 'hoge' - `); - } catch (e) { - assert.equal(e instanceof AiScriptIndexOutOfRangeError, true); - return; - } - assert.fail(); - }); - - test.concurrent('non-integer-indexed assignment', async () => { - try { - await exe(` - var a = [] - a[6.21] = 'hoge' - `); - } catch (e) { - assert.equal(e instanceof AiScriptIndexOutOfRangeError, true); - return; - } - assert.fail(); - }); -}); - -describe('chain', () => { - test.concurrent('chain access (prop + index + call)', async () => { - const res = await exe(` - let obj = { - a: { - b: [@(name) { name }, @(str) { "chan" }, @() { "kawaii" }], - }, - } - - <: obj.a.b[0]("ai") - `); - eq(res, STR('ai')); - }); - - test.concurrent('chained assign left side (prop + index)', async () => { - const res = await exe(` - let obj = { - a: { - b: ["ai", "chan", "kawaii"], - }, - } - - obj.a.b[1] = "taso" - - <: obj - `); - eq(res, OBJ(new Map([ - ['a', OBJ(new Map([ - ['b', ARR([STR('ai'), STR('taso'), STR('kawaii')])] - ]))] - ]))); - }); - - test.concurrent('chained assign right side (prop + index + call)', async () => { - const res = await exe(` - let obj = { - a: { - b: ["ai", "chan", "kawaii"], - }, - } - - var x = null - x = obj.a.b[1] - - <: x - `); - eq(res, STR('chan')); - }); - - test.concurrent('chained inc/dec left side (index + prop)', async () => { - const res = await exe(` - let arr = [ - { - a: 1, - b: 2, - } - ] - - arr[0].a += 1 - arr[0].b -= 1 - - <: arr - `); - eq(res, ARR([ - OBJ(new Map([ - ['a', NUM(2)], - ['b', NUM(1)] - ])) - ])); - }); - - test.concurrent('chained inc/dec left side (prop + index)', async () => { - const res = await exe(` - let obj = { - a: { - b: [1, 2, 3], - }, - } - - obj.a.b[1] += 1 - obj.a.b[2] -= 1 - - <: obj - `); - eq(res, OBJ(new Map([ - ['a', OBJ(new Map([ - ['b', ARR([NUM(1), NUM(3), NUM(2)])] - ]))] - ]))); - }); - - test.concurrent('prop in def', async () => { - const res = await exe(` - let x = @() { - let obj = { - a: 1 - } - obj.a - } - - <: x() - `); - eq(res, NUM(1)); - }); - - test.concurrent('prop in return', async () => { - const res = await exe(` - let x = @() { - let obj = { - a: 1 - } - return obj.a - 2 - } - - <: x() - `); - eq(res, NUM(1)); - }); - - test.concurrent('prop in each', async () => { - const res = await exe(` - let msgs = [] - let x = { a: ["ai", "chan", "kawaii"] } - each let item, x.a { - let y = { a: item } - msgs.push([y.a, "!"].join()) - } - <: msgs - `); - eq(res, ARR([STR('ai!'), STR('chan!'), STR('kawaii!')])); - }); - - test.concurrent('prop in for', async () => { - const res = await exe(` - let x = { times: 10, count: 0 } - for (let i, x.times) { - x.count = (x.count + i) - } - <: x.count - `); - eq(res, NUM(45)); - }); - - test.concurrent('object with index', async () => { - const res = await exe(` - let ai = {a: {}}['a'] - ai['chan'] = 'kawaii' - <: ai[{a: 'chan'}['a']] - `); - eq(res, STR('kawaii')); - }); - - test.concurrent('property chain with parenthesis', async () => { - let ast = Parser.parse(` - (a.b).c - `); - const line = ast[0]; - if ( - line.type !== 'prop' || - line.target.type !== 'prop' || - line.target.target.type !== 'identifier' - ) - assert.fail(); - assert.equal(line.target.target.name, 'a'); - assert.equal(line.target.name, 'b'); - assert.equal(line.name, 'c'); - }); - - test.concurrent('index chain with parenthesis', async () => { - let ast = Parser.parse(` - (a[42]).b - `); - const line = ast[0]; - if ( - line.type !== 'prop' || - line.target.type !== 'index' || - line.target.target.type !== 'identifier' || - line.target.index.type !== 'num' - ) - assert.fail(); - assert.equal(line.target.target.name, 'a'); - assert.equal(line.target.index.value, 42); - assert.equal(line.name, 'b'); - }); - - test.concurrent('call chain with parenthesis', async () => { - let ast = Parser.parse(` - (foo(42, 57)).bar - `); - const line = ast[0]; - if ( - line.type !== 'prop' || - line.target.type !== 'call' || - line.target.target.type !== 'identifier' || - line.target.args.length !== 2 || - line.target.args[0].type !== 'num' || - line.target.args[1].type !== 'num' - ) - assert.fail(); - assert.equal(line.target.target.name, 'foo'); - assert.equal(line.target.args[0].value, 42); - assert.equal(line.target.args[1].value, 57); - assert.equal(line.name, 'bar'); - }); - - test.concurrent('longer chain with parenthesis', async () => { - let ast = Parser.parse(` - (a.b.c).d.e - `); - const line = ast[0]; - if ( - line.type !== 'prop' || - line.target.type !== 'prop' || - line.target.target.type !== 'prop' || - line.target.target.target.type !== 'prop' || - line.target.target.target.target.type !== 'identifier' - ) - assert.fail(); - assert.equal(line.target.target.target.target.name, 'a'); - assert.equal(line.target.target.target.name, 'b'); - assert.equal(line.target.target.name, 'c'); - assert.equal(line.target.name, 'd'); - assert.equal(line.name, 'e'); - }); -}); - -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(` - <: (0 / 0) - `); - } catch (e) { - assert.ok(true); - return; - } - assert.fail(); -}); - -describe('Function call', () => { - test.concurrent('without args', async () => { - const res = await exe(` - @f() { - 42 - } - <: f() - `); - eq(res, NUM(42)); - }); - - test.concurrent('with args', async () => { - const res = await exe(` - @f(x) { - x - } - <: f(42) - `); - eq(res, NUM(42)); - }); - - test.concurrent('with args (separated by comma)', async () => { - const res = await exe(` - @f(x, y) { - (x + y) - } - <: f(1, 1) - `); - eq(res, NUM(2)); - }); - - test.concurrent('std: throw AiScript error when required arg missing', async () => { - try { - await exe(` - <: Core:eq(1) - `); - } catch (e) { - assert.ok(e instanceof AiScriptRuntimeError); - return; - } - 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 () => { - const res = await exe(` - @f() { - if true { - return "ai" - } - - "pope" - } - <: f() - `); - eq(res, STR('ai')); - }); - - test.concurrent('Early return (nested)', async () => { - const res = await exe(` - @f() { - if true { - if true { - return "ai" - } - } + let count = counter.count - "pope" - } - <: f() - `); - eq(res, STR('ai')); - }); + count() + count() + count() - test.concurrent('Early return (nested) 2', async () => { - const res = await exe(` - @f() { - if true { - return "ai" - } + <: get_count() + `); + eq(res, NUM(3)); +}); - "pope" - } +test.concurrent('Recursion', async () => { + const res = await exe(` + @fact(n) { + if (n == 0) { 1 } else { (fact((n - 1)) * n) } + } - @g() { - if (f() == "ai") { - return "kawaii" - } + <: fact(5) + `); + eq(res, NUM(120)); +}); - "pope" +describe('Object', () => { + test.concurrent('property access', async () => { + const res = await exe(` + let obj = { + a: { + b: { + c: 42, + }, + }, } - <: g() + <: obj.a.b.c `); - eq(res, STR('kawaii')); + eq(res, NUM(42)); }); - test.concurrent('Early return without block', async () => { + test.concurrent('property access (fn call)', async () => { const res = await exe(` - @f() { - if true return "ai" + @f() { 42 } - "pope" + let obj = { + a: { + b: { + c: f, + }, + }, } - <: f() + + <: obj.a.b.c() `); - eq(res, STR('ai')); + eq(res, NUM(42)); }); - test.concurrent('return inside for', async () => { + test.concurrent('property assign', async () => { const res = await exe(` - @f() { - var count = 0 - for (let i, 100) { - count += 1 - if (i == 42) { - return count + let obj = { + a: 1 + b: { + c: 2 + d: { + e: 3 } } } - <: f() - `); - eq(res, NUM(43)); - }); - test.concurrent('return inside for 2', async () => { - const res = await exe(` - @f() { - for (let i, 10) { - return 1 - } - 2 - } - <: f() + obj.a = 24 + obj.b.d.e = 42 + + <: obj `); - eq(res, NUM(1)); + eq(res, OBJ(new Map([ + ['a', NUM(24)], + ['b', OBJ(new Map([ + ['c', NUM(2)], + ['d', OBJ(new Map([ + ['e', NUM(42)], + ]))], + ]))], + ]))); }); - test.concurrent('return inside loop', async () => { + /* 未実装 + test.concurrent('string key', async () => { const res = await exe(` - @f() { - var count = 0 - loop { - count += 1 - if (count == 42) { - return count - } - } + let obj = { + "藍": 42, } - <: f() + + <: obj."藍" `); eq(res, NUM(42)); }); - test.concurrent('return inside loop 2', async () => { + test.concurrent('string key including colon and period', async () => { const res = await exe(` - @f() { - loop { - return 1 - } - 2 + let obj = { + ":.:": 42, } - <: f() - `); - eq(res, NUM(1)); - }); - test.concurrent('return inside each', async () => - { - const res = await exe(` - @f() { - var count = 0 - each (let item, ["ai", "chan", "kawaii"]) { - count += 1 - if (item == "chan") { - return count - } - } - } - <: f() + <: obj.":.:" `); - eq(res, NUM(2)); + eq(res, NUM(42)); }); - test.concurrent('return inside each 2', async () => - { + test.concurrent('expression key', async () => { const res = await exe(` - @f() { - each (let item, ["ai", "chan", "kawaii"]) { - return 1 - } - 2 + let key = "藍" + + let obj = { + : 42, } - <: f() + + <: obj `); - eq(res, NUM(1)); + eq(res, NUM(42)); }); + */ }); -describe('Eval', () => { - test.concurrent('returns value', async () => { +describe('Array', () => { + test.concurrent('Array item access', async () => { const res = await exe(` - let foo = eval { - let a = 1 - let b = 2 - (a + b) - } + let arr = ["ai", "chan", "kawaii"] - <: foo + <: arr[1] `); - eq(res, NUM(3)); + eq(res, STR('chan')); }); -}); -describe('exists', () => { - test.concurrent('Basic', async () => { + test.concurrent('Array item assign', async () => { const res = await exe(` - let foo = null - <: [(exists foo), (exists bar)] - `); - eq(res, ARR([BOOL(true), BOOL(false)])); - }); -}); + let arr = ["ai", "chan", "kawaii"] -describe('if', () => { - test.concurrent('if', async () => { - const res1 = await exe(` - var msg = "ai" - if true { - msg = "kawaii" - } - <: msg - `); - eq(res1, STR('kawaii')); + arr[1] = "taso" - const res2 = await exe(` - var msg = "ai" - if false { - msg = "kawaii" - } - <: msg + <: arr `); - eq(res2, STR('ai')); + eq(res, ARR([STR('ai'), STR('taso'), STR('kawaii')])); }); - test.concurrent('else', async () => { - const res1 = await exe(` - var msg = null - if true { - msg = "ai" - } else { - msg = "kawaii" - } - <: msg - `); - eq(res1, STR('ai')); + test.concurrent('Assign array item to out of range', async () => { + try { + await exe(` + let arr = [1, 2, 3] - const res2 = await exe(` - var msg = null - if false { - msg = "ai" - } else { - msg = "kawaii" - } - <: msg - `); - eq(res2, STR('kawaii')); - }); + arr[3] = 4 - test.concurrent('elif', async () => { - const res1 = await exe(` - var msg = "bebeyo" - if false { - msg = "ai" - } elif true { - msg = "kawaii" + <: null + `) + } catch (e) { + eq(e instanceof AiScriptIndexOutOfRangeError, false); } - <: msg - `); - eq(res1, STR('kawaii')); - const res2 = await exe(` - var msg = "bebeyo" - if false { - msg = "ai" - } elif false { - msg = "kawaii" + try { + await exe(` + let arr = [1, 2, 3] + + arr[9] = 10 + + <: null + `) + } catch (e) { + eq(e instanceof AiScriptIndexOutOfRangeError, true); } - <: 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" + test.concurrent('index out of range error', async () => { + try { + await exe(` + <: [42][1] + `); + } catch (e) { + assert.equal(e instanceof AiScriptIndexOutOfRangeError, true); + return; } - <: msg - `); - eq(res1, STR('chan')); + assert.fail(); + }); - const res2 = await exe(` - var msg = null - if false { - msg = "ai" - } elif false { - msg = "chan" - } else { - msg = "kawaii" + test.concurrent('index out of range on assignment', async () => { + try { + await exe(` + var a = [] + a[2] = 'hoge' + `); + } catch (e) { + assert.equal(e instanceof AiScriptIndexOutOfRangeError, true); + return; } - <: msg - `); - eq(res2, STR('kawaii')); + assert.fail(); }); - 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')); + test.concurrent('non-integer-indexed assignment', async () => { + try { + await exe(` + var a = [] + a[6.21] = 'hoge' + `); + } catch (e) { + assert.equal(e instanceof AiScriptIndexOutOfRangeError, true); + return; + } + assert.fail(); }); }); -describe('match', () => { - test.concurrent('Basic', async () => { +describe('chain', () => { + test.concurrent('chain access (prop + index + call)', async () => { const res = await exe(` - <: match 2 { - case 1 => "a" - case 2 => "b" - case 3 => "c" + let obj = { + a: { + b: [@(name) { name }, @(str) { "chan" }, @() { "kawaii" }], + }, } - `); - 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" - } + <: obj.a.b[0]("ai") `); - eq(res, NULL); + eq(res, STR('ai')); }); - test.concurrent('With default', async () => { + test.concurrent('chained assign left side (prop + index)', async () => { const res = await exe(` - <: match 42 { - case 1 => "a" - case 2 => "b" - case 3 => "c" - default => "d" + let obj = { + a: { + b: ["ai", "chan", "kawaii"], + }, } + + obj.a.b[1] = "taso" + + <: obj `); - eq(res, STR('d')); + eq(res, OBJ(new Map([ + ['a', OBJ(new Map([ + ['b', ARR([STR('ai'), STR('taso'), STR('kawaii')])] + ]))] + ]))); }); - test.concurrent('With block', async () => { + test.concurrent('chained assign right side (prop + index + call)', async () => { const res = await exe(` - <: match 2 { - case 1 => 1 - case 2 => { - let a = 1 - let b = 2 - (a + b) - } - case 3 => 3 + let obj = { + a: { + b: ["ai", "chan", "kawaii"], + }, } + + var x = null + x = obj.a.b[1] + + <: x `); - eq(res, NUM(3)); + eq(res, STR('chan')); }); - test.concurrent('With return', async () => { + test.concurrent('chained inc/dec left side (index + prop)', async () => { const res = await exe(` - @f(x) { - match x { - case 1 => { - return "ai" - } + let arr = [ + { + a: 1, + b: 2, } - "foo" - } - <: f(1) - `); - eq(res, STR('ai')); - }); -}); + ] -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)); - }); + arr[0].a += 1 + arr[0].b -= 1 - 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 + <: arr `); - eq(res, ARR([STR('ai'), STR('kawaii')])); + eq(res, ARR([ + OBJ(new Map([ + ['a', NUM(2)], + ['b', NUM(1)] + ])) + ])); }); -}); -describe('for', () => { - test.concurrent('Basic', async () => { + test.concurrent('chained inc/dec left side (prop + index)', async () => { const res = await exe(` - var count = 0 - for (let i, 10) { - count += i + 1 + let obj = { + a: { + b: [1, 2, 3], + }, } - <: 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 + obj.a.b[1] += 1 + obj.a.b[2] -= 1 + + <: obj `); - eq(res, NUM(65)); + eq(res, OBJ(new Map([ + ['a', OBJ(new Map([ + ['b', ARR([NUM(1), NUM(3), NUM(2)])] + ]))] + ]))); }); - test.concurrent('wuthout iterator', async () => { + test.concurrent('prop in def', async () => { const res = await exe(` - var count = 0 - for (10) { - count = (count + 1) + let x = @() { + let obj = { + a: 1 + } + obj.a } - <: count + + <: x() `); - eq(res, NUM(10)); + eq(res, NUM(1)); }); - test.concurrent('without brackets', async () => { + test.concurrent('prop in return', async () => { const res = await exe(` - var count = 0 - for let i, 10 { - count = (count + i) + let x = @() { + let obj = { + a: 1 + } + return obj.a + 2 } - <: count + + <: x() `); - eq(res, NUM(45)); + eq(res, NUM(1)); }); - test.concurrent('Break', async () => { + test.concurrent('prop in each', async () => { const res = await exe(` - var count = 0 - for (let i, 20) { - if (i == 11) break - count += i + let msgs = [] + let x = { a: ["ai", "chan", "kawaii"] } + each let item, x.a { + let y = { a: item } + msgs.push([y.a, "!"].join()) } - <: count + <: msgs `); - eq(res, NUM(55)); + eq(res, ARR([STR('ai!'), STR('chan!'), STR('kawaii!')])); }); - test.concurrent('continue', async () => { + test.concurrent('prop in for', async () => { const res = await exe(` - var count = 0 - for (let i, 10) { - if (i == 5) continue - count = (count + 1) + let x = { times: 10, count: 0 } + for (let i, x.times) { + x.count = (x.count + i) } - <: count + <: x.count `); - eq(res, NUM(9)); + eq(res, NUM(45)); }); - test.concurrent('single statement', async () => { + test.concurrent('object with index', async () => { const res = await exe(` - var count = 0 - for 10 count += 1 - <: count + let ai = {a: {}}['a'] + ai['chan'] = 'kawaii' + <: ai[{a: 'chan'}['a']] `); - eq(res, NUM(10)); + eq(res, STR('kawaii')); }); - test.concurrent('var name without space', async () => { - try { - await exe(` - for (leti, 10) { - <: i - } + test.concurrent('property chain with parenthesis', async () => { + let ast = Parser.parse(` + (a.b).c `); - } 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!')])); + const line = ast[0]; + if ( + line.type !== 'prop' || + line.target.type !== 'prop' || + line.target.target.type !== 'identifier' + ) + assert.fail(); + assert.equal(line.target.target.name, 'a'); + assert.equal(line.target.name, 'b'); + assert.equal(line.name, 'c'); }); - 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('index chain with parenthesis', async () => { + let ast = Parser.parse(` + (a[42]).b + `); + const line = ast[0]; + if ( + line.type !== 'prop' || + line.target.type !== 'index' || + line.target.target.type !== 'identifier' || + line.target.index.type !== 'num' + ) + assert.fail(); + assert.equal(line.target.target.name, 'a'); + assert.equal(line.target.index.value, 42); + assert.equal(line.name, 'b'); }); - 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('call chain with parenthesis', async () => { + let ast = Parser.parse(` + (foo(42, 57)).bar + `); + const line = ast[0]; + if ( + line.type !== 'prop' || + line.target.type !== 'call' || + line.target.target.type !== 'identifier' || + line.target.args.length !== 2 || + line.target.args[0].type !== 'num' || + line.target.args[1].type !== 'num' + ) + assert.fail(); + assert.equal(line.target.target.name, 'foo'); + assert.equal(line.target.args[0].value, 42); + assert.equal(line.target.args[1].value, 57); + assert.equal(line.name, 'bar'); }); - test.concurrent('var name without space', async () => { - try { - await exe(` - each letitem, ["ai", "chan", "kawaii"] { - <: item - } + test.concurrent('longer chain with parenthesis', async () => { + let ast = Parser.parse(` + (a.b.c).d.e `); - } catch (e) { - assert.ok(true); - return; - } - assert.fail(); + const line = ast[0]; + if ( + line.type !== 'prop' || + line.target.type !== 'prop' || + line.target.target.type !== 'prop' || + line.target.target.target.type !== 'prop' || + line.target.target.target.target.type !== 'identifier' + ) + assert.fail(); + assert.equal(line.target.target.target.target.name, 'a'); + assert.equal(line.target.target.target.name, 'b'); + assert.equal(line.target.target.name, 'c'); + assert.equal(line.target.name, 'd'); + assert.equal(line.name, 'e'); }); }); -describe('not', () => { - test.concurrent('Basic', async () => { - const res = await exe(` - <: !true +test.concurrent('Throws error when divided by zero', async () => { + try { + await exe(` + <: (0 / 0) `); - eq(res, BOOL(false)); - }); + } catch (e) { + assert.ok(true); + return; + } + assert.fail(); }); -describe('namespace', () => { - test.concurrent('standard', async () => { +describe('Function call', () => { + test.concurrent('without args', async () => { const res = await exe(` - <: Foo:bar() + @f() { + 42 + } + <: f() + `); + eq(res, NUM(42)); + }); - :: Foo { - @bar() { "ai" } + test.concurrent('with args', async () => { + const res = await exe(` + @f(x) { + x } + <: f(42) `); - eq(res, STR('ai')); + eq(res, NUM(42)); }); - test.concurrent('self ref', async () => { + test.concurrent('with args (separated by comma)', async () => { const res = await exe(` - <: Foo:bar() - - :: Foo { - let ai = "kawaii" - @bar() { ai } + @f(x, y) { + (x + y) } + <: f(1, 1) `); - eq(res, STR('kawaii')); + eq(res, NUM(2)); }); - test.concurrent('cannot declare mutable variable', async () => { + test.concurrent('std: throw AiScript error when required arg missing', async () => { try { await exe(` - :: Foo { - var ai = "kawaii" - } + <: Core:eq(1) `); } catch (e) { - assert.ok(true); + assert.ok(e instanceof AiScriptRuntimeError); return; } assert.fail(); }); - test.concurrent('nested', async () => { + test.concurrent('omitted args', async () => { const res = await exe(` - <: Foo:Bar:baz() - - :: Foo { - :: Bar { - @baz() { "ai" } - } + @f(x, y) { + [x, y] } + <: f(1) `); - eq(res, STR('ai')); + eq(res, ARR([NUM(1), NULL])); }); +}); - test.concurrent('nested ref', async () => { +describe('Return', () => { + test.concurrent('Early return', async () => { const res = await exe(` - <: Foo:baz - - :: Foo { - let baz = Bar:ai - :: Bar { - let ai = "kawaii" + @f() { + if true { + return "ai" } + + "pope" } + <: f() `); - eq(res, STR('kawaii')); + eq(res, STR('ai')); }); -}); -describe('literal', () => { - test.concurrent('string (single quote)', async () => { + test.concurrent('Early return (nested)', async () => { const res = await exe(` - <: 'foo' - `); - eq(res, STR('foo')); - }); + @f() { + if true { + if true { + return "ai" + } + } - test.concurrent('string (double quote)', async () => { - const res = await exe(` - <: "foo" + "pope" + } + <: f() `); - 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\'.')); + eq(res, STR('ai')); }); - test.concurrent('bool (true)', async () => { + test.concurrent('Early return (nested) 2', async () => { const res = await exe(` - <: true - `); - eq(res, BOOL(true)); - }); + @f() { + if true { + return "ai" + } - test.concurrent('bool (false)', async () => { - const res = await exe(` - <: false - `); - eq(res, BOOL(false)); - }); + "pope" + } - test.concurrent('number (Int)', async () => { - const res = await exe(` - <: 10 - `); - eq(res, NUM(10)); - }); + @g() { + if (f() == "ai") { + return "kawaii" + } - test.concurrent('number (Float)', async () => { - const res = await exe(` - <: 0.5 - `); - eq(res, NUM(0.5)); - }); + "pope" + } - test.concurrent('arr (separated by comma)', async () => { - const res = await exe(` - <: [1, 2, 3] + <: g() `); - eq(res, ARR([NUM(1), NUM(2), NUM(3)])); + eq(res, STR('kawaii')); }); - test.concurrent('arr (separated by comma) (with trailing comma)', async () => { + test.concurrent('Early return without block', async () => { const res = await exe(` - <: [1, 2, 3,] - `); - eq(res, ARR([NUM(1), NUM(2), NUM(3)])); - }); + @f() { + if true return "ai" - test.concurrent('arr (separated by line break)', async () => { - const res = await exe(` - <: [ - 1 - 2 - 3 - ] + "pope" + } + <: f() `); - eq(res, ARR([NUM(1), NUM(2), NUM(3)])); + eq(res, STR('ai')); }); - test.concurrent('arr (separated by line break and comma)', async () => { + test.concurrent('return inside for', async () => { const res = await exe(` - <: [ - 1, - 2, - 3 - ] + @f() { + var count = 0 + for (let i, 100) { + count += 1 + if (i == 42) { + return count + } + } + } + <: f() `); - eq(res, ARR([NUM(1), NUM(2), NUM(3)])); + eq(res, NUM(43)); }); - test.concurrent('arr (separated by line break and comma) (with trailing comma)', async () => { + test.concurrent('return inside for 2', async () => { const res = await exe(` - <: [ - 1, - 2, - 3, - ] + @f() { + for (let i, 10) { + return 1 + } + 2 + } + <: f() `); - eq(res, ARR([NUM(1), NUM(2), NUM(3)])); + eq(res, NUM(1)); }); - test.concurrent('obj (separated by comma)', async () => { + test.concurrent('return inside loop', async () => { const res = await exe(` - <: { a: 1, b: 2, c: 3 } + @f() { + var count = 0 + loop { + count += 1 + if (count == 42) { + return count + } + } + } + <: f() `); - eq(res, OBJ(new Map([['a', NUM(1)], ['b', NUM(2)], ['c', NUM(3)]]))); + eq(res, NUM(42)); }); - test.concurrent('obj (separated by comma) (with trailing comma)', async () => { + test.concurrent('return inside loop 2', async () => { const res = await exe(` - <: { a: 1, b: 2, c: 3, } + @f() { + loop { + return 1 + } + 2 + } + <: f() `); - eq(res, OBJ(new Map([['a', NUM(1)], ['b', NUM(2)], ['c', NUM(3)]]))); + eq(res, NUM(1)); }); - test.concurrent('obj (separated by line break)', async () => { + test.concurrent('return inside each', async () => + { const res = await exe(` - <: { - a: 1 - b: 2 - c: 3 + @f() { + var count = 0 + each (let item, ["ai", "chan", "kawaii"]) { + count += 1 + if (item == "chan") { + return count + } + } } + <: f() `); - eq(res, OBJ(new Map([['a', NUM(1)], ['b', NUM(2)], ['c', NUM(3)]]))); + eq(res, NUM(2)); }); - test.concurrent('obj and arr (separated by line break)', async () => { + test.concurrent('return inside each 2', async () => + { const res = await exe(` - <: { - a: 1 - b: [ - 1 - 2 - 3 - ] - c: 3 + @f() { + each (let item, ["ai", "chan", "kawaii"]) { + return 1 + } + 2 } + <: f() `); - eq(res, OBJ(new Map([ - ['a', NUM(1)], - ['b', ARR([NUM(1), NUM(2), NUM(3)])], - ['c', NUM(3)] - ]))); + eq(res, NUM(1)); }); }); @@ -2477,191 +796,6 @@ describe('type declaration', () => { }); }); -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('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; @@ -2785,35 +919,6 @@ describe('Location', () => { }); }); -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 () => { diff --git a/test/keywords.ts b/test/keywords.ts new file mode 100644 index 00000000..79848c5f --- /dev/null +++ b/test/keywords.ts @@ -0,0 +1,87 @@ +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', + '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/syntax.ts b/test/syntax.ts new file mode 100644 index 00000000..eefb8b15 --- /dev/null +++ b/test/syntax.ts @@ -0,0 +1,1471 @@ +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('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); +}; + From 5160d45614f9838fef505aaa3e340cf6bc09025d Mon Sep 17 00:00:00 2001 From: FineArchs <133759614+FineArchs@users.noreply.github.com> Date: Mon, 27 May 2024 20:36:49 +0900 Subject: [PATCH 49/62] add test --- test/syntax.ts | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/test/syntax.ts b/test/syntax.ts index eefb8b15..d092591c 100644 --- a/test/syntax.ts +++ b/test/syntax.ts @@ -758,6 +758,50 @@ describe('each', () => { }); }); +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(` From de55125bf5766069fd82e440c1192f9c82883d0a Mon Sep 17 00:00:00 2001 From: FineArchs <133759614+FineArchs@users.noreply.github.com> Date: Mon, 27 May 2024 20:38:28 +0900 Subject: [PATCH 50/62] Update keywords.ts --- test/keywords.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/keywords.ts b/test/keywords.ts index 79848c5f..88f183a1 100644 --- a/test/keywords.ts +++ b/test/keywords.ts @@ -9,6 +9,8 @@ const reservedWords = [ 'false', 'each', 'for', + 'do', + 'while', 'loop', 'break', 'continue', From 6e6f22bcc8aa81f5c34113d14775961da6c65d57 Mon Sep 17 00:00:00 2001 From: FineArchs <133759614+FineArchs@users.noreply.github.com> Date: Tue, 28 May 2024 20:40:06 +0900 Subject: [PATCH 51/62] test review Co-authored-by: uzmoi --- docs/syntax.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/syntax.md b/docs/syntax.md index e0b0ac7d..9962bb74 100644 --- a/docs/syntax.md +++ b/docs/syntax.md @@ -207,9 +207,9 @@ do { } while count < 42 <: count // 42 // 条件が最初からfalseの場合 -while false { +do { <: 'hoge' -} // hoge +} while false // hoge ``` ### loop From 81bad3d0876ce60f44744a4c3ae9384a026ebf7d Mon Sep 17 00:00:00 2001 From: Fine Archs Date: Tue, 4 Jun 2024 16:29:40 +0900 Subject: [PATCH 52/62] test conflict --- test/index.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/test/index.ts b/test/index.ts index 5160b336..6b441ffd 100644 --- a/test/index.ts +++ b/test/index.ts @@ -635,16 +635,6 @@ 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', () => { From 2144420043ebca405349dc4a215651bfe2e5174b Mon Sep 17 00:00:00 2001 From: Fine Archs Date: Thu, 6 Jun 2024 06:30:45 +0900 Subject: [PATCH 53/62] forbid ?= --- docs/syntax.md | 2 ++ src/parser/syntaxes/common.ts | 5 ++--- test/index.ts | 12 ++++++++++++ 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/docs/syntax.md b/docs/syntax.md index 4ec904f5..4d491f1d 100644 --- a/docs/syntax.md +++ b/docs/syntax.md @@ -109,6 +109,8 @@ var func = null @func() { // Runtime Error 'hoge' } +// 省略可能引数構文と初期値構文は併用できない +@func(a? = 1) {} // Syntax Error ``` ### 代入 diff --git a/src/parser/syntaxes/common.ts b/src/parser/syntaxes/common.ts index fe8ddf39..f185a158 100644 --- a/src/parser/syntaxes/common.ts +++ b/src/parser/syntaxes/common.ts @@ -27,12 +27,11 @@ export function parseParams(s: ITokenStream): { name: string, argType?: Ast.Node s.next(); let optional = false; + let defaultExpr; if ((s.getKind() as TokenKind) === TokenKind.Question) { s.next(); optional = true; - } - let defaultExpr; - if ((s.getKind() as TokenKind) === TokenKind.Eq) { + } else if ((s.getKind() as TokenKind) === TokenKind.Eq) { s.next(); defaultExpr = parseExpr(s, false); } diff --git a/test/index.ts b/test/index.ts index 6b441ffd..9cb52495 100644 --- a/test/index.ts +++ b/test/index.ts @@ -611,6 +611,18 @@ describe('Function call', () => { 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(` From e7759cce56b18a13402d8a85e55fd9fd100a350e Mon Sep 17 00:00:00 2001 From: FineArchs <133759614+FineArchs@users.noreply.github.com> Date: Wed, 10 Jul 2024 18:40:40 +0900 Subject: [PATCH 54/62] Update literals.md (#703) --- docs/literals.md | 41 ++++++++++++++++++++++++++--------------- 1 file changed, 26 insertions(+), 15 deletions(-) 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 ``` ### 関数 From 6634788b4fe33699e65e8fd43a88b0b6eeaa269b Mon Sep 17 00:00:00 2001 From: FineArchs <133759614+FineArchs@users.noreply.github.com> Date: Tue, 16 Jul 2024 22:47:16 +0900 Subject: [PATCH 55/62] divide test (#721) --- test/index.ts | 1226 +-------------------------------------- test/primitive-props.ts | 805 +++++++++++++++++++++++++ test/std.ts | 423 ++++++++++++++ 3 files changed, 1230 insertions(+), 1224 deletions(-) create mode 100644 test/primitive-props.ts create mode 100644 test/std.ts diff --git a/test/index.ts b/test/index.ts index ec29047d..b80a6c2a 100644 --- a/test/index.ts +++ b/test/index.ts @@ -4,8 +4,8 @@ */ import * as assert from 'assert'; -import { expect, test } from '@jest/globals'; -import { Parser, Interpreter, utils, errors, Ast } from '../src'; +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'; @@ -946,1228 +946,6 @@ describe('Location', () => { }); }); -describe('primitive props', () => { - 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), - ])); - }); - }); -}); - -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")'), - 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")); - }); - }); -}); - describe('Unicode', () => { test.concurrent('len', async () => { const res = await exe(` 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")); + }); +}); From e329a42facd64bad0c383a06e8889be231d9c711 Mon Sep 17 00:00:00 2001 From: uzmoi Date: Fri, 19 Jul 2024 21:22:03 +0900 Subject: [PATCH 56/62] =?UTF-8?q?Loc=E3=81=AB=E3=83=8E=E3=83=BC=E3=83=89?= =?UTF-8?q?=E3=81=AE=E7=B5=82=E4=BA=86=E4=BD=8D=E7=BD=AE=E3=82=92=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/error.ts | 16 ++-- src/interpreter/index.ts | 10 +-- src/node.ts | 7 +- src/parser/plugins/validate-keyword.ts | 2 +- src/parser/syntaxes/common.ts | 8 +- src/parser/syntaxes/expressions.ts | 119 +++++++++++++------------ src/parser/syntaxes/statements.ts | 91 ++++++++++--------- src/parser/syntaxes/toplevel.ts | 8 +- src/parser/utils.ts | 11 +-- src/type.ts | 2 +- test/index.ts | 7 +- test/interpreter.ts | 7 +- 12 files changed, 156 insertions(+), 132 deletions(-) diff --git a/src/error.ts b/src/error.ts index 6d66c0f5..4f1ffb53 100644 --- a/src/error.ts +++ b/src/error.ts @@ -1,11 +1,11 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import type { Loc } from './node.js'; +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 loc?: Loc; + public pos?: Pos; constructor(message: string, info?: any) { super(message); @@ -34,8 +34,8 @@ export class NonAiScriptError extends AiScriptError { */ export class AiScriptSyntaxError extends AiScriptError { public name = 'Syntax'; - constructor(message: string, public loc: Loc, info?: any) { - super(`${message} (Line ${loc.line}, Column ${loc.column})`, info); + constructor(message: string, public pos: Pos, info?: any) { + super(`${message} (Line ${pos.line}, Column ${pos.column})`, info); } } /** @@ -43,8 +43,8 @@ export class AiScriptSyntaxError extends AiScriptError { */ export class AiScriptTypeError extends AiScriptError { public name = 'Type'; - constructor(message: string, public loc: Loc, info?: any) { - super(`${message} (Line ${loc.line}, Column ${loc.column})`, info); + constructor(message: string, public pos: Pos, info?: any) { + super(`${message} (Line ${pos.line}, Column ${pos.column})`, info); } } @@ -53,8 +53,8 @@ export class AiScriptTypeError extends AiScriptError { */ export class AiScriptNamespaceError extends AiScriptError { public name = 'Namespace'; - constructor(message: string, public loc: Loc, info?: any) { - super(`${message} (Line ${loc.line}, Column ${loc.column})`, info); + constructor(message: string, public pos: Pos, info?: any) { + super(`${message} (Line ${pos.line}, Column ${pos.column})`, info); } } diff --git a/src/interpreter/index.ts b/src/interpreter/index.ts index 38c71545..1899731e 100644 --- a/src/interpreter/index.ts +++ b/src/interpreter/index.ts @@ -191,7 +191,7 @@ export class Interpreter { switch (node.type) { case 'def': { if (node.mut) { - throw new AiScriptNamespaceError('No "var" in namespace declaration: ' + node.name, node.loc); + throw new AiScriptNamespaceError('No "var" in namespace declaration: ' + node.name, node.loc.start); } const variable: Variable = { @@ -211,7 +211,7 @@ export class Interpreter { // exhaustiveness check const n: never = node; const nd = n as Ast.Node; - throw new AiScriptNamespaceError('invalid ns member type: ' + nd.type, nd.loc); + throw new AiScriptNamespaceError('invalid ns member type: ' + nd.type, nd.loc.start); } } } @@ -246,11 +246,11 @@ export class Interpreter { @autobind private _eval(node: Ast.Node, scope: Scope): Promise { return this.__eval(node, scope).catch(e => { - if (e.loc) throw e; + if (e.pos) throw e; else { const e2 = (e instanceof AiScriptError) ? e : new NonAiScriptError(e); - e2.loc = node.loc; - e2.message = `${e2.message} (Line ${node.loc.line}, Column ${node.loc.column})`; + e2.pos = node.loc.start; + e2.message = `${e2.message} (Line ${e2.pos.line}, Column ${e2.pos.column})`; throw e2; } }); diff --git a/src/node.ts b/src/node.ts index 43b556fd..ebc4ad62 100644 --- a/src/node.ts +++ b/src/node.ts @@ -2,11 +2,16 @@ * ASTノード */ -export type Loc = { +export type Pos = { line: number; column: number; }; +export type Loc = { + start: Pos; + end: Pos; +}; + export type Node = Namespace | Meta | Statement | Expression | TypeSource | Attribute; type NodeBase = { diff --git a/src/parser/plugins/validate-keyword.ts b/src/parser/plugins/validate-keyword.ts index 97b1a3cf..d44da8f9 100644 --- a/src/parser/plugins/validate-keyword.ts +++ b/src/parser/plugins/validate-keyword.ts @@ -53,7 +53,7 @@ const reservedWord = [ ]; function throwReservedWordError(name: string, loc: Ast.Loc): void { - throw new AiScriptSyntaxError(`Reserved word "${name}" cannot be used as variable name.`, loc); + throw new AiScriptSyntaxError(`Reserved word "${name}" cannot be used as variable name.`, loc.start); } function validateNode(node: Ast.Node): Ast.Node { diff --git a/src/parser/syntaxes/common.ts b/src/parser/syntaxes/common.ts index f185a158..b23962fd 100644 --- a/src/parser/syntaxes/common.ts +++ b/src/parser/syntaxes/common.ts @@ -126,7 +126,7 @@ export function parseType(s: ITokenStream): Ast.Node { * ``` */ function parseFnType(s: ITokenStream): Ast.Node { - const loc = s.token.loc; + const startPos = s.token.loc; s.nextWith(TokenKind.At); s.nextWith(TokenKind.OpenParen); @@ -153,7 +153,7 @@ function parseFnType(s: ITokenStream): Ast.Node { const resultType = parseType(s); - return NODE('fnTypeSource', { args: params, result: resultType }, loc); + return NODE('fnTypeSource', { args: params, result: resultType }, startPos, s.token.loc); } /** @@ -162,7 +162,7 @@ function parseFnType(s: ITokenStream): Ast.Node { * ``` */ function parseNamedType(s: ITokenStream): Ast.Node { - const loc = s.token.loc; + const startPos = s.token.loc; s.expect(TokenKind.Identifier); const name = s.token.value!; @@ -176,7 +176,7 @@ function parseNamedType(s: ITokenStream): Ast.Node { s.nextWith(TokenKind.Gt); } - return NODE('namedTypeSource', { name, inner }, loc); + return NODE('namedTypeSource', { name, inner }, startPos, s.token.loc); } //#endregion Type diff --git a/src/parser/syntaxes/expressions.ts b/src/parser/syntaxes/expressions.ts index 439b8fc1..023ba4ae 100644 --- a/src/parser/syntaxes/expressions.ts +++ b/src/parser/syntaxes/expressions.ts @@ -52,7 +52,7 @@ const operators: OpInfo[] = [ ]; function parsePrefix(s: ITokenStream, minBp: number): Ast.Node { - const loc = s.token.loc; + const startPos = s.token.loc; const op = s.getKind(); s.next(); @@ -64,38 +64,40 @@ function parsePrefix(s: ITokenStream, minBp: number): Ast.Node { 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 }, loc); + return NODE('num', { value: expr.value }, startPos, endPos); } else { - throw new AiScriptSyntaxError('currently, sign is only supported for number literal.', loc); + throw new AiScriptSyntaxError('currently, sign is only supported for number literal.', startPos); } // TODO: 将来的にサポートされる式を拡張 - // return NODE('plus', { expr }, loc); + // return NODE('plus', { expr }, startPos, endPos); } case TokenKind.Minus: { // 数値リテラル以外は非サポート if (expr.type === 'num') { - return NODE('num', { value: -1 * expr.value }, loc); + return NODE('num', { value: -1 * expr.value }, startPos, endPos); } else { - throw new AiScriptSyntaxError('currently, sign is only supported for number literal.', loc); + throw new AiScriptSyntaxError('currently, sign is only supported for number literal.', startPos); } // TODO: 将来的にサポートされる式を拡張 - // return NODE('minus', { expr }, loc); + // return NODE('minus', { expr }, startPos, endPos); } case TokenKind.Not: { - return NODE('not', { expr }, loc); + return NODE('not', { expr }, startPos, endPos); } default: { - throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[op]}`, loc); + throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[op]}`, startPos); } } } function parseInfix(s: ITokenStream, left: Ast.Node, minBp: number): Ast.Node { - const loc = s.token.loc; + const startPos = s.token.loc; const op = s.getKind(); s.next(); @@ -113,62 +115,63 @@ function parseInfix(s: ITokenStream, left: Ast.Node, minBp: number): Ast.Node { return NODE('prop', { target: left, name, - }, loc); + }, startPos, s.token.loc); } else { const right = parsePratt(s, minBp); + const endPos = s.token.loc; switch (op) { case TokenKind.Hat: { - return CALL_NODE('Core:pow', [left, right], loc); + return CALL_NODE('Core:pow', [left, right], startPos, endPos); } case TokenKind.Asterisk: { - return CALL_NODE('Core:mul', [left, right], loc); + return CALL_NODE('Core:mul', [left, right], startPos, endPos); } case TokenKind.Slash: { - return CALL_NODE('Core:div', [left, right], loc); + return CALL_NODE('Core:div', [left, right], startPos, endPos); } case TokenKind.Percent: { - return CALL_NODE('Core:mod', [left, right], loc); + return CALL_NODE('Core:mod', [left, right], startPos, endPos); } case TokenKind.Plus: { - return CALL_NODE('Core:add', [left, right], loc); + return CALL_NODE('Core:add', [left, right], startPos, endPos); } case TokenKind.Minus: { - return CALL_NODE('Core:sub', [left, right], loc); + return CALL_NODE('Core:sub', [left, right], startPos, endPos); } case TokenKind.Lt: { - return CALL_NODE('Core:lt', [left, right], loc); + return CALL_NODE('Core:lt', [left, right], startPos, endPos); } case TokenKind.LtEq: { - return CALL_NODE('Core:lteq', [left, right], loc); + return CALL_NODE('Core:lteq', [left, right], startPos, endPos); } case TokenKind.Gt: { - return CALL_NODE('Core:gt', [left, right], loc); + return CALL_NODE('Core:gt', [left, right], startPos, endPos); } case TokenKind.GtEq: { - return CALL_NODE('Core:gteq', [left, right], loc); + return CALL_NODE('Core:gteq', [left, right], startPos, endPos); } case TokenKind.Eq2: { - return CALL_NODE('Core:eq', [left, right], loc); + return CALL_NODE('Core:eq', [left, right], startPos, endPos); } case TokenKind.NotEq: { - return CALL_NODE('Core:neq', [left, right], loc); + return CALL_NODE('Core:neq', [left, right], startPos, endPos); } case TokenKind.And2: { - return NODE('and', { left, right }, loc); + return NODE('and', { left, right }, startPos, endPos); } case TokenKind.Or2: { - return NODE('or', { left, right }, loc); + return NODE('or', { left, right }, startPos, endPos); } default: { - throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[op]}`, loc); + throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[op]}`, startPos); } } } } function parsePostfix(s: ITokenStream, expr: Ast.Node): Ast.Node { - const loc = s.token.loc; + const startPos = s.token.loc; const op = s.getKind(); switch (op) { @@ -183,16 +186,16 @@ function parsePostfix(s: ITokenStream, expr: Ast.Node): Ast.Node { return NODE('index', { target: expr, index, - }, loc); + }, startPos, s.token.loc); } default: { - throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[op]}`, loc); + throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[op]}`, startPos); } } } function parseAtom(s: ITokenStream, isStatic: boolean): Ast.Node { - const loc = s.token.loc; + const startPos = s.token.loc; switch (s.getKind()) { case TokenKind.IfKeyword: { @@ -220,10 +223,12 @@ function parseAtom(s: ITokenStream, isStatic: boolean): Ast.Node { if (isStatic) break; - for (const element of s.token.children!) { + for (const [i, element] of s.token.children!.entries()) { switch (element.kind) { case TokenKind.TemplateStringElement: { - values.push(NODE('str', { value: element.value! }, element.loc)); + // トークンの終了位置を取得するために先読み + const nextToken = s.token.children![i + 1] ?? s.lookahead(1); + values.push(NODE('str', { value: element.value! }, element.loc, nextToken.loc)); break; } case TokenKind.TemplateExprElement: { @@ -243,28 +248,28 @@ function parseAtom(s: ITokenStream, isStatic: boolean): Ast.Node { } s.next(); - return NODE('tmpl', { tmpl: values }, loc); + return NODE('tmpl', { tmpl: values }, startPos, s.token.loc); } case TokenKind.StringLiteral: { const value = s.token.value!; s.next(); - return NODE('str', { value }, loc); + return NODE('str', { value }, startPos, s.token.loc); } case TokenKind.NumberLiteral: { // TODO: validate number value const value = Number(s.token.value!); s.next(); - return NODE('num', { value }, loc); + return NODE('num', { value }, startPos, s.token.loc); } case TokenKind.TrueKeyword: case TokenKind.FalseKeyword: { const value = (s.getKind() === TokenKind.TrueKeyword); s.next(); - return NODE('bool', { value }, loc); + return NODE('bool', { value }, startPos, s.token.loc); } case TokenKind.NullKeyword: { s.next(); - return NODE('null', { }, loc); + return NODE('null', {}, startPos, s.token.loc); } case TokenKind.OpenBrace: { return parseObject(s, isStatic); @@ -283,14 +288,14 @@ function parseAtom(s: ITokenStream, isStatic: boolean): Ast.Node { return expr; } } - throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[s.getKind()]}`, loc); + throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[s.getKind()]}`, startPos); } /** * Call = "(" [Expr *(SEP Expr) [SEP]] ")" */ function parseCall(s: ITokenStream, target: Ast.Node): Ast.Node { - const loc = s.token.loc; + const startPos = s.token.loc; const items: Ast.Node[] = []; s.nextWith(TokenKind.OpenParen); @@ -329,7 +334,7 @@ function parseCall(s: ITokenStream, target: Ast.Node): Ast.Node { return NODE('call', { target, args: items, - }, loc); + }, startPos, s.token.loc); } /** @@ -338,7 +343,7 @@ function parseCall(s: ITokenStream, target: Ast.Node): Ast.Node { * ``` */ function parseIf(s: ITokenStream): Ast.Node { - const loc = s.token.loc; + const startPos = s.token.loc; s.nextWith(TokenKind.IfKeyword); const cond = parseExpr(s, false); @@ -365,7 +370,7 @@ function parseIf(s: ITokenStream): Ast.Node { _else = parseBlockOrStatement(s); } - return NODE('if', { cond, then, elseif, else: _else }, loc); + return NODE('if', { cond, then, elseif, else: _else }, startPos, s.token.loc); } /** @@ -374,7 +379,7 @@ function parseIf(s: ITokenStream): Ast.Node { * ``` */ function parseFnExpr(s: ITokenStream): Ast.Node { - const loc = s.token.loc; + const startPos = s.token.loc; s.nextWith(TokenKind.At); @@ -388,7 +393,7 @@ function parseFnExpr(s: ITokenStream): Ast.Node { const body = parseBlock(s); - return NODE('fn', { args: params, retType: type, children: body }, loc); + return NODE('fn', { args: params, retType: type, children: body }, startPos, s.token.loc); } /** @@ -398,7 +403,7 @@ function parseFnExpr(s: ITokenStream): Ast.Node { * ``` */ function parseMatch(s: ITokenStream): Ast.Node { - const loc = s.token.loc; + const startPos = s.token.loc; s.nextWith(TokenKind.MatchKeyword); const about = parseExpr(s, false); @@ -470,7 +475,7 @@ function parseMatch(s: ITokenStream): Ast.Node { s.nextWith(TokenKind.CloseBrace); - return NODE('match', { about, qs, default: x }, loc); + return NODE('match', { about, qs, default: x }, startPos, s.token.loc); } /** @@ -479,11 +484,12 @@ function parseMatch(s: ITokenStream): Ast.Node { * ``` */ function parseEval(s: ITokenStream): Ast.Node { - const loc = s.token.loc; + const startPos = s.token.loc; s.nextWith(TokenKind.EvalKeyword); const statements = parseBlock(s); - return NODE('block', { statements }, loc); + + return NODE('block', { statements }, startPos, s.token.loc); } /** @@ -492,11 +498,12 @@ function parseEval(s: ITokenStream): Ast.Node { * ``` */ function parseExists(s: ITokenStream): Ast.Node { - const loc = s.token.loc; + const startPos = s.token.loc; s.nextWith(TokenKind.ExistsKeyword); const identifier = parseReference(s); - return NODE('exists', { identifier }, loc); + + return NODE('exists', { identifier }, startPos, s.token.loc); } /** @@ -505,7 +512,7 @@ function parseExists(s: ITokenStream): Ast.Node { * ``` */ function parseReference(s: ITokenStream): Ast.Node { - const loc = s.token.loc; + const startPos = s.token.loc; const segs: string[] = []; while (true) { @@ -526,7 +533,7 @@ function parseReference(s: ITokenStream): Ast.Node { segs.push(s.token.value!); s.next(); } - return NODE('identifier', { name: segs.join(':') }, loc); + return NODE('identifier', { name: segs.join(':') }, startPos, s.token.loc); } /** @@ -535,7 +542,7 @@ function parseReference(s: ITokenStream): Ast.Node { * ``` */ function parseObject(s: ITokenStream, isStatic: boolean): Ast.Node { - const loc = s.token.loc; + const startPos = s.token.loc; s.nextWith(TokenKind.OpenBrace); @@ -576,7 +583,7 @@ function parseObject(s: ITokenStream, isStatic: boolean): Ast.Node { s.nextWith(TokenKind.CloseBrace); - return NODE('obj', { value: map }, loc); + return NODE('obj', { value: map }, startPos, s.token.loc); } /** @@ -585,7 +592,7 @@ function parseObject(s: ITokenStream, isStatic: boolean): Ast.Node { * ``` */ function parseArray(s: ITokenStream, isStatic: boolean): Ast.Node { - const loc = s.token.loc; + const startPos = s.token.loc; s.nextWith(TokenKind.OpenBracket); @@ -618,7 +625,7 @@ function parseArray(s: ITokenStream, isStatic: boolean): Ast.Node { s.nextWith(TokenKind.CloseBracket); - return NODE('arr', { value }, loc); + return NODE('arr', { value }, startPos, s.token.loc); } //#region Pratt parsing diff --git a/src/parser/syntaxes/statements.ts b/src/parser/syntaxes/statements.ts index d60b8d5b..02c6ff38 100644 --- a/src/parser/syntaxes/statements.ts +++ b/src/parser/syntaxes/statements.ts @@ -14,7 +14,7 @@ import type { ITokenStream } from '../streams/token-stream.js'; * ``` */ export function parseStatement(s: ITokenStream): Ast.Node { - const loc = s.token.loc; + const startPos = s.token.loc; switch (s.getKind()) { case TokenKind.VarKeyword: @@ -53,11 +53,11 @@ export function parseStatement(s: ITokenStream): Ast.Node { } case TokenKind.BreakKeyword: { s.next(); - return NODE('break', {}, loc); + return NODE('break', {}, startPos, s.token.loc); } case TokenKind.ContinueKeyword: { s.next(); - return NODE('continue', {}, loc); + return NODE('continue', {}, startPos, s.token.loc); } } const expr = parseExpr(s, false); @@ -89,11 +89,10 @@ export function parseDefStatement(s: ITokenStream): Ast.Node { * ``` */ export function parseBlockOrStatement(s: ITokenStream): Ast.Node { - const loc = s.token.loc; - if (s.getKind() === TokenKind.OpenBrace) { + const startPos = s.token.loc; const statements = parseBlock(s); - return NODE('block', { statements }, loc); + return NODE('block', { statements }, startPos, s.token.loc); } else { return parseStatement(s); } @@ -105,7 +104,7 @@ export function parseBlockOrStatement(s: ITokenStream): Ast.Node { * ``` */ function parseVarDef(s: ITokenStream): Ast.Node { - const loc = s.token.loc; + const startPos = s.token.loc; let mut; switch (s.getKind()) { @@ -141,7 +140,7 @@ function parseVarDef(s: ITokenStream): Ast.Node { const expr = parseExpr(s, false); - return NODE('def', { name, varType: type, expr, mut, attr: [] }, loc); + return NODE('def', { name, varType: type, expr, mut, attr: [] }, startPos, s.token.loc); } /** @@ -150,7 +149,7 @@ function parseVarDef(s: ITokenStream): Ast.Node { * ``` */ function parseFnDef(s: ITokenStream): Ast.Node { - const loc = s.token.loc; + const startPos = s.token.loc; s.nextWith(TokenKind.At); @@ -168,16 +167,18 @@ function parseFnDef(s: ITokenStream): Ast.Node { const body = parseBlock(s); + const endPos = s.token.loc; + return NODE('def', { name, expr: NODE('fn', { args: params, retType: type, children: body, - }, loc), + }, startPos, endPos), mut: false, attr: [], - }, loc); + }, startPos, endPos); } /** @@ -186,11 +187,12 @@ function parseFnDef(s: ITokenStream): Ast.Node { * ``` */ function parseOut(s: ITokenStream): Ast.Node { - const loc = s.token.loc; + const startPos = s.token.loc; s.nextWith(TokenKind.Out); const expr = parseExpr(s, false); - return CALL_NODE('print', [expr], loc); + + return CALL_NODE('print', [expr], startPos, s.token.loc); } /** @@ -200,7 +202,7 @@ function parseOut(s: ITokenStream): Ast.Node { * ``` */ function parseEach(s: ITokenStream): Ast.Node { - const loc = s.token.loc; + const startPos = s.token.loc; let hasParen = false; s.nextWith(TokenKind.EachKeyword); @@ -234,11 +236,11 @@ function parseEach(s: ITokenStream): Ast.Node { var: name, items: items, for: body, - }, loc); + }, startPos, s.token.loc); } function parseFor(s: ITokenStream): Ast.Node { - const loc = s.token.loc; + const startPos = s.token.loc; let hasParen = false; s.nextWith(TokenKind.ForKeyword); @@ -252,7 +254,7 @@ function parseFor(s: ITokenStream): Ast.Node { // range syntax s.next(); - const identLoc = s.token.loc; + const identPos = s.token.loc; s.expect(TokenKind.Identifier); const name = s.token.value!; @@ -263,7 +265,7 @@ function parseFor(s: ITokenStream): Ast.Node { s.next(); _from = parseExpr(s, false); } else { - _from = NODE('num', { value: 0 }, identLoc); + _from = NODE('num', { value: 0 }, identPos, identPos); } if (s.getKind() === TokenKind.Comma) { @@ -285,7 +287,7 @@ function parseFor(s: ITokenStream): Ast.Node { from: _from, to, for: body, - }, loc); + }, startPos, s.token.loc); } else { // times syntax @@ -300,7 +302,7 @@ function parseFor(s: ITokenStream): Ast.Node { return NODE('for', { times, for: body, - }, loc); + }, startPos, s.token.loc); } } @@ -310,11 +312,12 @@ function parseFor(s: ITokenStream): Ast.Node { * ``` */ function parseReturn(s: ITokenStream): Ast.Node { - const loc = s.token.loc; + const startPos = s.token.loc; s.nextWith(TokenKind.ReturnKeyword); const expr = parseExpr(s, false); - return NODE('return', { expr }, loc); + + return NODE('return', { expr }, startPos, s.token.loc); } /** @@ -332,7 +335,7 @@ function parseStatementWithAttr(s: ITokenStream): Ast.Node { const statement = parseStatement(s); if (statement.type !== 'def') { - throw new AiScriptSyntaxError('invalid attribute.', statement.loc); + throw new AiScriptSyntaxError('invalid attribute.', statement.loc.start); } if (statement.attr != null) { statement.attr.push(...attrs); @@ -349,7 +352,7 @@ function parseStatementWithAttr(s: ITokenStream): Ast.Node { * ``` */ function parseAttr(s: ITokenStream): Ast.Node { - const loc = s.token.loc; + const startPos = s.token.loc; s.nextWith(TokenKind.OpenSharpBracket); @@ -361,12 +364,13 @@ function parseAttr(s: ITokenStream): Ast.Node { if (s.getKind() !== TokenKind.CloseBracket) { value = parseExpr(s, true); } else { - value = NODE('bool', { value: true }, loc); + const closePos = s.token.loc; + value = NODE('bool', { value: true }, closePos, closePos); } s.nextWith(TokenKind.CloseBracket); - return NODE('attr', { name, value }, loc); + return NODE('attr', { name, value }, startPos, s.token.loc); } /** @@ -375,11 +379,12 @@ function parseAttr(s: ITokenStream): Ast.Node { * ``` */ function parseLoop(s: ITokenStream): Ast.Node { - const loc = s.token.loc; + const startPos = s.token.loc; s.nextWith(TokenKind.LoopKeyword); const statements = parseBlock(s); - return NODE('loop', { statements }, loc); + + return NODE('loop', { statements }, startPos, s.token.loc); } /** @@ -388,23 +393,24 @@ function parseLoop(s: ITokenStream): Ast.Node { * ``` */ function parseDoWhile(s: ITokenStream): Ast.Node { - const doLoc = s.token.loc; + const doStartPos = s.token.loc; s.nextWith(TokenKind.DoKeyword); const body = parseBlockOrStatement(s); - const whileLoc = s.token.loc; + const whilePos = s.token.loc; s.nextWith(TokenKind.WhileKeyword); const cond = parseExpr(s, false); + const endPos = s.token.loc; return NODE('loop', { statements: [ body, NODE('if', { - cond: NODE('not', { expr: cond }, whileLoc), - then: NODE('break', {}, whileLoc), + cond: NODE('not', { expr: cond }, whilePos, endPos), + then: NODE('break', {}, endPos, endPos), elseif: [], - }, whileLoc), + }, whilePos, endPos), ], - }, doLoc); + }, doStartPos, endPos); } /** @@ -413,21 +419,22 @@ function parseDoWhile(s: ITokenStream): Ast.Node { * ``` */ function parseWhile(s: ITokenStream): Ast.Node { - const loc = s.token.loc; + const startPos = s.token.loc; s.nextWith(TokenKind.WhileKeyword); const cond = parseExpr(s, false); + const condEndPos = s.token.loc; const body = parseBlockOrStatement(s); return NODE('loop', { statements: [ NODE('if', { - cond: NODE('not', { expr: cond }, loc), - then: NODE('break', {}, loc), + cond: NODE('not', { expr: cond }, startPos, condEndPos), + then: NODE('break', {}, condEndPos, condEndPos), elseif: [], - }, loc), + }, startPos, condEndPos), body, ], - }, loc); + }, startPos, s.token.loc); } /** @@ -443,17 +450,17 @@ function tryParseAssign(s: ITokenStream, dest: Ast.Node): Ast.Node | undefined { case TokenKind.Eq: { s.next(); const expr = parseExpr(s, false); - return NODE('assign', { dest, expr }, loc); + return NODE('assign', { dest, expr }, loc, s.token.loc); } case TokenKind.PlusEq: { s.next(); const expr = parseExpr(s, false); - return NODE('addAssign', { dest, expr }, loc); + return NODE('addAssign', { dest, expr }, loc, s.token.loc); } case TokenKind.MinusEq: { s.next(); const expr = parseExpr(s, false); - return NODE('subAssign', { dest, expr }, loc); + return NODE('subAssign', { dest, expr }, loc, s.token.loc); } default: { return; diff --git a/src/parser/syntaxes/toplevel.ts b/src/parser/syntaxes/toplevel.ts index ebd32d28..a6342219 100644 --- a/src/parser/syntaxes/toplevel.ts +++ b/src/parser/syntaxes/toplevel.ts @@ -62,7 +62,7 @@ export function parseTopLevel(s: ITokenStream): Ast.Node[] { * ``` */ export function parseNamespace(s: ITokenStream): Ast.Node { - const loc = s.token.loc; + const startPos = s.token.loc; s.nextWith(TokenKind.Colon2); @@ -110,7 +110,7 @@ export function parseNamespace(s: ITokenStream): Ast.Node { } s.nextWith(TokenKind.CloseBrace); - return NODE('ns', { name, members }, loc); + return NODE('ns', { name, members }, startPos, s.token.loc); } /** @@ -119,7 +119,7 @@ export function parseNamespace(s: ITokenStream): Ast.Node { * ``` */ export function parseMeta(s: ITokenStream): Ast.Node { - const loc = s.token.loc; + const startPos = s.token.loc; s.nextWith(TokenKind.Sharp3); @@ -131,5 +131,5 @@ export function parseMeta(s: ITokenStream): Ast.Node { const value = parseExpr(s, true); - return NODE('meta', { name, value }, loc); + return NODE('meta', { name, value }, startPos, value.loc.end); } diff --git a/src/parser/utils.ts b/src/parser/utils.ts index 456764e5..1bd6d78f 100644 --- a/src/parser/utils.ts +++ b/src/parser/utils.ts @@ -1,19 +1,20 @@ import type * as Ast from '../node.js'; -export function NODE(type: string, params: Record, loc: { column: number, line: number }): Ast.Node { +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 = loc; + node.loc = { start, end }; return node as Ast.Node; } -export function CALL_NODE(name: string, args: Ast.Node[], loc: { column: number, line: number }): Ast.Node { +export function CALL_NODE(name: string, args: Ast.Node[], start: Ast.Pos, end: Ast.Pos): Ast.Node { return NODE('call', { - target: NODE('identifier', { name }, loc), + // 糖衣構文はidentifierがソースコードに出現しないので長さ0とする。 + target: NODE('identifier', { name }, start, start), args, - }, loc); + }, start, end); } diff --git a/src/type.ts b/src/type.ts index 54cd9f0f..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)}'`, typeSource.loc); + 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 b80a6c2a..a491abb5 100644 --- a/test/index.ts +++ b/test/index.ts @@ -928,7 +928,10 @@ describe('Location', () => { assert.equal(nodes.length, 1); node = nodes[0]; if (!node.loc) assert.fail(); - assert.deepEqual(node.loc, { line: 2, column: 4 }); + assert.deepEqual(node.loc, { + start: { line: 2, column: 4 }, + end: { line: 2, column: 15 }, + }); }); test.concurrent('comment', async () => { let node: Ast.Node; @@ -942,7 +945,7 @@ describe('Location', () => { assert.equal(nodes.length, 1); node = nodes[0]; if (!node.loc) assert.fail(); - assert.deepEqual(node.loc, { line: 5, column: 3 }); + assert.deepEqual(node.loc.start, { line: 5, column: 3 }); }); }); diff --git a/test/interpreter.ts b/test/interpreter.ts index f9953705..41b64013 100644 --- a/test/interpreter.ts +++ b/test/interpreter.ts @@ -1,6 +1,7 @@ import * as assert from 'assert'; import { expect, test } from '@jest/globals'; -import { Parser, Interpreter, values, errors, utils } from '../src'; +import { Parser, Interpreter, values, errors, utils, Ast } from '../src'; + let { FN_NATIVE } = values; let { AiScriptRuntimeError, AiScriptIndexOutOfRangeError } = errors; @@ -65,13 +66,13 @@ describe('error handler', () => { }); describe('error location', () => { - const exeAndGetErrLoc = (src: string): Promise => new Promise((ok, ng) => { + const exeAndGetErrLoc = (src: string): Promise => new Promise((ok, ng) => { const aiscript = new Interpreter({ emitError: FN_NATIVE((_args, _opts) => { throw Error('emitError'); }), }, { - err(e) { ok(e.loc) }, + err(e) { ok(e.pos) }, }); aiscript.exec(Parser.parse(src)).then(() => ng('error has not occured.')); }); From 966298a35857d1e19eab15979071d31ffee430d9 Mon Sep 17 00:00:00 2001 From: uzmoi Date: Sat, 20 Jul 2024 22:07:52 +0900 Subject: [PATCH 57/62] =?UTF-8?q?ITokenStream=E3=81=ABgetPos=E3=82=92?= =?UTF-8?q?=E7=94=9F=E3=82=84=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/parser/scanner.ts | 11 ++++- src/parser/streams/token-stream.ts | 16 ++++++- src/parser/syntaxes/common.ts | 14 +++--- src/parser/syntaxes/expressions.ts | 74 ++++++++++++++--------------- src/parser/syntaxes/statements.ts | 76 +++++++++++++++--------------- src/parser/syntaxes/toplevel.ts | 10 ++-- 6 files changed, 110 insertions(+), 91 deletions(-) diff --git a/src/parser/scanner.ts b/src/parser/scanner.ts index fb2d4c17..9c28580c 100644 --- a/src/parser/scanner.ts +++ b/src/parser/scanner.ts @@ -3,7 +3,7 @@ import { CharStream } from './streams/char-stream.js'; import { TOKEN, TokenKind } from './token.js'; import type { ITokenStream } from './streams/token-stream.js'; -import type { Token } from './token.js'; +import type { Token, TokenLocation } from './token.js'; const spaceChars = [' ', '\t']; const lineBreakChars = ['\r', '\n']; @@ -42,6 +42,13 @@ export class Scanner implements ITokenStream { return this.token.kind; } + /** + * カーソル位置にあるトークンの位置情報を取得します。 + */ + public getPos(): TokenLocation { + return this.token.loc; + } + /** * カーソル位置を次のトークンへ進めます。 */ @@ -75,7 +82,7 @@ export class Scanner implements ITokenStream { */ public expect(kind: TokenKind): void { if (this.getKind() !== kind) { - throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[this.getKind()]}`, this.token.loc); + throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[this.getKind()]}`, this.getPos()); } } diff --git a/src/parser/streams/token-stream.ts b/src/parser/streams/token-stream.ts index 0ef2fe23..af7a41c7 100644 --- a/src/parser/streams/token-stream.ts +++ b/src/parser/streams/token-stream.ts @@ -1,6 +1,6 @@ import { AiScriptSyntaxError } from '../../error.js'; import { TOKEN, TokenKind } from '../token.js'; -import type { Token } from '../token.js'; +import type { Token, TokenLocation } from '../token.js'; /** * トークンの読み取りに関するインターフェース @@ -16,6 +16,11 @@ export interface ITokenStream { */ getKind(): TokenKind; + /** + * カーソル位置にあるトークンの位置情報を取得します。 + */ + getPos(): TokenLocation; + /** * カーソル位置を次のトークンへ進めます。 */ @@ -74,6 +79,13 @@ export class TokenStream implements ITokenStream { return this.token.kind; } + /** + * カーソル位置にあるトークンの位置情報を取得します。 + */ + public getPos(): TokenLocation { + return this.token.loc; + } + /** * カーソル位置を次のトークンへ進めます。 */ @@ -101,7 +113,7 @@ export class TokenStream implements ITokenStream { */ public expect(kind: TokenKind): void { if (this.getKind() !== kind) { - throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[this.getKind()]}`, this.token.loc); + throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[this.getKind()]}`, this.getPos()); } } diff --git a/src/parser/syntaxes/common.ts b/src/parser/syntaxes/common.ts index b23962fd..faf5cc5e 100644 --- a/src/parser/syntaxes/common.ts +++ b/src/parser/syntaxes/common.ts @@ -60,7 +60,7 @@ export function parseParams(s: ITokenStream): { name: string, argType?: Ast.Node break; } default: { - throw new AiScriptSyntaxError('separator expected', s.token.loc); + throw new AiScriptSyntaxError('separator expected', s.getPos()); } } } @@ -99,7 +99,7 @@ export function parseBlock(s: ITokenStream): Ast.Node[] { break; } default: { - throw new AiScriptSyntaxError('Multiple statements cannot be placed on a single line.', s.token.loc); + throw new AiScriptSyntaxError('Multiple statements cannot be placed on a single line.', s.getPos()); } } } @@ -126,7 +126,7 @@ export function parseType(s: ITokenStream): Ast.Node { * ``` */ function parseFnType(s: ITokenStream): Ast.Node { - const startPos = s.token.loc; + const startPos = s.getPos(); s.nextWith(TokenKind.At); s.nextWith(TokenKind.OpenParen); @@ -140,7 +140,7 @@ function parseFnType(s: ITokenStream): Ast.Node { break; } default: { - throw new AiScriptSyntaxError('separator expected', s.token.loc); + throw new AiScriptSyntaxError('separator expected', s.getPos()); } } } @@ -153,7 +153,7 @@ function parseFnType(s: ITokenStream): Ast.Node { const resultType = parseType(s); - return NODE('fnTypeSource', { args: params, result: resultType }, startPos, s.token.loc); + return NODE('fnTypeSource', { args: params, result: resultType }, startPos, s.getPos()); } /** @@ -162,7 +162,7 @@ function parseFnType(s: ITokenStream): Ast.Node { * ``` */ function parseNamedType(s: ITokenStream): Ast.Node { - const startPos = s.token.loc; + const startPos = s.getPos(); s.expect(TokenKind.Identifier); const name = s.token.value!; @@ -176,7 +176,7 @@ function parseNamedType(s: ITokenStream): Ast.Node { s.nextWith(TokenKind.Gt); } - return NODE('namedTypeSource', { name, inner }, startPos, s.token.loc); + return NODE('namedTypeSource', { name, inner }, startPos, s.getPos()); } //#endregion Type diff --git a/src/parser/syntaxes/expressions.ts b/src/parser/syntaxes/expressions.ts index 023ba4ae..0e90b15c 100644 --- a/src/parser/syntaxes/expressions.ts +++ b/src/parser/syntaxes/expressions.ts @@ -52,7 +52,7 @@ const operators: OpInfo[] = [ ]; function parsePrefix(s: ITokenStream, minBp: number): Ast.Node { - const startPos = s.token.loc; + const startPos = s.getPos(); const op = s.getKind(); s.next(); @@ -97,7 +97,7 @@ function parsePrefix(s: ITokenStream, minBp: number): Ast.Node { } function parseInfix(s: ITokenStream, left: Ast.Node, minBp: number): Ast.Node { - const startPos = s.token.loc; + const startPos = s.getPos(); const op = s.getKind(); s.next(); @@ -115,10 +115,10 @@ function parseInfix(s: ITokenStream, left: Ast.Node, minBp: number): Ast.Node { return NODE('prop', { target: left, name, - }, startPos, s.token.loc); + }, startPos, s.getPos()); } else { const right = parsePratt(s, minBp); - const endPos = s.token.loc; + const endPos = s.getPos(); switch (op) { case TokenKind.Hat: { @@ -171,7 +171,7 @@ function parseInfix(s: ITokenStream, left: Ast.Node, minBp: number): Ast.Node { } function parsePostfix(s: ITokenStream, expr: Ast.Node): Ast.Node { - const startPos = s.token.loc; + const startPos = s.getPos(); const op = s.getKind(); switch (op) { @@ -186,7 +186,7 @@ function parsePostfix(s: ITokenStream, expr: Ast.Node): Ast.Node { return NODE('index', { target: expr, index, - }, startPos, s.token.loc); + }, startPos, s.getPos()); } default: { throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[op]}`, startPos); @@ -195,7 +195,7 @@ function parsePostfix(s: ITokenStream, expr: Ast.Node): Ast.Node { } function parseAtom(s: ITokenStream, isStatic: boolean): Ast.Node { - const startPos = s.token.loc; + const startPos = s.getPos(); switch (s.getKind()) { case TokenKind.IfKeyword: { @@ -248,28 +248,28 @@ function parseAtom(s: ITokenStream, isStatic: boolean): Ast.Node { } s.next(); - return NODE('tmpl', { tmpl: values }, startPos, s.token.loc); + return NODE('tmpl', { tmpl: values }, startPos, s.getPos()); } case TokenKind.StringLiteral: { const value = s.token.value!; s.next(); - return NODE('str', { value }, startPos, s.token.loc); + 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.token.loc); + 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.token.loc); + return NODE('bool', { value }, startPos, s.getPos()); } case TokenKind.NullKeyword: { s.next(); - return NODE('null', {}, startPos, s.token.loc); + return NODE('null', {}, startPos, s.getPos()); } case TokenKind.OpenBrace: { return parseObject(s, isStatic); @@ -295,7 +295,7 @@ function parseAtom(s: ITokenStream, isStatic: boolean): Ast.Node { * Call = "(" [Expr *(SEP Expr) [SEP]] ")" */ function parseCall(s: ITokenStream, target: Ast.Node): Ast.Node { - const startPos = s.token.loc; + const startPos = s.getPos(); const items: Ast.Node[] = []; s.nextWith(TokenKind.OpenParen); @@ -324,7 +324,7 @@ function parseCall(s: ITokenStream, target: Ast.Node): Ast.Node { break; } default: { - throw new AiScriptSyntaxError('separator expected', s.token.loc); + throw new AiScriptSyntaxError('separator expected', s.getPos()); } } } @@ -334,7 +334,7 @@ function parseCall(s: ITokenStream, target: Ast.Node): Ast.Node { return NODE('call', { target, args: items, - }, startPos, s.token.loc); + }, startPos, s.getPos()); } /** @@ -343,7 +343,7 @@ function parseCall(s: ITokenStream, target: Ast.Node): Ast.Node { * ``` */ function parseIf(s: ITokenStream): Ast.Node { - const startPos = s.token.loc; + const startPos = s.getPos(); s.nextWith(TokenKind.IfKeyword); const cond = parseExpr(s, false); @@ -370,7 +370,7 @@ function parseIf(s: ITokenStream): Ast.Node { _else = parseBlockOrStatement(s); } - return NODE('if', { cond, then, elseif, else: _else }, startPos, s.token.loc); + return NODE('if', { cond, then, elseif, else: _else }, startPos, s.getPos()); } /** @@ -379,7 +379,7 @@ function parseIf(s: ITokenStream): Ast.Node { * ``` */ function parseFnExpr(s: ITokenStream): Ast.Node { - const startPos = s.token.loc; + const startPos = s.getPos(); s.nextWith(TokenKind.At); @@ -393,7 +393,7 @@ function parseFnExpr(s: ITokenStream): Ast.Node { const body = parseBlock(s); - return NODE('fn', { args: params, retType: type, children: body }, startPos, s.token.loc); + return NODE('fn', { args: params, retType: type, children: body }, startPos, s.getPos()); } /** @@ -403,7 +403,7 @@ function parseFnExpr(s: ITokenStream): Ast.Node { * ``` */ function parseMatch(s: ITokenStream): Ast.Node { - const startPos = s.token.loc; + const startPos = s.getPos(); s.nextWith(TokenKind.MatchKeyword); const about = parseExpr(s, false); @@ -440,7 +440,7 @@ function parseMatch(s: ITokenStream): Ast.Node { break; } default: { - throw new AiScriptSyntaxError('separator expected', s.token.loc); + throw new AiScriptSyntaxError('separator expected', s.getPos()); } } } @@ -468,14 +468,14 @@ function parseMatch(s: ITokenStream): Ast.Node { break; } default: { - throw new AiScriptSyntaxError('separator expected', s.token.loc); + throw new AiScriptSyntaxError('separator expected', s.getPos()); } } } s.nextWith(TokenKind.CloseBrace); - return NODE('match', { about, qs, default: x }, startPos, s.token.loc); + return NODE('match', { about, qs, default: x }, startPos, s.getPos()); } /** @@ -484,12 +484,12 @@ function parseMatch(s: ITokenStream): Ast.Node { * ``` */ function parseEval(s: ITokenStream): Ast.Node { - const startPos = s.token.loc; + const startPos = s.getPos(); s.nextWith(TokenKind.EvalKeyword); const statements = parseBlock(s); - return NODE('block', { statements }, startPos, s.token.loc); + return NODE('block', { statements }, startPos, s.getPos()); } /** @@ -498,12 +498,12 @@ function parseEval(s: ITokenStream): Ast.Node { * ``` */ function parseExists(s: ITokenStream): Ast.Node { - const startPos = s.token.loc; + const startPos = s.getPos(); s.nextWith(TokenKind.ExistsKeyword); const identifier = parseReference(s); - return NODE('exists', { identifier }, startPos, s.token.loc); + return NODE('exists', { identifier }, startPos, s.getPos()); } /** @@ -512,18 +512,18 @@ function parseExists(s: ITokenStream): Ast.Node { * ``` */ function parseReference(s: ITokenStream): Ast.Node { - const startPos = s.token.loc; + 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.token.loc); + 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.token.loc); + throw new AiScriptSyntaxError('Cannot use spaces in a reference.', s.getPos()); } } else { break; @@ -533,7 +533,7 @@ function parseReference(s: ITokenStream): Ast.Node { segs.push(s.token.value!); s.next(); } - return NODE('identifier', { name: segs.join(':') }, startPos, s.token.loc); + return NODE('identifier', { name: segs.join(':') }, startPos, s.getPos()); } /** @@ -542,7 +542,7 @@ function parseReference(s: ITokenStream): Ast.Node { * ``` */ function parseObject(s: ITokenStream, isStatic: boolean): Ast.Node { - const startPos = s.token.loc; + const startPos = s.getPos(); s.nextWith(TokenKind.OpenBrace); @@ -576,14 +576,14 @@ function parseObject(s: ITokenStream, isStatic: boolean): Ast.Node { break; } default: { - throw new AiScriptSyntaxError('separator expected', s.token.loc); + throw new AiScriptSyntaxError('separator expected', s.getPos()); } } } s.nextWith(TokenKind.CloseBrace); - return NODE('obj', { value: map }, startPos, s.token.loc); + return NODE('obj', { value: map }, startPos, s.getPos()); } /** @@ -592,7 +592,7 @@ function parseObject(s: ITokenStream, isStatic: boolean): Ast.Node { * ``` */ function parseArray(s: ITokenStream, isStatic: boolean): Ast.Node { - const startPos = s.token.loc; + const startPos = s.getPos(); s.nextWith(TokenKind.OpenBracket); @@ -618,14 +618,14 @@ function parseArray(s: ITokenStream, isStatic: boolean): Ast.Node { break; } default: { - throw new AiScriptSyntaxError('separator expected', s.token.loc); + throw new AiScriptSyntaxError('separator expected', s.getPos()); } } } s.nextWith(TokenKind.CloseBracket); - return NODE('arr', { value }, startPos, s.token.loc); + return NODE('arr', { value }, startPos, s.getPos()); } //#region Pratt parsing diff --git a/src/parser/syntaxes/statements.ts b/src/parser/syntaxes/statements.ts index 02c6ff38..86e2d030 100644 --- a/src/parser/syntaxes/statements.ts +++ b/src/parser/syntaxes/statements.ts @@ -14,7 +14,7 @@ import type { ITokenStream } from '../streams/token-stream.js'; * ``` */ export function parseStatement(s: ITokenStream): Ast.Node { - const startPos = s.token.loc; + const startPos = s.getPos(); switch (s.getKind()) { case TokenKind.VarKeyword: @@ -53,11 +53,11 @@ export function parseStatement(s: ITokenStream): Ast.Node { } case TokenKind.BreakKeyword: { s.next(); - return NODE('break', {}, startPos, s.token.loc); + return NODE('break', {}, startPos, s.getPos()); } case TokenKind.ContinueKeyword: { s.next(); - return NODE('continue', {}, startPos, s.token.loc); + return NODE('continue', {}, startPos, s.getPos()); } } const expr = parseExpr(s, false); @@ -78,7 +78,7 @@ export function parseDefStatement(s: ITokenStream): Ast.Node { return parseFnDef(s); } default: { - throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[s.getKind()]}`, s.token.loc); + throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[s.getKind()]}`, s.getPos()); } } } @@ -90,9 +90,9 @@ export function parseDefStatement(s: ITokenStream): Ast.Node { */ export function parseBlockOrStatement(s: ITokenStream): Ast.Node { if (s.getKind() === TokenKind.OpenBrace) { - const startPos = s.token.loc; + const startPos = s.getPos(); const statements = parseBlock(s); - return NODE('block', { statements }, startPos, s.token.loc); + return NODE('block', { statements }, startPos, s.getPos()); } else { return parseStatement(s); } @@ -104,7 +104,7 @@ export function parseBlockOrStatement(s: ITokenStream): Ast.Node { * ``` */ function parseVarDef(s: ITokenStream): Ast.Node { - const startPos = s.token.loc; + const startPos = s.getPos(); let mut; switch (s.getKind()) { @@ -117,7 +117,7 @@ function parseVarDef(s: ITokenStream): Ast.Node { break; } default: { - throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[s.getKind()]}`, s.token.loc); + throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[s.getKind()]}`, s.getPos()); } } s.next(); @@ -140,7 +140,7 @@ function parseVarDef(s: ITokenStream): Ast.Node { const expr = parseExpr(s, false); - return NODE('def', { name, varType: type, expr, mut, attr: [] }, startPos, s.token.loc); + return NODE('def', { name, varType: type, expr, mut, attr: [] }, startPos, s.getPos()); } /** @@ -149,7 +149,7 @@ function parseVarDef(s: ITokenStream): Ast.Node { * ``` */ function parseFnDef(s: ITokenStream): Ast.Node { - const startPos = s.token.loc; + const startPos = s.getPos(); s.nextWith(TokenKind.At); @@ -167,7 +167,7 @@ function parseFnDef(s: ITokenStream): Ast.Node { const body = parseBlock(s); - const endPos = s.token.loc; + const endPos = s.getPos(); return NODE('def', { name, @@ -187,12 +187,12 @@ function parseFnDef(s: ITokenStream): Ast.Node { * ``` */ function parseOut(s: ITokenStream): Ast.Node { - const startPos = s.token.loc; + const startPos = s.getPos(); s.nextWith(TokenKind.Out); const expr = parseExpr(s, false); - return CALL_NODE('print', [expr], startPos, s.token.loc); + return CALL_NODE('print', [expr], startPos, s.getPos()); } /** @@ -202,7 +202,7 @@ function parseOut(s: ITokenStream): Ast.Node { * ``` */ function parseEach(s: ITokenStream): Ast.Node { - const startPos = s.token.loc; + const startPos = s.getPos(); let hasParen = false; s.nextWith(TokenKind.EachKeyword); @@ -221,7 +221,7 @@ function parseEach(s: ITokenStream): Ast.Node { if (s.getKind() === TokenKind.Comma) { s.next(); } else { - throw new AiScriptSyntaxError('separator expected', s.token.loc); + throw new AiScriptSyntaxError('separator expected', s.getPos()); } const items = parseExpr(s, false); @@ -236,11 +236,11 @@ function parseEach(s: ITokenStream): Ast.Node { var: name, items: items, for: body, - }, startPos, s.token.loc); + }, startPos, s.getPos()); } function parseFor(s: ITokenStream): Ast.Node { - const startPos = s.token.loc; + const startPos = s.getPos(); let hasParen = false; s.nextWith(TokenKind.ForKeyword); @@ -254,7 +254,7 @@ function parseFor(s: ITokenStream): Ast.Node { // range syntax s.next(); - const identPos = s.token.loc; + const identPos = s.getPos(); s.expect(TokenKind.Identifier); const name = s.token.value!; @@ -271,7 +271,7 @@ function parseFor(s: ITokenStream): Ast.Node { if (s.getKind() === TokenKind.Comma) { s.next(); } else { - throw new AiScriptSyntaxError('separator expected', s.token.loc); + throw new AiScriptSyntaxError('separator expected', s.getPos()); } const to = parseExpr(s, false); @@ -287,7 +287,7 @@ function parseFor(s: ITokenStream): Ast.Node { from: _from, to, for: body, - }, startPos, s.token.loc); + }, startPos, s.getPos()); } else { // times syntax @@ -302,7 +302,7 @@ function parseFor(s: ITokenStream): Ast.Node { return NODE('for', { times, for: body, - }, startPos, s.token.loc); + }, startPos, s.getPos()); } } @@ -312,12 +312,12 @@ function parseFor(s: ITokenStream): Ast.Node { * ``` */ function parseReturn(s: ITokenStream): Ast.Node { - const startPos = s.token.loc; + const startPos = s.getPos(); s.nextWith(TokenKind.ReturnKeyword); const expr = parseExpr(s, false); - return NODE('return', { expr }, startPos, s.token.loc); + return NODE('return', { expr }, startPos, s.getPos()); } /** @@ -352,7 +352,7 @@ function parseStatementWithAttr(s: ITokenStream): Ast.Node { * ``` */ function parseAttr(s: ITokenStream): Ast.Node { - const startPos = s.token.loc; + const startPos = s.getPos(); s.nextWith(TokenKind.OpenSharpBracket); @@ -364,13 +364,13 @@ function parseAttr(s: ITokenStream): Ast.Node { if (s.getKind() !== TokenKind.CloseBracket) { value = parseExpr(s, true); } else { - const closePos = s.token.loc; + const closePos = s.getPos(); value = NODE('bool', { value: true }, closePos, closePos); } s.nextWith(TokenKind.CloseBracket); - return NODE('attr', { name, value }, startPos, s.token.loc); + return NODE('attr', { name, value }, startPos, s.getPos()); } /** @@ -379,12 +379,12 @@ function parseAttr(s: ITokenStream): Ast.Node { * ``` */ function parseLoop(s: ITokenStream): Ast.Node { - const startPos = s.token.loc; + const startPos = s.getPos(); s.nextWith(TokenKind.LoopKeyword); const statements = parseBlock(s); - return NODE('loop', { statements }, startPos, s.token.loc); + return NODE('loop', { statements }, startPos, s.getPos()); } /** @@ -393,13 +393,13 @@ function parseLoop(s: ITokenStream): Ast.Node { * ``` */ function parseDoWhile(s: ITokenStream): Ast.Node { - const doStartPos = s.token.loc; + const doStartPos = s.getPos(); s.nextWith(TokenKind.DoKeyword); const body = parseBlockOrStatement(s); - const whilePos = s.token.loc; + const whilePos = s.getPos(); s.nextWith(TokenKind.WhileKeyword); const cond = parseExpr(s, false); - const endPos = s.token.loc; + const endPos = s.getPos(); return NODE('loop', { statements: [ @@ -419,10 +419,10 @@ function parseDoWhile(s: ITokenStream): Ast.Node { * ``` */ function parseWhile(s: ITokenStream): Ast.Node { - const startPos = s.token.loc; + const startPos = s.getPos(); s.nextWith(TokenKind.WhileKeyword); const cond = parseExpr(s, false); - const condEndPos = s.token.loc; + const condEndPos = s.getPos(); const body = parseBlockOrStatement(s); return NODE('loop', { @@ -434,7 +434,7 @@ function parseWhile(s: ITokenStream): Ast.Node { }, startPos, condEndPos), body, ], - }, startPos, s.token.loc); + }, startPos, s.getPos()); } /** @@ -443,24 +443,24 @@ function parseWhile(s: ITokenStream): Ast.Node { * ``` */ function tryParseAssign(s: ITokenStream, dest: Ast.Node): Ast.Node | undefined { - const loc = s.token.loc; + const loc = s.getPos(); // Assign switch (s.getKind()) { case TokenKind.Eq: { s.next(); const expr = parseExpr(s, false); - return NODE('assign', { dest, expr }, loc, s.token.loc); + return NODE('assign', { dest, expr }, loc, s.getPos()); } case TokenKind.PlusEq: { s.next(); const expr = parseExpr(s, false); - return NODE('addAssign', { dest, expr }, loc, s.token.loc); + return NODE('addAssign', { dest, expr }, loc, s.getPos()); } case TokenKind.MinusEq: { s.next(); const expr = parseExpr(s, false); - return NODE('subAssign', { dest, expr }, loc, s.token.loc); + return NODE('subAssign', { dest, expr }, loc, s.getPos()); } default: { return; diff --git a/src/parser/syntaxes/toplevel.ts b/src/parser/syntaxes/toplevel.ts index a6342219..a1914f0f 100644 --- a/src/parser/syntaxes/toplevel.ts +++ b/src/parser/syntaxes/toplevel.ts @@ -48,7 +48,7 @@ export function parseTopLevel(s: ITokenStream): Ast.Node[] { break; } default: { - throw new AiScriptSyntaxError('Multiple statements cannot be placed on a single line.', s.token.loc); + throw new AiScriptSyntaxError('Multiple statements cannot be placed on a single line.', s.getPos()); } } } @@ -62,7 +62,7 @@ export function parseTopLevel(s: ITokenStream): Ast.Node[] { * ``` */ export function parseNamespace(s: ITokenStream): Ast.Node { - const startPos = s.token.loc; + const startPos = s.getPos(); s.nextWith(TokenKind.Colon2); @@ -104,13 +104,13 @@ export function parseNamespace(s: ITokenStream): Ast.Node { break; } default: { - throw new AiScriptSyntaxError('Multiple statements cannot be placed on a single line.', s.token.loc); + 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.token.loc); + return NODE('ns', { name, members }, startPos, s.getPos()); } /** @@ -119,7 +119,7 @@ export function parseNamespace(s: ITokenStream): Ast.Node { * ``` */ export function parseMeta(s: ITokenStream): Ast.Node { - const startPos = s.token.loc; + const startPos = s.getPos(); s.nextWith(TokenKind.Sharp3); From 4c16df598e5533bb4d4757572c05a9923b79d539 Mon Sep 17 00:00:00 2001 From: uzmoi Date: Sat, 20 Jul 2024 22:18:50 +0900 Subject: [PATCH 58/62] rename `loc` to `pos` on `Token` --- src/parser/scanner.ts | 2 +- src/parser/streams/token-stream.ts | 2 +- src/parser/syntaxes/expressions.ts | 6 +++--- src/parser/token.ts | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/parser/scanner.ts b/src/parser/scanner.ts index 9c28580c..830176eb 100644 --- a/src/parser/scanner.ts +++ b/src/parser/scanner.ts @@ -46,7 +46,7 @@ export class Scanner implements ITokenStream { * カーソル位置にあるトークンの位置情報を取得します。 */ public getPos(): TokenLocation { - return this.token.loc; + return this.token.pos; } /** diff --git a/src/parser/streams/token-stream.ts b/src/parser/streams/token-stream.ts index af7a41c7..d4033fb7 100644 --- a/src/parser/streams/token-stream.ts +++ b/src/parser/streams/token-stream.ts @@ -83,7 +83,7 @@ export class TokenStream implements ITokenStream { * カーソル位置にあるトークンの位置情報を取得します。 */ public getPos(): TokenLocation { - return this.token.loc; + return this.token.pos; } /** diff --git a/src/parser/syntaxes/expressions.ts b/src/parser/syntaxes/expressions.ts index 0e90b15c..9fd0b893 100644 --- a/src/parser/syntaxes/expressions.ts +++ b/src/parser/syntaxes/expressions.ts @@ -228,7 +228,7 @@ function parseAtom(s: ITokenStream, isStatic: boolean): Ast.Node { case TokenKind.TemplateStringElement: { // トークンの終了位置を取得するために先読み const nextToken = s.token.children![i + 1] ?? s.lookahead(1); - values.push(NODE('str', { value: element.value! }, element.loc, nextToken.loc)); + values.push(NODE('str', { value: element.value! }, element.pos, nextToken.pos)); break; } case TokenKind.TemplateExprElement: { @@ -236,13 +236,13 @@ function parseAtom(s: ITokenStream, isStatic: boolean): Ast.Node { 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.loc); + 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.loc); + throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[element.kind]}`, element.pos); } } } diff --git a/src/parser/token.ts b/src/parser/token.ts index 99af4099..d1633b68 100644 --- a/src/parser/token.ts +++ b/src/parser/token.ts @@ -114,7 +114,7 @@ export type TokenLocation = { column: number, line: number }; export class Token { constructor( public kind: TokenKind, - public loc: { column: number, line: number }, + public pos: TokenLocation, public hasLeftSpacing = false, /** for number literal, string literal */ public value?: string, From b9ec1ad650618a0880911a128aff186d0244908d Mon Sep 17 00:00:00 2001 From: uzmoi Date: Sat, 20 Jul 2024 22:22:43 +0900 Subject: [PATCH 59/62] add test for tmpl location --- test/index.ts | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/test/index.ts b/test/index.ts index a491abb5..5f3757cb 100644 --- a/test/index.ts +++ b/test/index.ts @@ -947,6 +947,34 @@ describe('Location', () => { if (!node.loc) assert.fail(); assert.deepEqual(node.loc.start, { line: 5, column: 3 }); }); + test.concurrent('template', async () => { + let node: Ast.Node; + const parser = new Parser(); + const nodes = parser.parse(` + \`hoge{1}fuga\` + `); + 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 }, + }); + }); }); describe('Unicode', () => { From eaaedbc652023397926d8be876d0a0568e9d06ee Mon Sep 17 00:00:00 2001 From: uzmoi Date: Sat, 20 Jul 2024 22:54:08 +0900 Subject: [PATCH 60/62] fix tmpl location --- src/parser/scanner.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/parser/scanner.ts b/src/parser/scanner.ts index 830176eb..b3c0d892 100644 --- a/src/parser/scanner.ts +++ b/src/parser/scanner.ts @@ -576,12 +576,14 @@ export class Scanner implements ITokenStream { } // 埋め込み式の終了 if ((this.stream.char as string) === '}') { - this.stream.next(); elements.push(TOKEN(TokenKind.TemplateExprElement, elementLoc, { hasLeftSpacing, children: tokenBuf })); - tokenBuf = []; // ここから文字列エレメントになるので位置を更新 elementLoc = this.stream.getPos(); + // TemplateExprElementトークンの終了位置をTokenStreamが取得するためのEOFトークンを追加 + tokenBuf.push(TOKEN(TokenKind.EOF, elementLoc)); + tokenBuf = []; state = 'string'; + this.stream.next(); break; } const token = this.readToken(); From a8fd9278046565f8fe5d2dfa96c75527affb0ad3 Mon Sep 17 00:00:00 2001 From: uzmoi Date: Sat, 20 Jul 2024 23:10:37 +0900 Subject: [PATCH 61/62] update api report --- etc/aiscript.api.md | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/etc/aiscript.api.md b/etc/aiscript.api.md index f0653e2d..bd332994 100644 --- a/etc/aiscript.api.md +++ b/etc/aiscript.api.md @@ -22,9 +22,9 @@ abstract class AiScriptError extends Error { // (undocumented) info?: any; // (undocumented) - loc?: Loc; - // (undocumented) name: string; + // (undocumented) + pos?: Pos; } // @public @@ -34,11 +34,11 @@ class AiScriptIndexOutOfRangeError extends AiScriptRuntimeError { // @public class AiScriptNamespaceError extends AiScriptError { - constructor(message: string, loc: Loc, info?: any); - // (undocumented) - loc: Loc; + constructor(message: string, pos: Pos, info?: any); // (undocumented) name: string; + // (undocumented) + pos: Pos; } // @public @@ -50,20 +50,20 @@ class AiScriptRuntimeError extends AiScriptError { // @public class AiScriptSyntaxError extends AiScriptError { - constructor(message: string, loc: Loc, info?: any); - // (undocumented) - loc: Loc; + constructor(message: string, pos: Pos, info?: any); // (undocumented) name: string; + // (undocumented) + pos: Pos; } // @public class AiScriptTypeError extends AiScriptError { - constructor(message: string, loc: Loc, info?: any); - // (undocumented) - loc: Loc; + constructor(message: string, pos: Pos, info?: any); // (undocumented) name: string; + // (undocumented) + pos: Pos; } // @public @@ -119,6 +119,7 @@ declare namespace Ast { export { isStatement, isExpression, + Pos, Loc, Node_2 as Node, Namespace, @@ -390,10 +391,10 @@ function isString(val: Value): val is VStr; // @public (undocumented) function jsToVal(val: any): Value; -// @public +// @public (undocumented) type Loc = { - line: number; - column: number; + start: Pos; + end: Pos; }; // @public (undocumented) @@ -503,6 +504,12 @@ export type ParserPlugin = (nodes: Ast.Node[]) => Ast.Node[]; // @public (undocumented) export type PluginType = 'validate' | 'transform'; +// @public +type Pos = { + line: number; + column: number; +}; + // @public (undocumented) type Prop = NodeBase & { type: 'prop'; From 2554b1b21d96971c97702adbfccc57b438beb59f Mon Sep 17 00:00:00 2001 From: uzmoi Date: Sat, 20 Jul 2024 23:44:05 +0900 Subject: [PATCH 62/62] =?UTF-8?q?rename=E6=BC=8F=E3=82=8C=E3=82=92?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/parser/scanner.ts | 168 ++++++++++++++--------------- src/parser/streams/token-stream.ts | 6 +- src/parser/syntaxes/statements.ts | 8 +- src/parser/token.ts | 8 +- test/interpreter.ts | 12 +-- test/parser.ts | 6 +- 6 files changed, 104 insertions(+), 104 deletions(-) diff --git a/src/parser/scanner.ts b/src/parser/scanner.ts index b3c0d892..4feb5ee7 100644 --- a/src/parser/scanner.ts +++ b/src/parser/scanner.ts @@ -3,7 +3,7 @@ import { CharStream } from './streams/char-stream.js'; import { TOKEN, TokenKind } from './token.js'; import type { ITokenStream } from './streams/token-stream.js'; -import type { Token, TokenLocation } from './token.js'; +import type { Token, TokenPosition } from './token.js'; const spaceChars = [' ', '\t']; const lineBreakChars = ['\r', '\n']; @@ -45,7 +45,7 @@ export class Scanner implements ITokenStream { /** * カーソル位置にあるトークンの位置情報を取得します。 */ - public getPos(): TokenLocation { + public getPos(): TokenPosition { return this.token.pos; } @@ -112,11 +112,11 @@ export class Scanner implements ITokenStream { } // トークン位置を記憶 - const loc = this.stream.getPos(); + const pos = this.stream.getPos(); if (lineBreakChars.includes(this.stream.char)) { this.stream.next(); - token = TOKEN(TokenKind.NewLine, loc, { hasLeftSpacing }); + token = TOKEN(TokenKind.NewLine, pos, { hasLeftSpacing }); return token; } switch (this.stream.char) { @@ -124,9 +124,9 @@ export class Scanner implements ITokenStream { this.stream.next(); if (!this.stream.eof && (this.stream.char as string) === '=') { this.stream.next(); - token = TOKEN(TokenKind.NotEq, loc, { hasLeftSpacing }); + token = TOKEN(TokenKind.NotEq, pos, { hasLeftSpacing }); } else { - token = TOKEN(TokenKind.Not, loc, { hasLeftSpacing }); + token = TOKEN(TokenKind.Not, pos, { hasLeftSpacing }); } break; } @@ -141,72 +141,72 @@ export class Scanner implements ITokenStream { this.stream.next(); if (!this.stream.eof && (this.stream.char as string) === '#') { this.stream.next(); - token = TOKEN(TokenKind.Sharp3, loc, { hasLeftSpacing }); + token = TOKEN(TokenKind.Sharp3, pos, { hasLeftSpacing }); } } else if (!this.stream.eof && (this.stream.char as string) === '[') { this.stream.next(); - token = TOKEN(TokenKind.OpenSharpBracket, loc, { hasLeftSpacing }); + token = TOKEN(TokenKind.OpenSharpBracket, pos, { hasLeftSpacing }); } else { - throw new AiScriptSyntaxError('invalid character: "#"', loc); + throw new AiScriptSyntaxError('invalid character: "#"', pos); } break; } case '%': { this.stream.next(); - token = TOKEN(TokenKind.Percent, loc, { hasLeftSpacing }); + 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, loc, { hasLeftSpacing }); + token = TOKEN(TokenKind.And2, pos, { hasLeftSpacing }); } break; } case '(': { this.stream.next(); - token = TOKEN(TokenKind.OpenParen, loc, { hasLeftSpacing }); + token = TOKEN(TokenKind.OpenParen, pos, { hasLeftSpacing }); break; } case ')': { this.stream.next(); - token = TOKEN(TokenKind.CloseParen, loc, { hasLeftSpacing }); + token = TOKEN(TokenKind.CloseParen, pos, { hasLeftSpacing }); break; } case '*': { this.stream.next(); - token = TOKEN(TokenKind.Asterisk, loc, { hasLeftSpacing }); + 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, loc, { hasLeftSpacing }); + token = TOKEN(TokenKind.PlusEq, pos, { hasLeftSpacing }); } else { - token = TOKEN(TokenKind.Plus, loc, { hasLeftSpacing }); + token = TOKEN(TokenKind.Plus, pos, { hasLeftSpacing }); } break; } case ',': { this.stream.next(); - token = TOKEN(TokenKind.Comma, loc, { hasLeftSpacing }); + 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, loc, { hasLeftSpacing }); + token = TOKEN(TokenKind.MinusEq, pos, { hasLeftSpacing }); } else { - token = TOKEN(TokenKind.Minus, loc, { hasLeftSpacing }); + token = TOKEN(TokenKind.Minus, pos, { hasLeftSpacing }); } break; } case '.': { this.stream.next(); - token = TOKEN(TokenKind.Dot, loc, { hasLeftSpacing }); + token = TOKEN(TokenKind.Dot, pos, { hasLeftSpacing }); break; } case '/': { @@ -220,7 +220,7 @@ export class Scanner implements ITokenStream { this.skipCommentLine(); continue; } else { - token = TOKEN(TokenKind.Slash, loc, { hasLeftSpacing }); + token = TOKEN(TokenKind.Slash, pos, { hasLeftSpacing }); } break; } @@ -228,27 +228,27 @@ export class Scanner implements ITokenStream { this.stream.next(); if (!this.stream.eof && (this.stream.char as string) === ':') { this.stream.next(); - token = TOKEN(TokenKind.Colon2, loc, { hasLeftSpacing }); + token = TOKEN(TokenKind.Colon2, pos, { hasLeftSpacing }); } else { - token = TOKEN(TokenKind.Colon, loc, { hasLeftSpacing }); + token = TOKEN(TokenKind.Colon, pos, { hasLeftSpacing }); } break; } case ';': { this.stream.next(); - token = TOKEN(TokenKind.SemiColon, loc, { hasLeftSpacing }); + 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, loc, { hasLeftSpacing }); + token = TOKEN(TokenKind.LtEq, pos, { hasLeftSpacing }); } else if (!this.stream.eof && (this.stream.char as string) === ':') { this.stream.next(); - token = TOKEN(TokenKind.Out, loc, { hasLeftSpacing }); + token = TOKEN(TokenKind.Out, pos, { hasLeftSpacing }); } else { - token = TOKEN(TokenKind.Lt, loc, { hasLeftSpacing }); + token = TOKEN(TokenKind.Lt, pos, { hasLeftSpacing }); } break; } @@ -256,12 +256,12 @@ export class Scanner implements ITokenStream { this.stream.next(); if (!this.stream.eof && (this.stream.char as string) === '=') { this.stream.next(); - token = TOKEN(TokenKind.Eq2, loc, { hasLeftSpacing }); + token = TOKEN(TokenKind.Eq2, pos, { hasLeftSpacing }); } else if (!this.stream.eof && (this.stream.char as string) === '>') { this.stream.next(); - token = TOKEN(TokenKind.Arrow, loc, { hasLeftSpacing }); + token = TOKEN(TokenKind.Arrow, pos, { hasLeftSpacing }); } else { - token = TOKEN(TokenKind.Eq, loc, { hasLeftSpacing }); + token = TOKEN(TokenKind.Eq, pos, { hasLeftSpacing }); } break; } @@ -269,40 +269,40 @@ export class Scanner implements ITokenStream { this.stream.next(); if (!this.stream.eof && (this.stream.char as string) === '=') { this.stream.next(); - token = TOKEN(TokenKind.GtEq, loc, { hasLeftSpacing }); + token = TOKEN(TokenKind.GtEq, pos, { hasLeftSpacing }); } else { - token = TOKEN(TokenKind.Gt, loc, { hasLeftSpacing }); + token = TOKEN(TokenKind.Gt, pos, { hasLeftSpacing }); } break; } case '?': { this.stream.next(); - token = TOKEN(TokenKind.Question, loc, { hasLeftSpacing }); + token = TOKEN(TokenKind.Question, pos, { hasLeftSpacing }); break; } case '@': { this.stream.next(); - token = TOKEN(TokenKind.At, loc, { hasLeftSpacing }); + token = TOKEN(TokenKind.At, pos, { hasLeftSpacing }); break; } case '[': { this.stream.next(); - token = TOKEN(TokenKind.OpenBracket, loc, { hasLeftSpacing }); + token = TOKEN(TokenKind.OpenBracket, pos, { hasLeftSpacing }); break; } case '\\': { this.stream.next(); - token = TOKEN(TokenKind.BackSlash, loc, { hasLeftSpacing }); + token = TOKEN(TokenKind.BackSlash, pos, { hasLeftSpacing }); break; } case ']': { this.stream.next(); - token = TOKEN(TokenKind.CloseBracket, loc, { hasLeftSpacing }); + token = TOKEN(TokenKind.CloseBracket, pos, { hasLeftSpacing }); break; } case '^': { this.stream.next(); - token = TOKEN(TokenKind.Hat, loc, { hasLeftSpacing }); + token = TOKEN(TokenKind.Hat, pos, { hasLeftSpacing }); break; } case '`': { @@ -311,20 +311,20 @@ export class Scanner implements ITokenStream { } case '{': { this.stream.next(); - token = TOKEN(TokenKind.OpenBrace, loc, { hasLeftSpacing }); + 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, loc, { hasLeftSpacing }); + token = TOKEN(TokenKind.Or2, pos, { hasLeftSpacing }); } break; } case '}': { this.stream.next(); - token = TOKEN(TokenKind.CloseBrace, loc, { hasLeftSpacing }); + token = TOKEN(TokenKind.CloseBrace, pos, { hasLeftSpacing }); break; } } @@ -339,7 +339,7 @@ export class Scanner implements ITokenStream { token = wordToken; break; } - throw new AiScriptSyntaxError(`invalid character: "${this.stream.char}"`, loc); + throw new AiScriptSyntaxError(`invalid character: "${this.stream.char}"`, pos); } break; } @@ -350,7 +350,7 @@ export class Scanner implements ITokenStream { // read a word let value = ''; - const loc = this.stream.getPos(); + const pos = this.stream.getPos(); while (!this.stream.eof && wordChar.test(this.stream.char)) { value += this.stream.char; @@ -362,70 +362,70 @@ export class Scanner implements ITokenStream { // check word kind switch (value) { case 'null': { - return TOKEN(TokenKind.NullKeyword, loc, { hasLeftSpacing }); + return TOKEN(TokenKind.NullKeyword, pos, { hasLeftSpacing }); } case 'true': { - return TOKEN(TokenKind.TrueKeyword, loc, { hasLeftSpacing }); + return TOKEN(TokenKind.TrueKeyword, pos, { hasLeftSpacing }); } case 'false': { - return TOKEN(TokenKind.FalseKeyword, loc, { hasLeftSpacing }); + return TOKEN(TokenKind.FalseKeyword, pos, { hasLeftSpacing }); } case 'each': { - return TOKEN(TokenKind.EachKeyword, loc, { hasLeftSpacing }); + return TOKEN(TokenKind.EachKeyword, pos, { hasLeftSpacing }); } case 'for': { - return TOKEN(TokenKind.ForKeyword, loc, { hasLeftSpacing }); + return TOKEN(TokenKind.ForKeyword, pos, { hasLeftSpacing }); } case 'loop': { - return TOKEN(TokenKind.LoopKeyword, loc, { hasLeftSpacing }); + return TOKEN(TokenKind.LoopKeyword, pos, { hasLeftSpacing }); } case 'do': { - return TOKEN(TokenKind.DoKeyword, loc, { hasLeftSpacing }); + return TOKEN(TokenKind.DoKeyword, pos, { hasLeftSpacing }); } case 'while': { - return TOKEN(TokenKind.WhileKeyword, loc, { hasLeftSpacing }); + return TOKEN(TokenKind.WhileKeyword, pos, { hasLeftSpacing }); } case 'break': { - return TOKEN(TokenKind.BreakKeyword, loc, { hasLeftSpacing }); + return TOKEN(TokenKind.BreakKeyword, pos, { hasLeftSpacing }); } case 'continue': { - return TOKEN(TokenKind.ContinueKeyword, loc, { hasLeftSpacing }); + return TOKEN(TokenKind.ContinueKeyword, pos, { hasLeftSpacing }); } case 'match': { - return TOKEN(TokenKind.MatchKeyword, loc, { hasLeftSpacing }); + return TOKEN(TokenKind.MatchKeyword, pos, { hasLeftSpacing }); } case 'case': { - return TOKEN(TokenKind.CaseKeyword, loc, { hasLeftSpacing }); + return TOKEN(TokenKind.CaseKeyword, pos, { hasLeftSpacing }); } case 'default': { - return TOKEN(TokenKind.DefaultKeyword, loc, { hasLeftSpacing }); + return TOKEN(TokenKind.DefaultKeyword, pos, { hasLeftSpacing }); } case 'if': { - return TOKEN(TokenKind.IfKeyword, loc, { hasLeftSpacing }); + return TOKEN(TokenKind.IfKeyword, pos, { hasLeftSpacing }); } case 'elif': { - return TOKEN(TokenKind.ElifKeyword, loc, { hasLeftSpacing }); + return TOKEN(TokenKind.ElifKeyword, pos, { hasLeftSpacing }); } case 'else': { - return TOKEN(TokenKind.ElseKeyword, loc, { hasLeftSpacing }); + return TOKEN(TokenKind.ElseKeyword, pos, { hasLeftSpacing }); } case 'return': { - return TOKEN(TokenKind.ReturnKeyword, loc, { hasLeftSpacing }); + return TOKEN(TokenKind.ReturnKeyword, pos, { hasLeftSpacing }); } case 'eval': { - return TOKEN(TokenKind.EvalKeyword, loc, { hasLeftSpacing }); + return TOKEN(TokenKind.EvalKeyword, pos, { hasLeftSpacing }); } case 'var': { - return TOKEN(TokenKind.VarKeyword, loc, { hasLeftSpacing }); + return TOKEN(TokenKind.VarKeyword, pos, { hasLeftSpacing }); } case 'let': { - return TOKEN(TokenKind.LetKeyword, loc, { hasLeftSpacing }); + return TOKEN(TokenKind.LetKeyword, pos, { hasLeftSpacing }); } case 'exists': { - return TOKEN(TokenKind.ExistsKeyword, loc, { hasLeftSpacing }); + return TOKEN(TokenKind.ExistsKeyword, pos, { hasLeftSpacing }); } default: { - return TOKEN(TokenKind.Identifier, loc, { hasLeftSpacing, value }); + return TOKEN(TokenKind.Identifier, pos, { hasLeftSpacing, value }); } } } @@ -434,7 +434,7 @@ export class Scanner implements ITokenStream { let wholeNumber = ''; let fractional = ''; - const loc = this.stream.getPos(); + const pos = this.stream.getPos(); while (!this.stream.eof && digit.test(this.stream.char)) { wholeNumber += this.stream.char; @@ -450,7 +450,7 @@ export class Scanner implements ITokenStream { this.stream.next(); } if (fractional.length === 0) { - throw new AiScriptSyntaxError('digit expected', loc); + throw new AiScriptSyntaxError('digit expected', pos); } } let value; @@ -459,7 +459,7 @@ export class Scanner implements ITokenStream { } else { value = wholeNumber; } - return TOKEN(TokenKind.NumberLiteral, loc, { hasLeftSpacing, value }); + return TOKEN(TokenKind.NumberLiteral, pos, { hasLeftSpacing, value }); } private readStringLiteral(hasLeftSpacing: boolean): Token { @@ -467,14 +467,14 @@ export class Scanner implements ITokenStream { const literalMark = this.stream.char; let state: 'string' | 'escape' | 'finish' = 'string'; - const loc = this.stream.getPos(); + const pos = this.stream.getPos(); this.stream.next(); while (state !== 'finish') { switch (state) { case 'string': { if (this.stream.eof) { - throw new AiScriptSyntaxError('unexpected EOF', loc); + throw new AiScriptSyntaxError('unexpected EOF', pos); } if (this.stream.char === '\\') { this.stream.next(); @@ -492,7 +492,7 @@ export class Scanner implements ITokenStream { } case 'escape': { if (this.stream.eof) { - throw new AiScriptSyntaxError('unexpected EOF', loc); + throw new AiScriptSyntaxError('unexpected EOF', pos); } value += this.stream.char; this.stream.next(); @@ -501,7 +501,7 @@ export class Scanner implements ITokenStream { } } } - return TOKEN(TokenKind.StringLiteral, loc, { hasLeftSpacing, value }); + return TOKEN(TokenKind.StringLiteral, pos, { hasLeftSpacing, value }); } private readTemplate(hasLeftSpacing: boolean): Token { @@ -510,8 +510,8 @@ export class Scanner implements ITokenStream { let tokenBuf: Token[] = []; let state: 'string' | 'escape' | 'expr' | 'finish' = 'string'; - const loc = this.stream.getPos(); - let elementLoc = loc; + const pos = this.stream.getPos(); + let elementPos = pos; this.stream.next(); while (state !== 'finish') { @@ -519,7 +519,7 @@ export class Scanner implements ITokenStream { case 'string': { // テンプレートの終了が無いままEOFに達した if (this.stream.eof) { - throw new AiScriptSyntaxError('unexpected EOF', loc); + throw new AiScriptSyntaxError('unexpected EOF', pos); } // エスケープ if (this.stream.char === '\\') { @@ -531,7 +531,7 @@ export class Scanner implements ITokenStream { if (this.stream.char === '`') { this.stream.next(); if (buf.length > 0) { - elements.push(TOKEN(TokenKind.TemplateStringElement, elementLoc, { hasLeftSpacing, value: buf })); + elements.push(TOKEN(TokenKind.TemplateStringElement, elementPos, { hasLeftSpacing, value: buf })); } state = 'finish'; break; @@ -540,11 +540,11 @@ export class Scanner implements ITokenStream { if (this.stream.char === '{') { this.stream.next(); if (buf.length > 0) { - elements.push(TOKEN(TokenKind.TemplateStringElement, elementLoc, { hasLeftSpacing, value: buf })); + elements.push(TOKEN(TokenKind.TemplateStringElement, elementPos, { hasLeftSpacing, value: buf })); buf = ''; } // ここから式エレメントになるので位置を更新 - elementLoc = this.stream.getPos(); + elementPos = this.stream.getPos(); state = 'expr'; break; } @@ -555,7 +555,7 @@ export class Scanner implements ITokenStream { case 'escape': { // エスケープ対象の文字が無いままEOFに達した if (this.stream.eof) { - throw new AiScriptSyntaxError('unexpected EOF', loc); + throw new AiScriptSyntaxError('unexpected EOF', pos); } // 普通の文字として取り込み buf += this.stream.char; @@ -567,7 +567,7 @@ export class Scanner implements ITokenStream { case 'expr': { // 埋め込み式の終端記号が無いままEOFに達した if (this.stream.eof) { - throw new AiScriptSyntaxError('unexpected EOF', loc); + throw new AiScriptSyntaxError('unexpected EOF', pos); } // skip spasing if (spaceChars.includes(this.stream.char)) { @@ -576,11 +576,11 @@ export class Scanner implements ITokenStream { } // 埋め込み式の終了 if ((this.stream.char as string) === '}') { - elements.push(TOKEN(TokenKind.TemplateExprElement, elementLoc, { hasLeftSpacing, children: tokenBuf })); + elements.push(TOKEN(TokenKind.TemplateExprElement, elementPos, { hasLeftSpacing, children: tokenBuf })); // ここから文字列エレメントになるので位置を更新 - elementLoc = this.stream.getPos(); + elementPos = this.stream.getPos(); // TemplateExprElementトークンの終了位置をTokenStreamが取得するためのEOFトークンを追加 - tokenBuf.push(TOKEN(TokenKind.EOF, elementLoc)); + tokenBuf.push(TOKEN(TokenKind.EOF, elementPos)); tokenBuf = []; state = 'string'; this.stream.next(); @@ -593,7 +593,7 @@ export class Scanner implements ITokenStream { } } - return TOKEN(TokenKind.Template, loc, { hasLeftSpacing, children: elements }); + return TOKEN(TokenKind.Template, pos, { hasLeftSpacing, children: elements }); } private skipCommentLine(): void { diff --git a/src/parser/streams/token-stream.ts b/src/parser/streams/token-stream.ts index d4033fb7..d6d3f68b 100644 --- a/src/parser/streams/token-stream.ts +++ b/src/parser/streams/token-stream.ts @@ -1,6 +1,6 @@ import { AiScriptSyntaxError } from '../../error.js'; import { TOKEN, TokenKind } from '../token.js'; -import type { Token, TokenLocation } from '../token.js'; +import type { Token, TokenPosition } from '../token.js'; /** * トークンの読み取りに関するインターフェース @@ -19,7 +19,7 @@ export interface ITokenStream { /** * カーソル位置にあるトークンの位置情報を取得します。 */ - getPos(): TokenLocation; + getPos(): TokenPosition; /** * カーソル位置を次のトークンへ進めます。 @@ -82,7 +82,7 @@ export class TokenStream implements ITokenStream { /** * カーソル位置にあるトークンの位置情報を取得します。 */ - public getPos(): TokenLocation { + public getPos(): TokenPosition { return this.token.pos; } diff --git a/src/parser/syntaxes/statements.ts b/src/parser/syntaxes/statements.ts index 86e2d030..2c50138e 100644 --- a/src/parser/syntaxes/statements.ts +++ b/src/parser/syntaxes/statements.ts @@ -443,24 +443,24 @@ function parseWhile(s: ITokenStream): Ast.Node { * ``` */ function tryParseAssign(s: ITokenStream, dest: Ast.Node): Ast.Node | undefined { - const loc = s.getPos(); + const startPos = s.getPos(); // Assign switch (s.getKind()) { case TokenKind.Eq: { s.next(); const expr = parseExpr(s, false); - return NODE('assign', { dest, expr }, loc, s.getPos()); + return NODE('assign', { dest, expr }, startPos, s.getPos()); } case TokenKind.PlusEq: { s.next(); const expr = parseExpr(s, false); - return NODE('addAssign', { dest, expr }, loc, s.getPos()); + return NODE('addAssign', { dest, expr }, startPos, s.getPos()); } case TokenKind.MinusEq: { s.next(); const expr = parseExpr(s, false); - return NODE('subAssign', { dest, expr }, loc, s.getPos()); + return NODE('subAssign', { dest, expr }, startPos, s.getPos()); } default: { return; diff --git a/src/parser/token.ts b/src/parser/token.ts index d1633b68..d4bdaf49 100644 --- a/src/parser/token.ts +++ b/src/parser/token.ts @@ -109,12 +109,12 @@ export enum TokenKind { CloseBrace, } -export type TokenLocation = { column: number, line: number }; +export type TokenPosition = { column: number, line: number }; export class Token { constructor( public kind: TokenKind, - public pos: TokenLocation, + public pos: TokenPosition, public hasLeftSpacing = false, /** for number literal, string literal */ public value?: string, @@ -127,6 +127,6 @@ export class Token { * - opts.value: for number literal, string literal * - opts.children: for template syntax */ -export function TOKEN(kind: TokenKind, loc: TokenLocation, opts?: { hasLeftSpacing?: boolean, value?: Token['value'], children?: Token['children'] }): Token { - return new Token(kind, loc, opts?.hasLeftSpacing, opts?.value, opts?.children); +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/test/interpreter.ts b/test/interpreter.ts index 41b64013..446613c8 100644 --- a/test/interpreter.ts +++ b/test/interpreter.ts @@ -66,7 +66,7 @@ describe('error handler', () => { }); describe('error location', () => { - const exeAndGetErrLoc = (src: string): Promise => new Promise((ok, ng) => { + const exeAndGetErrPos = (src: string): Promise => new Promise((ok, ng) => { const aiscript = new Interpreter({ emitError: FN_NATIVE((_args, _opts) => { throw Error('emitError'); @@ -78,14 +78,14 @@ describe('error location', () => { }); test.concurrent('Non-aiscript Error', async () => { - return expect(exeAndGetErrLoc(`/* (の位置 + return expect(exeAndGetErrPos(`/* (の位置 */ emitError() `)).resolves.toEqual({ line: 3, column: 13}); }); test.concurrent('No "var" in namespace declaration', async () => { - return expect(exeAndGetErrLoc(`// vの位置 + return expect(exeAndGetErrPos(`// vの位置 :: Ai { let chan = 'kawaii' var kun = '!?' @@ -94,14 +94,14 @@ describe('error location', () => { }); test.concurrent('Index out of range', async () => { - return expect(exeAndGetErrLoc(`// [の位置 + return expect(exeAndGetErrPos(`// [の位置 let arr = [] arr[0] `)).resolves.toEqual({ line: 3, column: 7}); }); test.concurrent('Error in passed function', async () => { - return expect(exeAndGetErrLoc(`// /の位置 + return expect(exeAndGetErrPos(`// /の位置 [0, 1, 2].map(@(v){ 0/v }) @@ -109,7 +109,7 @@ describe('error location', () => { }); test.concurrent('No such prop', async () => { - return expect(exeAndGetErrLoc(`// .の位置 + return expect(exeAndGetErrPos(`// .の位置 [].ai `)).resolves.toEqual({ line: 2, column: 6}); }); diff --git a/test/parser.ts b/test/parser.ts index 893fff91..8fc188da 100644 --- a/test/parser.ts +++ b/test/parser.ts @@ -1,6 +1,6 @@ import * as assert from 'assert'; import { Scanner } from '../src/parser/scanner'; -import { TOKEN, TokenKind, TokenLocation } from '../src/parser/token'; +import { TOKEN, TokenKind, TokenPosition } from '../src/parser/token'; import { CharStream } from '../src/parser/streams/char-stream'; describe('CharStream', () => { @@ -77,8 +77,8 @@ describe('Scanner', () => { const stream = new Scanner(source); return stream; } - function next(stream: Scanner, kind: TokenKind, loc: TokenLocation, opts: { hasLeftSpacing?: boolean, value?: string }) { - assert.deepStrictEqual(stream.token, TOKEN(kind, loc, opts)); + function next(stream: Scanner, kind: TokenKind, pos: TokenPosition, opts: { hasLeftSpacing?: boolean, value?: string }) { + assert.deepStrictEqual(stream.token, TOKEN(kind, pos, opts)); stream.next(); }