diff --git a/etc/aiscript.api.md b/etc/aiscript.api.md index f7b63bf4..a3a6ffb5 100644 --- a/etc/aiscript.api.md +++ b/etc/aiscript.api.md @@ -145,16 +145,20 @@ declare namespace Ast { Statement, Definition, Attribute, - Return, + AddAssign, + SubAssign, + Assign, + Expression, + ControlFlow, + If, + Match, + Block, Each, For, Loop, Break, Continue, - AddAssign, - SubAssign, - Assign, - Expression, + Return, Plus, Minus, Not, @@ -172,10 +176,7 @@ declare namespace Ast { Neq, And, Or, - If, Fn, - Match, - Block, Exists, Tmpl, Str, @@ -213,6 +214,7 @@ type Attribute = NodeBase & { // @public (undocumented) type Block = NodeBase & { type: 'block'; + label?: string; statements: (Statement | Expression)[]; }; @@ -228,6 +230,8 @@ type Bool = NodeBase & { // @public (undocumented) type Break = NodeBase & { type: 'break'; + label?: string; + expr?: Expression; }; // @public (undocumented) @@ -240,8 +244,12 @@ type Call = NodeBase & { // @public (undocumented) type Continue = NodeBase & { type: 'continue'; + label?: string; }; +// @public (undocumented) +type ControlFlow = If | Match | Block | Each | For | Loop; + // @public (undocumented) type Definition = NodeBase & { type: 'def'; @@ -262,9 +270,10 @@ type Div = NodeBase & { // @public (undocumented) type Each = NodeBase & { type: 'each'; + label?: string; var: Expression; items: Expression; - for: Statement | Expression; + for: Block; }; // @public (undocumented) @@ -306,7 +315,7 @@ type Exists = NodeBase & { 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 | Plus | Minus | Not | Pow | Mul | Div | Rem | Add | Sub | Lt | Lteq | Gt | Gteq | Eq | Neq | And | Or | Identifier | Call | Index | Prop; +type Expression = ControlFlow | Return | Break | Continue | Fn | Exists | Tmpl | Str | Num | Bool | Null | Obj | Arr | Plus | Minus | Not | Pow | Mul | Div | Rem | Add | Sub | Lt | Lteq | Gt | Gteq | Eq | Neq | And | Or | Identifier | Call | Index | Prop; // @public (undocumented) const FALSE: { @@ -343,11 +352,12 @@ type FnTypeSource = NodeBase & { // @public (undocumented) type For = NodeBase & { type: 'for'; + label?: string; var?: string; from?: Expression; to?: Expression; times?: Expression; - for: Statement | Expression; + for: Block; }; // @public (undocumented) @@ -376,13 +386,14 @@ type Identifier = NodeBase & { // @public (undocumented) type If = NodeBase & { type: 'if'; + label?: string; cond: Expression; - then: Statement | Expression; + then: Block; elseif: { cond: Expression; - then: Statement | Expression; + then: Block; }[]; - else?: Statement | Expression; + else?: Block; }; // @public (undocumented) @@ -475,6 +486,7 @@ type Loc = { // @public (undocumented) type Loop = NodeBase & { type: 'loop'; + label?: string; statements: (Statement | Expression)[]; }; @@ -495,12 +507,13 @@ type Lteq = NodeBase & { // @public (undocumented) type Match = NodeBase & { type: 'match'; + label?: string; about: Expression; qs: { q: Expression; - a: Statement | Expression; + a: Expression; }[]; - default?: Statement | Expression; + default?: Expression; }; // @public (undocumented) @@ -651,7 +664,7 @@ function reprValue(value: Value, literalLike?: boolean, processedObjects?: Set VStr; diff --git a/src/interpreter/control.ts b/src/interpreter/control.ts index e748344e..4eb520b7 100644 --- a/src/interpreter/control.ts +++ b/src/interpreter/control.ts @@ -1,6 +1,6 @@ import { AiScriptRuntimeError } from '../error.js'; +import { NULL, type Value } from './value.js'; import type { Reference } from './reference.js'; -import type { Value } from './value.js'; export type CReturn = { type: 'return'; @@ -9,11 +9,13 @@ export type CReturn = { export type CBreak = { type: 'break'; - value: null; + label?: string; + value: Value; }; export type CContinue = { type: 'continue'; + label?: string; value: null; }; @@ -25,13 +27,15 @@ export const RETURN = (v: CReturn['value']): CReturn => ({ value: v, }); -export const BREAK = (): CBreak => ({ +export const BREAK = (v: CBreak['value'], label?: string): CBreak => ({ type: 'break' as const, - value: null, + label, + value: v, }); -export const CONTINUE = (): CContinue => ({ +export const CONTINUE = (label?: string): CContinue => ({ type: 'continue' as const, + label, value: null, }); @@ -46,6 +50,26 @@ export function unWrapRet(v: Value | Control): Value { } } +/** + * 値がbreakで、ラベルが指定されていないまたは一致する場合のみ、その中身を取り出します。 + */ +export function unWrapBreak(v: Value | Control, label: string | undefined): Value | Control { + if (v.type === 'break' && (v.label == null || v.label === label)) { + return v.value; + } + return v; +} + +/** + * 値がbreakで、ラベルが一致する場合のみ、その中身を取り出します。 + */ +export function unWrapLabeledBreak(v: Value | Control, label: string | undefined): Value | Control { + if (v.type === 'break' && v.label != null && v.label === label) { + return v.value; + } + return v; +} + export function assertValue(v: Value | Control): asserts v is Value { switch (v.type) { case 'return': diff --git a/src/interpreter/index.ts b/src/interpreter/index.ts index 2d89264f..da36ee1e 100644 --- a/src/interpreter/index.ts +++ b/src/interpreter/index.ts @@ -4,15 +4,15 @@ import { autobind } from '../utils/mini-autobind.js'; import { AiScriptError, NonAiScriptError, AiScriptNamespaceError, AiScriptIndexOutOfRangeError, AiScriptRuntimeError, AiScriptHostsideError } from '../error.js'; -import * as Ast from '../node.js'; import { Scope } from './scope.js'; import { std } from './lib/std.js'; -import { RETURN, unWrapRet, BREAK, CONTINUE, assertValue, isControl, type Control } from './control.js'; +import { RETURN, unWrapRet, BREAK, CONTINUE, assertValue, isControl, type Control, unWrapLabeledBreak, unWrapBreak } from './control.js'; import { assertNumber, assertString, assertFunction, assertBoolean, assertObject, assertArray, eq, isObject, isArray, expectAny, reprValue, isFunction } from './util.js'; import { NULL, FN_NATIVE, BOOL, NUM, STR, ARR, OBJ, FN, ERROR } from './value.js'; import { getPrimProp } from './primitive-props.js'; import { Variable } from './variable.js'; import { Reference } from './reference.js'; +import type * as Ast from '../node.js'; import type { JsValue } from './util.js'; import type { Value, VFn, VUserFn } from './value.js'; @@ -292,11 +292,6 @@ export class Interpreter { } } - @autobind - private _evalClause(node: Ast.Statement | Ast.Expression, scope: Scope, callStack: readonly CallInfo[]): Promise { - return this._eval(node, Ast.isStatement(node) ? scope.createChildScope() : scope, callStack); - } - @autobind private async _evalBinaryOperation(op: string, leftExpr: Ast.Expression, rightExpr: Ast.Expression, scope: Scope, callStack: readonly CallInfo[]): Promise { const callee = scope.get(op); @@ -367,24 +362,24 @@ export class Interpreter { case 'if': { const cond = await this._eval(node.cond, scope, callStack); if (isControl(cond)) { - return cond; + return unWrapLabeledBreak(cond, node.label); } assertBoolean(cond); if (cond.value) { - return this._evalClause(node.then, scope, callStack); + return unWrapLabeledBreak(await this._eval(node.then, scope, callStack), node.label); } for (const elseif of node.elseif) { const cond = await this._eval(elseif.cond, scope, callStack); if (isControl(cond)) { - return cond; + return unWrapLabeledBreak(cond, node.label); } assertBoolean(cond); if (cond.value) { - return this._evalClause(elseif.then, scope, callStack); + return unWrapLabeledBreak(await this._eval(elseif.then, scope, callStack), node.label); } } if (node.else) { - return this._evalClause(node.else, scope, callStack); + return unWrapLabeledBreak(await this._eval(node.else, scope, callStack), node.label); } return NULL; } @@ -392,19 +387,19 @@ export class Interpreter { case 'match': { const about = await this._eval(node.about, scope, callStack); if (isControl(about)) { - return about; + return unWrapLabeledBreak(about, node.label); } for (const qa of node.qs) { const q = await this._eval(qa.q, scope, callStack); if (isControl(q)) { - return q; + return unWrapLabeledBreak(q, node.label); } if (eq(about, q)) { - return await this._evalClause(qa.a, scope, callStack); + return unWrapLabeledBreak(await this._eval(qa.a, scope, callStack), node.label); } } if (node.default) { - return await this._evalClause(node.default, scope, callStack); + return unWrapLabeledBreak(await this._eval(node.default, scope, callStack), node.label); } return NULL; } @@ -414,25 +409,32 @@ export class Interpreter { while (true) { const v = await this._run(node.statements, scope.createChildScope(), callStack); if (v.type === 'break') { - break; + return unWrapBreak(v, node.label); + } else if (v.type === 'continue') { + if (v.label != null && v.label !== node.label) { + return v; + } } else if (v.type === 'return') { return v; } } - return NULL; } case 'for': { if (node.times) { const times = await this._eval(node.times, scope, callStack); if (isControl(times)) { - return times; + return unWrapBreak(times, node.label); } assertNumber(times); for (let i = 0; i < times.value; i++) { - const v = await this._evalClause(node.for, scope, callStack); + const v = await this._eval(node.for, scope, callStack); if (v.type === 'break') { - break; + return unWrapBreak(v, node.label); + } else if (v.type === 'continue') { + if (v.label != null && v.label !== node.label) { + return v; + } } else if (v.type === 'return') { return v; } @@ -440,11 +442,11 @@ export class Interpreter { } else { const from = await this._eval(node.from!, scope, callStack); if (isControl(from)) { - return from; + return unWrapBreak(from, node.label); } const to = await this._eval(node.to!, scope, callStack); if (isControl(to)) { - return to; + return unWrapBreak(to, node.label); } assertNumber(from); assertNumber(to); @@ -456,7 +458,11 @@ export class Interpreter { }], ])), callStack); if (v.type === 'break') { - break; + return unWrapBreak(v, node.label); + } else if (v.type === 'continue') { + if (v.label != null && v.label !== node.label) { + return v; + } } else if (v.type === 'return') { return v; } @@ -468,7 +474,7 @@ export class Interpreter { case 'each': { const items = await this._eval(node.items, scope, callStack); if (isControl(items)) { - return items; + return unWrapBreak(items, node.label); } assertArray(items); for (const item of items.value) { @@ -476,7 +482,11 @@ export class Interpreter { this.define(eachScope, node.var, item, false); const v = await this._eval(node.for, eachScope, callStack); if (v.type === 'break') { - break; + return unWrapBreak(v, node.label); + } else if (v.type === 'continue') { + if (v.label != null && v.label !== node.label) { + return v; + } } else if (v.type === 'return') { return v; } @@ -695,7 +705,7 @@ export class Interpreter { } case 'block': { - return this._run(node.statements, scope.createChildScope(), callStack); + return unWrapLabeledBreak(await this._run(node.statements, scope.createChildScope(), callStack), node.label); } case 'exists': { @@ -719,9 +729,13 @@ export class Interpreter { } case 'return': { - const val = await this._eval(node.expr, scope, callStack); - if (isControl(val)) { - return val; + let val: Value = NULL; + if (node.expr != null) { + const valOrControl = await this._eval(node.expr, scope, callStack); + if (isControl(valOrControl)) { + return valOrControl; + } + val = valOrControl; } this.log('block:return', { scope: scope.name, val: val }); return RETURN(val); @@ -729,12 +743,20 @@ export class Interpreter { case 'break': { this.log('block:break', { scope: scope.name }); - return BREAK(); + if (node.expr != null) { + const val = await this._eval(node.expr, scope, callStack); + if (isControl(val)) { + return val; + } + return BREAK(val, node.label); + } else { + return BREAK(NULL, node.label); + } } case 'continue': { this.log('block:continue', { scope: scope.name }); - return CONTINUE(); + return CONTINUE(node.label); } case 'ns': { diff --git a/src/node.ts b/src/node.ts index 19e94e97..14ff6393 100644 --- a/src/node.ts +++ b/src/node.ts @@ -34,21 +34,24 @@ export type Meta = NodeBase & { export type Statement = Definition | - Return | - Each | - For | - Loop | - Break | - Continue | Assign | AddAssign | SubAssign; -const statementTypes = [ - 'def', 'return', 'each', 'for', 'loop', 'break', 'continue', 'assign', 'addAssign', 'subAssign', -]; export function isStatement(x: Node): x is Statement { - return statementTypes.includes(x.type); + switch (x.type) { + case 'def': + case 'assign': + case 'addAssign': + case 'subAssign': { + x satisfies Statement; + return true; + } + default: { + x satisfies Exclude; + return false; + } + } } export type Definition = NodeBase & { @@ -66,40 +69,6 @@ export type Attribute = NodeBase & { value: Expression; // 値 }; -export type Return = NodeBase & { - type: 'return'; // return文 - expr: Expression; // 式 -}; - -export type Each = NodeBase & { - type: 'each'; // each文 - var: Expression; // イテレータ宣言 - items: Expression; // 配列 - for: Statement | Expression; // 本体処理 -}; - -export type For = NodeBase & { - type: 'for'; // for文 - var?: string; // イテレータ変数名 - from?: Expression; // 開始値 - to?: Expression; // 終値 - times?: Expression; // 回数 - for: Statement | Expression; // 本体処理 -}; - -export type Loop = NodeBase & { - type: 'loop'; // loop文 - statements: (Statement | Expression)[]; // 処理 -}; - -export type Break = NodeBase & { - type: 'break'; // break文 -}; - -export type Continue = NodeBase & { - type: 'continue'; // continue文 -}; - export type AddAssign = NodeBase & { type: 'addAssign'; // 加算代入文 dest: Expression; // 代入先 @@ -121,10 +90,11 @@ export type Assign = NodeBase & { // expressions export type Expression = - If | + ControlFlow | + Return | + Break | + Continue | Fn | - Match | - Block | Exists | Tmpl | Str | @@ -155,15 +125,134 @@ export type Expression = Index | Prop; -const expressionTypes = [ - 'if', 'fn', 'match', 'block', 'exists', 'tmpl', 'str', 'num', 'bool', 'null', 'obj', 'arr', - 'not', 'pow', 'mul', 'div', 'rem', 'add', 'sub', 'lt', 'lteq', 'gt', 'gteq', 'eq', 'neq', 'and', 'or', - 'identifier', 'call', 'index', 'prop', -]; export function isExpression(x: Node): x is Expression { - return expressionTypes.includes(x.type); + switch (x.type) { + case 'if': + case 'match': + case 'block': + case 'each': + case 'for': + case 'loop': + case 'return': + case 'break': + case 'continue': + case 'fn': + case 'exists': + case 'tmpl': + case 'str': + case 'num': + case 'bool': + case 'null': + case 'obj': + case 'arr': + case 'plus': + case 'minus': + case 'not': + case 'pow': + case 'mul': + case 'div': + case 'rem': + case 'add': + case 'sub': + case 'lt': + case 'lteq': + case 'gt': + case 'gteq': + case 'eq': + case 'neq': + case 'and': + case 'or': + case 'identifier': + case 'call': + case 'index': + case 'prop': { + x satisfies Expression; + return true; + } + default: { + x satisfies Exclude; + return false; + } + } } +export type ControlFlow = + If | + Match | + Block | + Each | + For | + Loop; + +export type If = NodeBase & { + type: 'if'; // if式 + label?: string; // ラベル + cond: Expression; // 条件式 + then: Block; // then節 + elseif: { + cond: Expression; // elifの条件式 + then: Block;// elif節 + }[]; + else?: Block; // else節 +}; + +export type Match = NodeBase & { + type: 'match'; // パターンマッチ + label?: string; // ラベル + about: Expression; // 対象 + qs: { + q: Expression; // 条件 + a: Expression; // 結果 + }[]; + default?: Expression; // デフォルト値 +}; + +export type Block = NodeBase & { + type: 'block'; // ブロックまたはeval式 + label?: string; // ラベル + statements: (Statement | Expression)[]; // 処理 +}; + +export type Each = NodeBase & { + type: 'each'; // each文 + label?: string; // ラベル + var: Expression; // イテレータ宣言 + items: Expression; // 配列 + for: Block; // 本体処理 +}; + +export type For = NodeBase & { + type: 'for'; // for文 + label?: string; // ラベル + var?: string; // イテレータ変数名 + from?: Expression; // 開始値 + to?: Expression; // 終値 + times?: Expression; // 回数 + for: Block; // 本体処理 +}; + +export type Loop = NodeBase & { + type: 'loop'; // loop文 + label?: string; // ラベル + statements: (Statement | Expression)[]; // 処理 +}; + +export type Break = NodeBase & { + type: 'break'; // break文 + label?: string; // ラベル + expr?: Expression; // 式 +}; + +export type Continue = NodeBase & { + type: 'continue'; // continue文 + label?: string; // ラベル +}; + +export type Return = NodeBase & { + type: 'return'; // return文 + expr?: Expression; // 式 +}; + export type Plus = NodeBase & { type: 'plus'; // 正号 expr: Expression; // 式 @@ -263,17 +352,6 @@ export type Or = NodeBase & { right: Expression; } -export type If = NodeBase & { - type: 'if'; // if式 - cond: Expression; // 条件式 - then: Statement | Expression; // then節 - elseif: { - cond: Expression; // elifの条件式 - then: Statement | Expression;// elif節 - }[]; - else?: Statement | Expression; // else節 -}; - export type Fn = NodeBase & { type: 'fn'; // 関数 params: { @@ -286,21 +364,6 @@ export type Fn = NodeBase & { children: (Statement | Expression)[]; // 本体処理 }; -export type Match = NodeBase & { - type: 'match'; // パターンマッチ - about: Expression; // 対象 - qs: { - q: Expression; // 条件 - a: Statement | Expression; // 結果 - }[]; - default?: Statement | Expression; // デフォルト値 -}; - -export type Block = NodeBase & { - type: 'block'; // ブロックまたはeval式 - statements: (Statement | Expression)[]; // 処理 -}; - export type Exists = NodeBase & { type: 'exists'; // 変数の存在判定 identifier: Identifier; // 変数名 diff --git a/src/parser/index.ts b/src/parser/index.ts index 74f535fd..6c11dee6 100644 --- a/src/parser/index.ts +++ b/src/parser/index.ts @@ -1,7 +1,7 @@ import { Scanner } from './scanner.js'; import { parseTopLevel } from './syntaxes/toplevel.js'; -import { validateJumpStatements } from './plugins/validate-jump-statements.js'; +import { validateJumpExpressions } from './plugins/validate-jump-statements.js'; import { validateKeyword } from './plugins/validate-keyword.js'; import { validateType } from './plugins/validate-type.js'; @@ -22,7 +22,7 @@ export class Parser { validate: [ validateKeyword, validateType, - validateJumpStatements, + validateJumpExpressions, ], transform: [ ], diff --git a/src/parser/plugins/validate-jump-statements.ts b/src/parser/plugins/validate-jump-statements.ts index d1a346e2..a676561a 100644 --- a/src/parser/plugins/validate-jump-statements.ts +++ b/src/parser/plugins/validate-jump-statements.ts @@ -3,18 +3,31 @@ import { AiScriptSyntaxError } from '../../error.js'; import type * as Ast from '../../node.js'; -function isInLoopScope(ancestors: Ast.Node[]): boolean { +function getCorrespondingBlock(ancestors: Ast.Node[], label?: string): Ast.ControlFlow | undefined { for (let i = ancestors.length - 1; i >= 0; i--) { - switch (ancestors[i]!.type) { + const ancestor = ancestors[i]!; + switch (ancestor.type) { case 'loop': case 'for': - case 'each': - return true; + case 'each': { + if (label != null && label !== ancestor.label) { + continue; + } + return ancestor; + } + case 'if': + case 'match': + case 'block': { + if (label == null || label !== ancestor.label) { + continue; + } + return ancestor; + } case 'fn': - return false; + return; } } - return false; + return; } function validateNode(node: Ast.Node, ancestors: Ast.Node[]): Ast.Node { @@ -26,14 +39,30 @@ function validateNode(node: Ast.Node, ancestors: Ast.Node[]): Ast.Node { break; } case 'break': { - if (!isInLoopScope(ancestors)) { - throw new AiScriptSyntaxError('break must be inside for / each / while / do-while / loop', node.loc.start); + if (getCorrespondingBlock(ancestors, node.label) == null) { + if (node.label != null) { + throw new AiScriptSyntaxError(`label "${node.label}" is not defined`, node.loc.start); + } + throw new AiScriptSyntaxError('unlabeled break must be inside for / each / while / do-while / loop', node.loc.start); } break; } case 'continue': { - if (!isInLoopScope(ancestors)) { + const block = getCorrespondingBlock(ancestors, node.label); + if (block == null) { + if (node.label != null) { + throw new AiScriptSyntaxError(`label "${node.label}" is not defined`, node.loc.start); + } throw new AiScriptSyntaxError('continue must be inside for / each / while / do-while / loop', node.loc.start); + } else { + switch (block.type) { + case 'if': + throw new AiScriptSyntaxError('cannot use continue for if', node.loc.start); + case 'match': + throw new AiScriptSyntaxError('cannot use continue for match', node.loc.start); + case 'block': + throw new AiScriptSyntaxError('cannot use continue for eval', node.loc.start); + } } break; } @@ -41,7 +70,7 @@ function validateNode(node: Ast.Node, ancestors: Ast.Node[]): Ast.Node { return node; } -export function validateJumpStatements(nodes: Ast.Node[]): Ast.Node[] { +export function validateJumpExpressions(nodes: Ast.Node[]): Ast.Node[] { for (const node of nodes) { visitNode(node, validateNode); } diff --git a/src/parser/plugins/validate-keyword.ts b/src/parser/plugins/validate-keyword.ts index 024a1dea..7e06ce61 100644 --- a/src/parser/plugins/validate-keyword.ts +++ b/src/parser/plugins/validate-keyword.ts @@ -101,15 +101,39 @@ function validateNode(node: Ast.Node): Ast.Node { break; } case 'each': { + if (node.label != null && reservedWord.includes(node.label)) { + throwReservedWordError(node.label, node.loc); + } validateDest(node.var); break; } case 'for': { + if (node.label != null && reservedWord.includes(node.label)) { + throwReservedWordError(node.label, node.loc); + } if (node.var != null && reservedWord.includes(node.var)) { throwReservedWordError(node.var, node.loc); } break; } + case 'loop': { + if (node.label != null && reservedWord.includes(node.label)) { + throwReservedWordError(node.label, node.loc); + } + break; + } + case 'break': { + if (node.label != null && reservedWord.includes(node.label)) { + throwReservedWordError(node.label, node.loc); + } + break; + } + case 'continue': { + if (node.label != null && reservedWord.includes(node.label)) { + throwReservedWordError(node.label, node.loc); + } + break; + } case 'fn': { for (const param of node.params) { validateDest(param.dest); diff --git a/src/parser/scanner.ts b/src/parser/scanner.ts index 401f2bd3..f2b611f7 100644 --- a/src/parser/scanner.ts +++ b/src/parser/scanner.ts @@ -153,7 +153,7 @@ export class Scanner implements ITokenStream { this.stream.next(); return TOKEN(TokenKind.OpenSharpBracket, pos, { hasLeftSpacing }); } else { - throw new AiScriptSyntaxError('invalid character: "#"', pos); + return TOKEN(TokenKind.Sharp, pos, { hasLeftSpacing }); } } case '%': { diff --git a/src/parser/syntaxes/control-flow.ts b/src/parser/syntaxes/control-flow.ts new file mode 100644 index 00000000..5744c5ac --- /dev/null +++ b/src/parser/syntaxes/control-flow.ts @@ -0,0 +1,415 @@ +/** + * 制御構造式 + */ + +import { AiScriptSyntaxError } from '../../error.js'; +import { TokenKind } from '../token.js'; +import { unexpectedTokenError, NODE } from '../utils.js'; +import { parseBlock, parseDest, parseOptionalSeparator } from './common.js'; +import { parseExpr } from './expressions.js'; + +import type * as Ast from '../../node.js'; +import type { ITokenStream } from '../streams/token-stream.js'; + +/** + * ```abnf + * ControlFlowExpr = ["#" IDENT ":"] ControlFlowExprWithoutLabel + * ``` +*/ +export function parseControlFlowExpr(s: ITokenStream): Ast.ControlFlow { + let label: string | undefined; + if (s.is(TokenKind.Sharp)) { + s.next(); + + s.expect(TokenKind.Identifier); + label = s.getTokenValue(); + s.next(); + + s.expect(TokenKind.Colon); + s.next(); + } + + const statement = parseControlFlowExprWithoutLabel(s); + statement.label = label; + return statement; +} + +/** + * ```abnf + * ControlFlowExprWithoutLabel = If / Match / Eval / Each / For / Loop + * ``` +*/ +function parseControlFlowExprWithoutLabel(s: ITokenStream): Ast.ControlFlow { + const tokenKind = s.getTokenKind(); + switch (tokenKind) { + case TokenKind.IfKeyword: { + return parseIf(s); + } + case TokenKind.MatchKeyword: { + return parseMatch(s); + } + case TokenKind.EvalKeyword: { + return parseEval(s); + } + case TokenKind.EachKeyword: { + return parseEach(s); + } + case TokenKind.ForKeyword: { + return parseFor(s); + } + case TokenKind.LoopKeyword: { + return parseLoop(s); + } + case TokenKind.DoKeyword: { + return parseDoWhile(s); + } + case TokenKind.WhileKeyword: { + return parseWhile(s); + } + } + throw unexpectedTokenError(tokenKind, s.getPos()); +} + +/** + * ```abnf + * If = "if" Expr Block *("elif" Expr Block) ["else" Block] + * ``` +*/ +function parseIf(s: ITokenStream): Ast.If { + const startPos = s.getPos(); + + s.expect(TokenKind.IfKeyword); + s.next(); + const cond = parseExpr(s, false); + const then = parseBlockAsExpr(s); + + if (s.is(TokenKind.NewLine) && [TokenKind.ElifKeyword, TokenKind.ElseKeyword].includes(s.lookahead(1).kind)) { + s.next(); + } + + const elseif: Ast.If['elseif'] = []; + while (s.is(TokenKind.ElifKeyword)) { + s.next(); + const elifCond = parseExpr(s, false); + const elifThen = parseBlockAsExpr(s); + if (s.is(TokenKind.NewLine) && [TokenKind.ElifKeyword, TokenKind.ElseKeyword].includes(s.lookahead(1).kind)) { + s.next(); + } + elseif.push({ cond: elifCond, then: elifThen }); + } + + let _else = undefined; + if (s.is(TokenKind.ElseKeyword)) { + s.next(); + _else = parseBlockAsExpr(s); + } + + return NODE('if', { cond, then, elseif, else: _else }, startPos, s.getPos()); +} + +/** + * ```abnf + * Match = "match" Expr "{" [(MatchCase *(SEP MatchCase) [SEP DefaultCase] [SEP]) / DefaultCase [SEP]] "}" + * ``` +*/ +function parseMatch(s: ITokenStream): Ast.Match { + const startPos = s.getPos(); + + s.expect(TokenKind.MatchKeyword); + s.next(); + const about = parseExpr(s, false); + + s.expect(TokenKind.OpenBrace); + s.next(); + + if (s.is(TokenKind.NewLine)) { + s.next(); + } + + const qs: Ast.Match['qs'] = []; + let x: Ast.Match['default']; + if (s.is(TokenKind.CaseKeyword)) { + qs.push(parseMatchCase(s)); + let sep = parseOptionalSeparator(s); + while (s.is(TokenKind.CaseKeyword)) { + if (!sep) { + throw new AiScriptSyntaxError('separator expected', s.getPos()); + } + qs.push(parseMatchCase(s)); + sep = parseOptionalSeparator(s); + } + if (s.is(TokenKind.DefaultKeyword)) { + if (!sep) { + throw new AiScriptSyntaxError('separator expected', s.getPos()); + } + x = parseDefaultCase(s); + parseOptionalSeparator(s); + } + } else if (s.is(TokenKind.DefaultKeyword)) { + x = parseDefaultCase(s); + parseOptionalSeparator(s); + } + + s.expect(TokenKind.CloseBrace); + s.next(); + + return NODE('match', { about, qs, default: x }, startPos, s.getPos()); +} + +/** + * ```abnf + * MatchCase = "case" Expr "=>" Block + * ``` +*/ +function parseMatchCase(s: ITokenStream): Ast.Match['qs'][number] { + s.expect(TokenKind.CaseKeyword); + s.next(); + const q = parseExpr(s, false); + s.expect(TokenKind.Arrow); + s.next(); + const a = parseBlockOrExpr(s); + return { q, a }; +} + +/** + * ```abnf + * DefaultCase = "default" "=>" Block + * ``` +*/ +function parseDefaultCase(s: ITokenStream): Ast.Match['default'] { + s.expect(TokenKind.DefaultKeyword); + s.next(); + s.expect(TokenKind.Arrow); + s.next(); + return parseBlockOrExpr(s); +} + +/** + * ```abnf + * Eval = "eval" Block + * ``` +*/ +function parseEval(s: ITokenStream): Ast.Block { + const startPos = s.getPos(); + + s.expect(TokenKind.EvalKeyword); + s.next(); + const statements = parseBlock(s); + + return NODE('block', { statements }, startPos, s.getPos()); +} + +/** + * ```abnf + * Each = "each" "(" "let" Dest "," Expr ")" Block + * / "each" "let" Dest "," Expr Block + * ``` +*/ +function parseEach(s: ITokenStream): Ast.Each { + const startPos = s.getPos(); + let hasParen = false; + + s.expect(TokenKind.EachKeyword); + s.next(); + + if (s.is(TokenKind.OpenParen)) { + hasParen = true; + s.next(); + } + + s.expect(TokenKind.LetKeyword); + s.next(); + + const dest = parseDest(s); + + if (s.is(TokenKind.Comma)) { + s.next(); + } else { + throw new AiScriptSyntaxError('separator expected', s.getPos()); + } + + const items = parseExpr(s, false); + + if (hasParen) { + s.expect(TokenKind.CloseParen); + s.next(); + } + + const body = parseBlockAsExpr(s); + + return NODE('each', { + var: dest, + items: items, + for: body, + }, startPos, s.getPos()); +} + +/** + * ```abnf + * For = ForRange / ForTimes + * ForRange = "for" "(" "let" IDENT ["=" Expr] "," Expr ")" Block + * / "for" "let" IDENT ["=" Expr] "," Expr Block + * ForTimes = "for" "(" Expr ")" Block + * / "for" Expr Block + * ``` +*/ +function parseFor(s: ITokenStream): Ast.For { + const startPos = s.getPos(); + let hasParen = false; + + s.expect(TokenKind.ForKeyword); + s.next(); + + if (s.is(TokenKind.OpenParen)) { + hasParen = true; + s.next(); + } + + if (s.is(TokenKind.LetKeyword)) { + // range syntax + s.next(); + + const identPos = s.getPos(); + + s.expect(TokenKind.Identifier); + const name = s.getTokenValue(); + s.next(); + + let _from: Ast.Expression; + if (s.is(TokenKind.Eq)) { + s.next(); + _from = parseExpr(s, false); + } else { + _from = NODE('num', { value: 0 }, identPos, identPos); + } + + if (s.is(TokenKind.Comma)) { + s.next(); + } else { + throw new AiScriptSyntaxError('separator expected', s.getPos()); + } + + const to = parseExpr(s, false); + + if (hasParen) { + s.expect(TokenKind.CloseParen); + s.next(); + } + + const body = parseBlockAsExpr(s); + + return NODE('for', { + var: name, + from: _from, + to, + for: body, + }, startPos, s.getPos()); + } else { + // times syntax + + const times = parseExpr(s, false); + + if (hasParen) { + s.expect(TokenKind.CloseParen); + s.next(); + } + + const body = parseBlockAsExpr(s); + + return NODE('for', { + times, + for: body, + }, startPos, s.getPos()); + } +} + +/** + * ```abnf + * Loop = "loop" Block + * ``` +*/ +function parseLoop(s: ITokenStream): Ast.Loop { + const startPos = s.getPos(); + + s.expect(TokenKind.LoopKeyword); + s.next(); + const statements = parseBlock(s); + + return NODE('loop', { statements }, startPos, s.getPos()); +} + +/** + * ```abnf + * Loop = "do" Block "while" Expr + * ``` +*/ +function parseDoWhile(s: ITokenStream): Ast.Loop { + const doStartPos = s.getPos(); + s.expect(TokenKind.DoKeyword); + s.next(); + const body = parseBlockAsExpr(s); + const whilePos = s.getPos(); + s.expect(TokenKind.WhileKeyword); + s.next(); + const cond = parseExpr(s, false); + const endPos = s.getPos(); + + return NODE('loop', { + statements: [ + body, + NODE('if', { + cond: NODE('not', { expr: cond }, whilePos, endPos), + then: NODE('block', { + statements: [NODE('break', {}, endPos, endPos)], + }, endPos, endPos), + elseif: [], + }, whilePos, endPos), + ], + }, doStartPos, endPos); +} + +/** + * ```abnf + * Loop = "while" Expr Block + * ``` +*/ +function parseWhile(s: ITokenStream): Ast.Loop { + const startPos = s.getPos(); + s.expect(TokenKind.WhileKeyword); + s.next(); + const cond = parseExpr(s, false); + const condEndPos = s.getPos(); + const body = parseBlockAsExpr(s); + + return NODE('loop', { + statements: [ + NODE('if', { + cond: NODE('not', { expr: cond }, startPos, condEndPos), + then: NODE('block', { + statements: [NODE('break', {}, condEndPos, condEndPos)], + }, condEndPos, condEndPos), + elseif: [], + }, startPos, condEndPos), + body, + ], + }, startPos, s.getPos()); +} + +/** + * ```abnf + * BlockOrExpr = Block / Expr + * ``` +*/ +function parseBlockOrExpr(s: ITokenStream): Ast.Expression { + if (s.is(TokenKind.OpenBrace)) { + return parseBlockAsExpr(s); + } else { + return parseExpr(s, false); + } +} + +function parseBlockAsExpr(s: ITokenStream): Ast.Block { + const startPos = s.getPos(); + const statements = parseBlock(s); + return NODE('block', { statements }, startPos, s.getPos()); +} diff --git a/src/parser/syntaxes/expressions.ts b/src/parser/syntaxes/expressions.ts index 8ef6b356..9752d442 100644 --- a/src/parser/syntaxes/expressions.ts +++ b/src/parser/syntaxes/expressions.ts @@ -2,8 +2,8 @@ import { AiScriptSyntaxError, AiScriptUnexpectedEOFError } from '../../error.js' import { NODE, unexpectedTokenError } from '../utils.js'; import { TokenStream } from '../streams/token-stream.js'; import { TokenKind } from '../token.js'; -import { parseBlock, parseOptionalSeparator, parseParams, parseType } from './common.js'; -import { parseBlockOrStatement } from './statements.js'; +import { parseBlock, parseParams, parseType } from './common.js'; +import { parseControlFlowExpr } from './control-flow.js'; import type * as Ast from '../../node.js'; import type { ITokenStream } from '../streams/token-stream.js'; @@ -193,22 +193,10 @@ function parseAtom(s: ITokenStream, isStatic: boolean): Ast.Expression { const startPos = s.getPos(); switch (s.getTokenKind()) { - 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); @@ -287,8 +275,23 @@ function parseAtom(s: ITokenStream, isStatic: boolean): Ast.Expression { s.next(); return expr; } + case TokenKind.ReturnKeyword: { + if (isStatic) break; + return parseReturn(s); + } + case TokenKind.BreakKeyword: { + if (isStatic) break; + return parseBreak(s); + } + case TokenKind.ContinueKeyword: { + if (isStatic) break; + return parseContinue(s); + } + } + if (isStatic) { + throw unexpectedTokenError(s.getTokenKind(), s.getPos()); } - throw unexpectedTokenError(s.getTokenKind(), startPos); + return parseControlFlowExpr(s); } /** @@ -342,43 +345,6 @@ function parseCall(s: ITokenStream, target: Ast.Expression): Ast.Call { }, startPos, s.getPos()); } -/** - * ```abnf - * If = "if" Expr BlockOrStatement *("elif" Expr BlockOrStatement) ["else" BlockOrStatement] - * ``` -*/ -function parseIf(s: ITokenStream): Ast.If { - const startPos = s.getPos(); - - s.expect(TokenKind.IfKeyword); - s.next(); - const cond = parseExpr(s, false); - const then = parseBlockOrStatement(s); - - if (s.is(TokenKind.NewLine) && [TokenKind.ElifKeyword, TokenKind.ElseKeyword].includes(s.lookahead(1).kind)) { - s.next(); - } - - const elseif: Ast.If['elseif'] = []; - while (s.is(TokenKind.ElifKeyword)) { - s.next(); - const elifCond = parseExpr(s, false); - const elifThen = parseBlockOrStatement(s); - if (s.is(TokenKind.NewLine) && [TokenKind.ElifKeyword, TokenKind.ElseKeyword].includes(s.lookahead(1).kind)) { - s.next(); - } - elseif.push({ cond: elifCond, then: elifThen }); - } - - let _else = undefined; - if (s.is(TokenKind.ElseKeyword)) { - s.next(); - _else = parseBlockOrStatement(s); - } - - return NODE('if', { cond, then, elseif, else: _else }, startPos, s.getPos()); -} - /** * ```abnf * FnExpr = "@" Params [":" Type] Block @@ -403,98 +369,6 @@ function parseFnExpr(s: ITokenStream): Ast.Fn { return NODE('fn', { params: params, retType: type, children: body }, startPos, s.getPos()); } -/** - * ```abnf - * Match = "match" Expr "{" [(MatchCase *(SEP MatchCase) [SEP DefaultCase] [SEP]) / DefaultCase [SEP]] "}" - * ``` -*/ -function parseMatch(s: ITokenStream): Ast.Match { - const startPos = s.getPos(); - - s.expect(TokenKind.MatchKeyword); - s.next(); - const about = parseExpr(s, false); - - s.expect(TokenKind.OpenBrace); - s.next(); - - if (s.is(TokenKind.NewLine)) { - s.next(); - } - - const qs: Ast.Match['qs'] = []; - let x: Ast.Match['default']; - if (s.is(TokenKind.CaseKeyword)) { - qs.push(parseMatchCase(s)); - let sep = parseOptionalSeparator(s); - while (s.is(TokenKind.CaseKeyword)) { - if (!sep) { - throw new AiScriptSyntaxError('separator expected', s.getPos()); - } - qs.push(parseMatchCase(s)); - sep = parseOptionalSeparator(s); - } - if (s.is(TokenKind.DefaultKeyword)) { - if (!sep) { - throw new AiScriptSyntaxError('separator expected', s.getPos()); - } - x = parseDefaultCase(s); - parseOptionalSeparator(s); - } - } else if (s.is(TokenKind.DefaultKeyword)) { - x = parseDefaultCase(s); - parseOptionalSeparator(s); - } - - s.expect(TokenKind.CloseBrace); - s.next(); - - return NODE('match', { about, qs, default: x }, startPos, s.getPos()); -} - -/** - * ```abnf - * MatchCase = "case" Expr "=>" BlockOrStatement - * ``` -*/ -function parseMatchCase(s: ITokenStream): Ast.Match['qs'][number] { - s.expect(TokenKind.CaseKeyword); - s.next(); - const q = parseExpr(s, false); - s.expect(TokenKind.Arrow); - s.next(); - const a = parseBlockOrStatement(s); - return { q, a }; -} - -/** - * ```abnf - * DefaultCase = "default" "=>" BlockOrStatement - * ``` -*/ -function parseDefaultCase(s: ITokenStream): Ast.Match['default'] { - s.expect(TokenKind.DefaultKeyword); - s.next(); - s.expect(TokenKind.Arrow); - s.next(); - return parseBlockOrStatement(s); -} - -/** - * ```abnf - * Eval = "eval" Block - * ``` -*/ -function parseEval(s: ITokenStream): Ast.Block { - const startPos = s.getPos(); - - s.expect(TokenKind.EvalKeyword); - s.next(); - const statements = parseBlock(s); - - return NODE('block', { statements }, startPos, s.getPos()); -} - /** * ```abnf * Exists = "exists" Reference @@ -643,6 +517,84 @@ function parseArray(s: ITokenStream, isStatic: boolean): Ast.Arr { return NODE('arr', { value }, startPos, s.getPos()); } +/** + * ```abnf + * Return = "return" [Expr] + * ``` +*/ +function parseReturn(s: ITokenStream): Ast.Return { + const startPos = s.getPos(); + + s.expect(TokenKind.ReturnKeyword); + s.next(); + const expr = tryParseExpr(s); + + return NODE('return', { expr }, startPos, s.getPos()); +} + +/** + * ```abnf + * Break = "break" ["#" IDENT] [Expr] + * ``` +*/ +function parseBreak(s: ITokenStream): Ast.Break { + const startPos = s.getPos(); + + s.expect(TokenKind.BreakKeyword); + s.next(); + + let label: string | undefined; + if (s.is(TokenKind.Sharp)) { + s.next(); + + s.expect(TokenKind.Identifier); + label = s.getTokenValue(); + s.next(); + } + + const expr = tryParseExpr(s); + + return NODE('break', { label, expr }, startPos, s.getPos()); +} + +/** + * ```abnf + * Continue = "continue" ["#" IDENT] + * ``` +*/ +function parseContinue(s: ITokenStream): Ast.Continue { + const startPos = s.getPos(); + + s.expect(TokenKind.ContinueKeyword); + s.next(); + + let label: string | undefined; + if (s.is(TokenKind.Sharp)) { + s.next(); + + s.expect(TokenKind.Identifier); + label = s.getTokenValue(); + s.next(); + } + + return NODE('continue', { label }, startPos, s.getPos()); +} + +function tryParseExpr(s: ITokenStream): Ast.Expression | undefined { + switch (s.getTokenKind()) { + case TokenKind.NewLine: + case TokenKind.SemiColon: + case TokenKind.Comma: + case TokenKind.CloseParen: + case TokenKind.CloseBracket: + case TokenKind.CloseBrace: + return; + default: { + return parseExpr(s, false); + } + } +} + //#region Pratt parsing type PrefixInfo = { opKind: 'prefix', kind: TokenKind, bp: number }; diff --git a/src/parser/syntaxes/statements.ts b/src/parser/syntaxes/statements.ts index 5097313f..4a25de95 100644 --- a/src/parser/syntaxes/statements.ts +++ b/src/parser/syntaxes/statements.ts @@ -8,8 +8,6 @@ import type * as Ast from '../../node.js'; import type { ITokenStream } from '../streams/token-stream.js'; export function parseStatement(s: ITokenStream): Ast.Statement | Ast.Expression { - const startPos = s.getPos(); - switch (s.getTokenKind()) { case TokenKind.VarKeyword: case TokenKind.LetKeyword: { @@ -25,35 +23,9 @@ export function parseStatement(s: ITokenStream): Ast.Statement | Ast.Expression case TokenKind.Out: { return parseOut(s); } - case TokenKind.ReturnKeyword: { - return parseReturn(s); - } case TokenKind.OpenSharpBracket: { return parseStatementWithAttr(s); } - case TokenKind.EachKeyword: { - return parseEach(s); - } - case TokenKind.ForKeyword: { - return parseFor(s); - } - case TokenKind.LoopKeyword: { - return parseLoop(s); - } - case TokenKind.DoKeyword: { - return parseDoWhile(s); - } - case TokenKind.WhileKeyword: { - return parseWhile(s); - } - case TokenKind.BreakKeyword: { - s.next(); - return NODE('break', {}, startPos, s.getPos()); - } - case TokenKind.ContinueKeyword: { - s.next(); - return NODE('continue', {}, startPos, s.getPos()); - } } const expr = parseExpr(s, false); const assign = tryParseAssign(s, expr); @@ -83,21 +55,6 @@ export function parseDefStatement(s: ITokenStream): Ast.Definition { } } -/** - * ```abnf - * BlockOrStatement = Block / Statement - * ``` -*/ -export function parseBlockOrStatement(s: ITokenStream): Ast.Statement | Ast.Expression { - if (s.is(TokenKind.OpenBrace)) { - const startPos = s.getPos(); - const statements = parseBlock(s); - return NODE('block', { statements }, startPos, s.getPos()); - } else { - return parseStatement(s); - } -} - /** * ```abnf * VarDef = ("let" / "var") Dest [":" Type] "=" Expr @@ -198,145 +155,6 @@ function parseOut(s: ITokenStream): Ast.Call { return CALL_NODE('print', [expr], startPos, s.getPos()); } -/** - * ```abnf - * Each = "each" "(" "let" Dest "," Expr ")" BlockOrStatement - * / "each" "let" Dest "," Expr BlockOrStatement - * ``` -*/ -function parseEach(s: ITokenStream): Ast.Each { - const startPos = s.getPos(); - let hasParen = false; - - s.expect(TokenKind.EachKeyword); - s.next(); - - if (s.is(TokenKind.OpenParen)) { - hasParen = true; - s.next(); - } - - s.expect(TokenKind.LetKeyword); - s.next(); - - const dest = parseDest(s); - - if (s.is(TokenKind.Comma)) { - s.next(); - } else { - throw new AiScriptSyntaxError('separator expected', s.getPos()); - } - - const items = parseExpr(s, false); - - if (hasParen) { - s.expect(TokenKind.CloseParen); - s.next(); - } - - const body = parseBlockOrStatement(s); - - return NODE('each', { - var: dest, - items: items, - for: body, - }, startPos, s.getPos()); -} - -/** - * ```abnf - * For = ForRange / ForTimes - * ForRange = "for" "(" "let" IDENT ["=" Expr] "," Expr ")" BlockOrStatement - * / "for" "let" IDENT ["=" Expr] "," Expr BlockOrStatement - * ForTimes = "for" "(" Expr ")" BlockOrStatement - * / "for" Expr BlockOrStatement - * ``` -*/ -function parseFor(s: ITokenStream): Ast.For { - const startPos = s.getPos(); - let hasParen = false; - - s.expect(TokenKind.ForKeyword); - s.next(); - - if (s.is(TokenKind.OpenParen)) { - hasParen = true; - s.next(); - } - - if (s.is(TokenKind.LetKeyword)) { - // range syntax - s.next(); - - const identPos = s.getPos(); - - s.expect(TokenKind.Identifier); - const name = s.getTokenValue(); - s.next(); - - let _from: Ast.Expression; - if (s.is(TokenKind.Eq)) { - s.next(); - _from = parseExpr(s, false); - } else { - _from = NODE('num', { value: 0 }, identPos, identPos); - } - - if (s.is(TokenKind.Comma)) { - s.next(); - } else { - throw new AiScriptSyntaxError('separator expected', s.getPos()); - } - - const to = parseExpr(s, false); - - if (hasParen) { - s.expect(TokenKind.CloseParen); - s.next(); - } - - const body = parseBlockOrStatement(s); - - return NODE('for', { - var: name, - from: _from, - to, - for: body, - }, startPos, s.getPos()); - } else { - // times syntax - - const times = parseExpr(s, false); - - if (hasParen) { - s.expect(TokenKind.CloseParen); - s.next(); - } - - const body = parseBlockOrStatement(s); - - return NODE('for', { - times, - for: body, - }, startPos, s.getPos()); - } -} - -/** - * ```abnf - * Return = "return" Expr - * ``` -*/ -function parseReturn(s: ITokenStream): Ast.Return { - const startPos = s.getPos(); - - s.expect(TokenKind.ReturnKeyword); - s.next(); - const expr = parseExpr(s, false); - - return NODE('return', { expr }, startPos, s.getPos()); -} - /** * ```abnf * StatementWithAttr = *Attr Statement @@ -393,74 +211,6 @@ function parseAttr(s: ITokenStream): Ast.Attribute { return NODE('attr', { name, value }, startPos, s.getPos()); } -/** - * ```abnf - * Loop = "loop" Block - * ``` -*/ -function parseLoop(s: ITokenStream): Ast.Loop { - const startPos = s.getPos(); - - s.expect(TokenKind.LoopKeyword); - s.next(); - const statements = parseBlock(s); - - return NODE('loop', { statements }, startPos, s.getPos()); -} - -/** - * ```abnf - * Loop = "do" BlockOrStatement "while" Expr - * ``` -*/ -function parseDoWhile(s: ITokenStream): Ast.Loop { - const doStartPos = s.getPos(); - s.expect(TokenKind.DoKeyword); - s.next(); - const body = parseBlockOrStatement(s); - const whilePos = s.getPos(); - s.expect(TokenKind.WhileKeyword); - s.next(); - const cond = parseExpr(s, false); - const endPos = s.getPos(); - - return NODE('loop', { - statements: [ - body, - NODE('if', { - cond: NODE('not', { expr: cond }, whilePos, endPos), - then: NODE('break', {}, endPos, endPos), - elseif: [], - }, whilePos, endPos), - ], - }, doStartPos, endPos); -} - -/** - * ```abnf - * Loop = "while" Expr BlockOrStatement - * ``` -*/ -function parseWhile(s: ITokenStream): Ast.Loop { - const startPos = s.getPos(); - s.expect(TokenKind.WhileKeyword); - s.next(); - const cond = parseExpr(s, false); - const condEndPos = s.getPos(); - const body = parseBlockOrStatement(s); - - return NODE('loop', { - statements: [ - NODE('if', { - cond: NODE('not', { expr: cond }, startPos, condEndPos), - then: NODE('break', {}, condEndPos, condEndPos), - elseif: [], - }, startPos, condEndPos), - body, - ], - }, startPos, s.getPos()); -} - /** * ```abnf * Assign = Expr ("=" / "+=" / "-=") Expr diff --git a/src/parser/token.ts b/src/parser/token.ts index d4bdaf49..7e633b33 100644 --- a/src/parser/token.ts +++ b/src/parser/token.ts @@ -39,6 +39,8 @@ export enum TokenKind { Not, /** "!=" */ NotEq, + /** "#" */ + Sharp, /** "#[" */ OpenSharpBracket, /** "###" */ diff --git a/src/parser/visit.ts b/src/parser/visit.ts index 36fcf3a0..4451b09b 100644 --- a/src/parser/visit.ts +++ b/src/parser/visit.ts @@ -15,7 +15,9 @@ function visitNodeInner(node: Ast.Node, fn: (node: Ast.Node, ancestors: Ast.Node break; } case 'return': { - result.expr = visitNodeInner(result.expr, fn, ancestors) as Ast.Return['expr']; + if (result.expr != null) { + result.expr = visitNodeInner(result.expr, fn, ancestors) as Ast.Return['expr']; + } break; } case 'each': { diff --git a/test/index.ts b/test/index.ts index af97bf11..5925e9fd 100644 --- a/test/index.ts +++ b/test/index.ts @@ -690,18 +690,6 @@ describe('Return', () => { eq(res, STR('kawaii')); }); - test.concurrent('Early return without block', async () => { - const res = await exe(` - @f() { - if true return "ai" - - "pope" - } - <: f() - `); - eq(res, STR('ai')); - }); - test.concurrent('return inside for', async () => { const res = await exe(` @f() { @@ -808,7 +796,7 @@ describe('type declaration', () => { x.push(0) y = "abc" var r: bool = z(x[0]) - x.push(if r 5 else 10) + x.push(if r { 5 } else { 10 }) x } @@ -1100,10 +1088,10 @@ describe('extra', () => { let res = [] for (let i = 1, 15) { let msg = - if (i % 15 == 0) "FizzBuzz" - elif (i % 3 == 0) "Fizz" - elif (i % 5 == 0) "Buzz" - else i + if i % 15 == 0 { "FizzBuzz" } + elif i % 3 == 0 { "Fizz" } + elif i % 5 == 0 { "Buzz" } + else { i } res.push(msg) } <: res diff --git a/test/interpreter.ts b/test/interpreter.ts index bcf9e64d..49aeb912 100644 --- a/test/interpreter.ts +++ b/test/interpreter.ts @@ -103,9 +103,9 @@ describe('error location', () => { test.concurrent('Error in passed function', async () => { return expect(exeAndGetErrPos(`// (の位置 [1, 2, 3].map(@(v){ - if v==1 Core:abort("error") + if v==1 { Core:abort("error") } }) - `)).resolves.toEqual({ line: 3, column: 23}); + `)).resolves.toEqual({ line: 3, column: 25 }); }); test.concurrent('No such prop', async () => { diff --git a/test/jump-statements.ts b/test/jump-statements.ts index 80470d9a..0ba6b95f 100644 --- a/test/jump-statements.ts +++ b/test/jump-statements.ts @@ -17,6 +17,18 @@ describe('return', () => { assert.rejects(() => exe('return 1')); }); + test.concurrent('without expr', async () => { + const res = await exe(` + @f() { + return + 1 + } + <: f() + `); + eq(res, NULL); + assert.rejects(() => exe('return')); + }); + test.concurrent('in eval', async () => { const res = await exe(` @f() { @@ -34,12 +46,12 @@ describe('return', () => { test.concurrent('cond', async () => { const res = await exe(` @f() { - let a = if eval { return true } {} + let a = if (return true) {} } <: f() `); eq(res, BOOL(true)); - assert.rejects(() => exe('<: if eval { return true } {}')); + assert.rejects(() => exe('<: if (return true) {}')); }); test.concurrent('then', async () => { @@ -58,12 +70,12 @@ describe('return', () => { test.concurrent('elif cond', async () => { const res = await exe(` @f() { - let a = if false {} elif eval { return true } {} + let a = if false {} elif (return true) {} } <: f() `); eq(res, BOOL(true)); - assert.rejects(() => exe('<: if false {} elif eval { return true } {}')); + assert.rejects(() => exe('<: if false {} elif (return true) {}')); }); test.concurrent('elif then', async () => { @@ -77,7 +89,7 @@ describe('return', () => { <: f() `); eq(res, NUM(1)); - assert.rejects(() => exe('<: if false {} elif true eval { return true }')); + assert.rejects(() => exe('<: if false {} elif true (return true)')); }); test.concurrent('else', async () => { @@ -91,7 +103,7 @@ describe('return', () => { <: f() `); eq(res, NUM(1)); - assert.rejects(() => exe('<: if false {} else eval { return true }')); + assert.rejects(() => exe('<: if false {} else (return true)')); }); }); @@ -99,19 +111,19 @@ describe('return', () => { test.concurrent('about', async () => { const res = await exe(` @f() { - let a = match eval { return 1 } {} + let a = match (return 1) {} } <: f() `); eq(res, NUM(1)); - assert.rejects(() => exe('<: match eval { return 1 } {}')); + assert.rejects(() => exe('<: match (return 1) {}')); }); test.concurrent('case q', async () => { const res = await exe(` @f() { let a = match 0 { - case eval { return 0 } => { + case (return 0) => { return 1 } } @@ -119,7 +131,7 @@ describe('return', () => { <: f() `); eq(res, NUM(0)); - assert.rejects(() => exe('<: match 0 { case eval { return 0 } => {} }')) + assert.rejects(() => exe('<: match 0 { case (return 0) => {} }')) }); test.concurrent('case a', async () => { @@ -157,23 +169,23 @@ describe('return', () => { test.concurrent('left', async () => { const res = await exe(` @f() { - eval { return 1 } + 2 + (return 1) + 2 } <: f() `); eq(res, NUM(1)); - assert.rejects(() => exe('<: eval { return 1 } + 2')); + assert.rejects(() => exe('<: (return 1) + 2')); }); test.concurrent('right', async () => { const res = await exe(` @f() { - 1 + eval { return 2 } + 1 + (return 2) } <: f() `); eq(res, NUM(2)); - assert.rejects(() => exe('<: 1 + eval { return 2 }')); + assert.rejects(() => exe('<: 1 + (return 2)')); }); }); @@ -181,23 +193,23 @@ describe('return', () => { test.concurrent('callee', async () => { const res = await exe(` @f() { - eval { return print }('Hello, world!') + (return print)('Hello, world!') } f()('Hi') `); eq(res, STR('Hi')); - assert.rejects(() => exe(`eval { return print }('Hello, world!')`)); + assert.rejects(() => exe(`(return print)('Hello, world!')`)); }); test.concurrent('arg', async () => { const res = await exe(` @f() { - print(eval { return 'Hello, world!' }) + print(return 'Hello, world!') } <: f() `); eq(res, STR('Hello, world!')); - assert.rejects(() => exe(`print(eval { return 'Hello, world' })`)) + assert.rejects(() => exe(`print(return 'Hello, world')`)) }); }); @@ -205,34 +217,34 @@ describe('return', () => { test.concurrent('times', async () => { const res = await exe(` @f() { - for eval { return 1 } {} + for (return 1) {} } <: f() `); eq(res, NUM(1)); - assert.rejects(() => exe('for eval { return 1 } {}')); + assert.rejects(() => exe('for (return 1) {}')); }); test.concurrent('from', async () => { const res = await exe(` @f() { - for let i = eval { return 1 }, 2 {} + for let i = (return 1), 2 {} } <: f() `); eq(res, NUM(1)); - assert.rejects(() => exe('for let i = eval { return 1 }, 2 {}')); + assert.rejects(() => exe('for let i = (return 1), 2 {}')); }); test.concurrent('to', async () => { const res = await exe(` @f() { - for let i = 0, eval { return 1 } {} + for let i = 0, (return 1) {} } <: f() `); eq(res, NUM(1)); - assert.rejects(() => exe('for let i = 0, eval { return 1 } {}')); + assert.rejects(() => exe('for let i = 0, (return 1) {}')); }); test.concurrent('for', async () => { @@ -253,12 +265,12 @@ describe('return', () => { test.concurrent('items', async () => { const res = await exe(` @f() { - each let v, [eval { return 1 }] {} + each let v, [return 1] {} } <: f() `); eq(res, NUM(1)); - assert.rejects(() => exe('each let v, [eval { return 1 }] {}')); + assert.rejects(() => exe('each let v, [return 1] {}')); }); test.concurrent('for', async () => { @@ -280,72 +292,72 @@ describe('return', () => { const res = await exe(` @f() { let a = null - a = eval { return 1 } + a = (return 1) } <: f() `); eq(res, NUM(1)); - assert.rejects(() => exe('let a = null; a = eval { return 1 }')); + assert.rejects(() => exe('let a = null; a = (return 1)')); }); test.concurrent('index target', async () => { const res = await exe(` @f() { let a = [null] - eval { return a }[0] = 1 + (return a)[0] = 1 } <: f() `); eq(res, ARR([NULL])); - assert.rejects(() => exe('let a = [null]; eval { return a }[0] = 1')); + assert.rejects(() => exe('let a = [null]; (return a)[0] = 1')); }); test.concurrent('index', async () => { const res = await exe(` @f() { let a = [null] - a[eval { return 0 }] = 1 + a[return 0] = 1 } <: f() `); eq(res, NUM(0)); - assert.rejects(() => exe('let a = [null]; a[eval { return 0 }] = 1')); + assert.rejects(() => exe('let a = [null]; a[return 0] = 1')); }); test.concurrent('prop target', async () => { const res = await exe(` @f() { let o = {} - eval { return o }.p = 1 + (return o).p = 1 } <: f() `); eq(res, OBJ(new Map())); - assert.rejects(() => exe('let o = {}; eval { return o }.p = 1')); + assert.rejects(() => exe('let o = {}; (return o).p = 1')); }); test.concurrent('arr', async () => { const res = await exe(` @f() { let o = {} - [eval { return o }.p] = [1] + [(return o).p] = [1] } <: f() `); eq(res, OBJ(new Map())); - assert.rejects(() => exe('let o = {}; [eval { return o }.p] = [1]')); + assert.rejects(() => exe('let o = {}; [(return o).p] = [1]')); }); test.concurrent('obj', async () => { const res = await exe(` @f() { let o = {} - { a: eval { return o }.p } = { a: 1 } + { a: (return o).p } = { a: 1 } } <: f() `); eq(res, OBJ(new Map())); - assert.rejects(() => exe('let o = {}; { a: eval { return o }.p } = { a: 1 }')); + assert.rejects(() => exe('let o = {}; { a: (return o).p } = { a: 1 }')); }); }); @@ -354,24 +366,24 @@ describe('return', () => { const res = await exe(` @f() { let a = [0] - a[eval { return 0 }] += 1 + a[return 0] += 1 } <: f() `); eq(res, NUM(0)); - assert.rejects(() => exe('let a = [0]; a[eval { return 0 }] += 1')); + assert.rejects(() => exe('let a = [0]; a[return 0] += 1')); }); test.concurrent('expr', async () => { const res = await exe(` @f() { let a = 0 - a += eval { return 1 } + a += (return 1) } <: f() `); eq(res, NUM(1)); - assert.rejects(() => exe('let a = 0; a += eval { return 1 }')); + assert.rejects(() => exe('let a = 0; a += (return 1)')); }); }); @@ -380,114 +392,136 @@ describe('return', () => { const res = await exe(` @f() { let a = [0] - a[eval { return 0 }] -= 1 + a[return 0] -= 1 } <: f() `); eq(res, NUM(0)); - assert.rejects(() => exe('let a = [0]; a[eval { return 0 }] -= 1')); + assert.rejects(() => exe('let a = [0]; a[return 0] -= 1')); }); test.concurrent('expr', async () => { const res = await exe(` @f() { let a = 0 - a -= eval { return 1 } + a -= (return 1) } <: f() `); eq(res, NUM(1)); - assert.rejects(() => exe('let a = 0; a -= eval { return 1 }')); + assert.rejects(() => exe('let a = 0; a -= (return 1)')); }); }); test.concurrent('in array', async () => { const res = await exe(` @f() { - let a = [eval { return 1 }] + let a = [return 1] } <: f() `); eq(res, NUM(1)); - assert.rejects(() => exe('<: [eval { return 1 }]')); + assert.rejects(() => exe('<: [return 1]')); }); test.concurrent('in object', async () => { const res = await exe(` @f() { let o = { - p: eval { return 1 } + p: (return 1) } } <: f() `); eq(res, NUM(1)); - assert.rejects(() => exe('<: { p: eval { return 1 } }')); + assert.rejects(() => exe('<: { p: (return 1) }')); }); test.concurrent('in prop', async () => { const res = await exe(` @f() { let p = { - p: eval { return 1 } + p: (return 1) }.p } <: f() `); eq(res, NUM(1)); - assert.rejects(() => exe('<: { p: eval { return 1 } }.p')); + assert.rejects(() => exe('<: { p: (return 1) }.p')); }); describe('in index', () => { test.concurrent('target', async () => { const res = await exe(` @f() { - let v = [eval { return 1 }][0] + let v = [return 1][0] } <: f() `); eq(res, NUM(1)); - assert.rejects(() => exe('<: [eval { return 1 }][0]')); + assert.rejects(() => exe('<: [return 1][0]')); }); test.concurrent('index', async () => { const res = await exe(` @f() { - let v = [1][eval { return 0 }] + let v = [1][return 0] } <: f() `); eq(res, NUM(0)); - assert.rejects(() => exe('<: [0][eval { return 1 }]')); + assert.rejects(() => exe('<: [0][return 1]')); }); }); + test.concurrent('in plus', async () => { + const res = await exe(` + @f() { + let b = +(return true) + } + <: f() + `); + eq(res, BOOL(true)); + assert.rejects(() => exe('<: +(return true)')); + }); + + test.concurrent('in minus', async () => { + const res = await exe(` + @f() { + let b = -(return true) + } + <: f() + `); + eq(res, BOOL(true)); + assert.rejects(() => exe('<: -(return true)')); + }); + test.concurrent('in not', async () => { const res = await exe(` @f() { - let b = !eval { return true } + let b = !(return true) } <: f() `); eq(res, BOOL(true)); - assert.rejects(() => exe('<: !eval { return true }')); + assert.rejects(() => exe('<: !(return true)')); }); test.concurrent('in function default param', async () => { const res = await exe(` @f() { - let g = @(x = eval { return 1 }) {} + let g = @(x = (return 1)) {} } <: f() `); eq(res, NUM(1)); - assert.rejects(() => exe('<: @(x = eval { return 1 }){}')); + assert.rejects(() => exe('<: @(x = (return 1)){}')); }); test.concurrent('in template', async () => { const res = await exe(` @f() { - let s = \`{eval { return 1 }}\` + let s = \`{(return 1)}\` } <: f() `); @@ -498,35 +532,52 @@ describe('return', () => { test.concurrent('in return', async () => { const res = await exe(` @f() { - return eval { return 1 } + 2 + return (return 1) + 2 } <: f() `); eq(res, NUM(1)); - assert.rejects(() => exe('return eval { return 1 } + 2')); + assert.rejects(() => exe('return (return 1) + 2')); + }); + + test.concurrent('in break', async () => { + const res = await exe(` + @f() { + for 1 { + break (return 1) + 2 + } + } + <: f() + `) + eq(res, NUM(1)); + assert.rejects(() => exe(` + for 1 { + break (return 1) + 2 + } + `)); }); describe('in and', async () => { test.concurrent('left', async () => { const res = await exe(` @f() { - eval { return 1 } && false + (return 1) && false } <: f() `); eq(res, NUM(1)); - assert.rejects(() => exe('eval { return 1 } && false')); + assert.rejects(() => exe('(return 1) && false')); }); test.concurrent('right', async () => { const res = await exe(` @f() { - true && eval { return 1 } + true && (return 1) } <: f() `); eq(res, NUM(1)); - assert.rejects(() => exe('true && eval { return 1 }')); + assert.rejects(() => exe('true && (return 1)')); }); }); @@ -534,23 +585,23 @@ describe('return', () => { test.concurrent('left', async () => { const res = await exe(` @f() { - eval { return 1 } || false + (return 1) || false } <: f() `); eq(res, NUM(1)); - assert.rejects(() => exe('eval { return 1 } || false')); + assert.rejects(() => exe('(return 1) || false')); }); test.concurrent('right', async () => { const res = await exe(` @f() { - false || eval { return 1 } + false || (return 1) } <: f() `); eq(res, NUM(1)); - assert.rejects(() => exe('false || eval { return 1 }')); + assert.rejects(() => exe('false || (return 1)')); }); }); }); @@ -624,6 +675,552 @@ describe('break', () => { } `)); }); + + test.concurrent('as expr', async () => { + const res = await exe(` + var x = true + for 1 { + x = false || break + } + <: x + `); + eq(res, BOOL(true)); + assert.rejects(() => exe('<: false || break')); + }); + + test.concurrent('invalid label', async () => { + assert.rejects(() => exe(` + for 1 { + break #l + } + `)); + }); + + describe('with expr', () => { + test.concurrent('in each', async () => { + const res = await exe(` + <: each let v, [0] { + break 1 + } + `); + eq(res, NUM(1)); + }); + + test.concurrent('in for times', async () => { + const res = await exe(` + <: for 1 { + break 1 + } + `); + eq(res, NUM(1)); + }); + + test.concurrent('in for range', async () => { + const res = await exe(` + <: for let i, 1 { + break 1 + } + `); + eq(res, NUM(1)); + }); + + test.concurrent('in loop', async () => { + const res = await exe(` + <: loop { + break 1 + } + `); + eq(res, NUM(1)); + }); + + test.concurrent('toplevel', async () => { + assert.rejects(() => exe('break 1')); + }); + }); + + describe('labeled each', () => { + test.concurrent('inner each', async () => { + const res = await exe(` + var x = 0 + #l: each let v, [0] { + each let v, [0] { + x = 1 + break #l + } + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner for', async () => { + const res = await exe(` + var x = 0 + #l: each let v, [0] { + for 1 { + x = 1 + break #l + } + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner loop', async () => { + const res = await exe(` + var x = 0 + #l: each let v, [0] { + loop { + x = 1 + break #l + } + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner do-while', async () => { + const res = await exe(` + var x = 0 + #l: each let v, [0] { + do { + x = 1 + break #l + } while false + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner while', async () => { + const res = await exe(` + var x = 0 + #l: each let v, [0] { + while true { + x = 1 + break #l + } + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + }); + + describe('labeled for', () => { + test.concurrent('inner each', async () => { + const res = await exe(` + var x = 0 + #l: for 1 { + each let v, [0] { + x = 1 + break #l + } + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner for', async () => { + const res = await exe(` + var x = 0 + #l: for 1 { + for 1 { + x = 1 + break #l + } + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner loop', async () => { + const res = await exe(` + var x = 0 + #l: for 1 { + loop { + x = 1 + break #l + } + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner do-while', async () => { + const res = await exe(` + var x = 0 + #l: for 1 { + do { + x = 1 + break #l + } while false + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner while', async () => { + const res = await exe(` + var x = 0 + #l: for 1 { + while true { + x = 1 + break #l + } + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + }); + + describe('labeled loop', () => { + test.concurrent('inner each', async () => { + const res = await exe(` + var x = 0 + #l: loop { + each let v, [0] { + x = 1 + break #l + } + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner for', async () => { + const res = await exe(` + var x = 0 + #l: loop { + for 1 { + x = 1 + break #l + } + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner loop', async () => { + const res = await exe(` + var x = 0 + #l: loop { + loop { + x = 1 + break #l + } + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner do-while', async () => { + const res = await exe(` + var x = 0 + #l: loop { + do { + x = 1 + break #l + } while false + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner while', async () => { + const res = await exe(` + var x = 0 + #l: loop { + while true { + x = 1 + break #l + } + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + }); + + describe('labeled do-while', () => { + test.concurrent('inner each', async () => { + const res = await exe(` + var x = 0 + #l: do { + each let v, [0] { + x = 1 + break #l + } + x = 2 + } while false + <: x + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner for', async () => { + const res = await exe(` + var x = 0 + #l: do { + for 1 { + x = 1 + break #l + } + x = 2 + } while false + <: x + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner loop', async () => { + const res = await exe(` + var x = 0 + #l: do { + loop { + x = 1 + break #l + } + x = 2 + } while false + <: x + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner do-while', async () => { + const res = await exe(` + var x = 0 + #l: do { + do { + x = 1 + break #l + } while false + x = 2 + } while false + <: x + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner while', async () => { + const res = await exe(` + var x = 0 + #l: do { + while true { + x = 1 + break #l + } + x = 2 + } while false + <: x + `); + eq(res, NUM(1)); + }); + }); + + describe('labeled while', () => { + test.concurrent('inner each', async () => { + const res = await exe(` + var x = 0 + #l: while true { + each let v, [0] { + x = 1 + break #l + } + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner for', async () => { + const res = await exe(` + var x = 0 + #l: while true { + for 1 { + x = 1 + break #l + } + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner loop', async () => { + const res = await exe(` + var x = 0 + #l: while true { + loop { + x = 1 + break #l + } + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner do-while', async () => { + const res = await exe(` + var x = 0 + #l: while true { + do { + x = 1 + break #l + } while false + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner while', async () => { + const res = await exe(` + var x = 0 + #l: while true { + while true { + x = 1 + break #l + } + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + }); + + describe('labeled if', () => { + test.concurrent('simple break', async () => { + const res = await exe(` + <: #l: if true { + break #l + 2 + } + `); + eq(res, NULL); + }); + + describe('with expr', () => { + test.concurrent('inner each', async () => { + const res = await exe(` + <: #l: if true { + each let v, [0] { + break #l 1 + } + } + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner for', async () => { + const res = await exe(` + <: #l: if true { + for 1 { + break #l 1 + } + } + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner loop', async () => { + const res = await exe(` + <: #l: if true { + loop { + break #l 1 + } + } + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner if', async () => { + const res = await exe(` + <: #l: if true { + if true { + break #l 1 + 2 + } + } + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner match', async () => { + const res = await exe(` + <: #l: if true { + match 0 { + default => { + break #l 1 + 2 + } + } + } + `); + eq(res, NUM(1)); + }); + }); + }); + + describe('labeled match', () => { + test.concurrent('simple break', async () => { + const res = await exe(` + <: #l: match 0 { + default => { + break #l + 2 + } + } + `); + eq(res, NULL); + }); + }); + + describe('labeled eval', () => { + test.concurrent('simple break', async () => { + const res = await exe(` + <: #l: eval { + break #l + 2 + } + `); + eq(res, NULL); + }); + }); }); describe('continue', () => { @@ -695,4 +1292,305 @@ describe('continue', () => { } `)); }); + + test.concurrent('as expr', async () => { + const res = await exe(` + var x = true + for 1 { + x = false || continue + } + <: x + `); + eq(res, BOOL(true)); + assert.rejects(() => exe('<: false || continue')); + }); + + test.concurrent('invalid label', async () => { + assert.rejects(() => exe(` + for 1 { + continue #l + } + `)); + assert.rejects(() => exe(` + #l: eval { + continue #l + } + `)); + }); + + describe('labeled each', () => { + test.concurrent('inner each', async () => { + const res = await exe(` + var x = 0 + #l: each let v, [0] { + each let v, [0] { + x = 1 + continue #l + } + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner for times', async () => { + const res = await exe(` + var x = 0 + #l: each let v, [0] { + for 1 { + x = 1 + continue #l + } + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner for range', async () => { + const res = await exe(` + var x = 0 + #l: each let v, [0] { + for let i, 1 { + x = 1 + continue #l + } + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner loop', async () => { + const res = await exe(` + var x = 0 + #l: each let v, [0] { + loop { + x = 1 + continue #l + } + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner do-while', async () => { + const res = await exe(` + var x = 0 + #l: each let v, [0] { + do { + x = 1 + continue #l + } while false + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner while', async () => { + const res = await exe(` + var x = 0 + #l: each let v, [0] { + while true { + x = 1 + continue #l + } + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + }); + + describe('labeled for', () => { + test.concurrent('inner each', async () => { + const res = await exe(` + var x = 0 + #l: for 1 { + each let v, [0] { + x = 1 + continue #l + } + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner for time', async () => { + const res = await exe(` + var x = 0 + #l: for 1 { + for 1 { + x = 1 + continue #l + } + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner for range', async () => { + const res = await exe(` + var x = 0 + #l: for 1 { + for let i, 1 { + x = 1 + continue #l + } + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner loop', async () => { + const res = await exe(` + var x = 0 + #l: for 1 { + loop { + x = 1 + continue #l + } + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner do-while', async () => { + const res = await exe(` + var x = 0 + #l: for 1 { + do { + x = 1 + continue #l + } while false + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner while', async () => { + const res = await exe(` + var x = 0 + #l: for 1 { + while true { + x = 1 + continue #l + } + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + }); + + describe('labeled while', () => { + test.concurrent('inner each', async () => { + const res = await exe(` + var x = 0 + #l: while x == 0 { + each let v, [0] { + x = 1 + continue #l + } + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner for time', async () => { + const res = await exe(` + var x = 0 + #l: while x == 0 { + for 1 { + x = 1 + continue #l + } + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner for range', async () => { + const res = await exe(` + var x = 0 + #l: while x == 0 { + for let i, 1 { + x = 1 + continue #l + } + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner loop', async () => { + const res = await exe(` + var x = 0 + #l: while x == 0 { + loop { + x = 1 + continue #l + } + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner do-while', async () => { + const res = await exe(` + var x = 0 + #l: while x == 0 { + do { + x = 1 + continue #l + } while false + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + + test.concurrent('inner while', async () => { + const res = await exe(` + var x = 0 + #l: while x == 0 { + while true { + x = 1 + continue #l + } + x = 2 + } + <: x + `); + eq(res, NUM(1)); + }); + }); }); diff --git a/test/keywords.ts b/test/keywords.ts index 1311fd47..737577f9 100644 --- a/test/keywords.ts +++ b/test/keywords.ts @@ -64,9 +64,38 @@ const sampleCodes = Object.entries({ ` ### ${word} 1 `, + + for: word => + ` + #${word}: for 1 {} + `, + + each: word => + ` + #${word}: each let v, [0] {} + `, + + while: word => + ` + #${word}: while false {} + `, + + break: word => + ` + #${word}: for 1 { + break #${word} + } + `, + + continue: word => + ` + #${word}: for 1 { + continue #${word} + } + `, }); -function pickRandom(arr: T[]): T { +function pickRandom(arr: readonly T[]): T { return arr[Math.floor(Math.random() * arr.length)]; } diff --git a/test/primitive-props.ts b/test/primitive-props.ts index 6ea2cdab..badeff65 100644 --- a/test/primitive-props.ts +++ b/test/primitive-props.ts @@ -89,7 +89,7 @@ describe('str', () => { 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() + ].map(@(v){if v { '1' } else { '0' }}).join() `); eq(res, STR('11111111')); }); @@ -485,7 +485,7 @@ describe('arr', () => { 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() + ].map(@(v){if v { '1' } else { '0' }}).join() `); eq(res, STR('11111111')); }); diff --git a/test/syntax.ts b/test/syntax.ts index e6b3b938..48623df2 100644 --- a/test/syntax.ts +++ b/test/syntax.ts @@ -723,7 +723,7 @@ describe('for', () => { const res = await exe(` var count = 0 for (let i, 20) { - if (i == 11) break + if i == 11 { break } count += i } <: count @@ -735,7 +735,7 @@ describe('for', () => { const res = await exe(` var count = 0 for (let i, 10) { - if (i == 5) continue + if i == 5 { continue } count = (count + 1) } <: count @@ -743,15 +743,6 @@ describe('for', () => { 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(` @@ -774,6 +765,24 @@ describe('for', () => { `); }); }); + + test.concurrent('with label', async () => { + const res = await exe(` + var count = 0 + #label: for (let i, 10) { + count += i + 1 + } + <: count + `); + eq(res, NUM(55)); + }); + + test.concurrent('expr', async () => { + const res = await exe(` + <: for 1 { "a" } + `); + eq(res, NULL); + }); }); describe('each', () => { @@ -801,7 +810,7 @@ describe('each', () => { const res = await exe(` let msgs = [] each let item, ["ai", "chan", "kawaii", "yo"] { - if (item == "kawaii") break + if item == "kawaii" { break } msgs.push([item, "!"].join()) } <: msgs @@ -809,15 +818,6 @@ describe('each', () => { 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(` @@ -831,6 +831,24 @@ describe('each', () => { } assert.fail(); }); + + test.concurrent('with label', async () => { + const res = await exe(` + let msgs = [] + #label: each let item, ["ai", "chan", "kawaii"] { + msgs.push([item, "!"].join()) + } + <: msgs + `); + eq(res, ARR([STR('ai!'), STR('chan!'), STR('kawaii!')])); + }); + + test.concurrent('expr', async () => { + const res = await exe(` + <: each let v, [0] { "a" } + `); + eq(res, NULL); + }); }); describe('while', () => { @@ -853,6 +871,24 @@ describe('while', () => { `); eq(res, NULL); }); + + test.concurrent('with label', async () => { + const res = await exe(` + var count = 0 + #label: while count < 42 { + count += 1 + } + <: count + `); + eq(res, NUM(42)); + }); + + test.concurrent('expr', async () => { + const res = await exe(` + <: while false { "a" } + `); + eq(res, NULL); + }); }); describe('do-while', () => { @@ -875,6 +911,24 @@ describe('do-while', () => { `); eq(res, STR('hoge')); }); + + test.concurrent('with label', async () => { + const res = await exe(` + var count = 0 + do { + count += 1 + } while count < 42 + <: count + `); + eq(res, NUM(42)); + }); + + test.concurrent('expr', async () => { + const res = await exe(` + <: do { "a" } while false + `); + eq(res, NULL); + }); }); describe('loop', () => { @@ -882,7 +936,7 @@ describe('loop', () => { const res = await exe(` var count = 0 loop { - if (count == 10) break + if count == 10 { break } count = (count + 1) } <: count @@ -896,14 +950,33 @@ describe('loop', () => { var b = [] loop { var x = a.shift() - if (x == "chan") continue - if (x == "yo") break + if x == "chan" { continue } + if x == "yo" { break } b.push(x) } <: b `); eq(res, ARR([STR('ai'), STR('kawaii')])); }); + + test.concurrent('with label', async () => { + const res = await exe(` + var count = 0 + #label: loop { + if count == 10 { break } + count = (count + 1) + } + <: count + `); + eq(res, NUM(10)); + }); + + test.concurrent('expr', async () => { + const res = await exe(` + <: loop { break } + `); + eq(res, NULL); + }); }); /* @@ -1369,7 +1442,7 @@ describe('Infix expression', () => { }); test.concurrent('number + if expression', async () => { - eq(await exe('<: 1 + if true 1 else 2'), NUM(2)); + eq(await exe('<: 1 + if true { 1 } else { 2 }'), NUM(2)); }); test.concurrent('number + match expression', async () => { @@ -1520,12 +1593,12 @@ describe('if', () => { test.concurrent('expr', async () => { const res1 = await exe(` - <: if true "ai" else "kawaii" + <: if true { "ai" } else { "kawaii" } `); eq(res1, STR('ai')); const res2 = await exe(` - <: if false "ai" else "kawaii" + <: if false { "ai" } else { "kawaii" } `); eq(res2, STR('kawaii')); }); @@ -1550,6 +1623,13 @@ describe('if', () => { `); }); }); + + test.concurrent('expr with label', async () => { + const res = await exe(` + <: #label: if true { 1 } + `); + eq(res, NUM(1)); + }); }); describe('eval', () => { @@ -1565,6 +1645,13 @@ describe('eval', () => { `); eq(res, NUM(3)); }); + + test.concurrent('expr with label', async () => { + const res = await exe(` + <: #label: eval { 1 } + `); + eq(res, NUM(1)); + }); }); describe('match', () => { @@ -1646,6 +1733,13 @@ describe('match', () => { `); }); }); + + test.concurrent('expr with label', async () => { + const res = await exe(` + <: #label: match 0 { default => 1 } + `); + eq(res, NUM(1)); + }); }); describe('exists', () => { diff --git a/unreleased/control-flow.md b/unreleased/control-flow.md new file mode 100644 index 00000000..2d75b19d --- /dev/null +++ b/unreleased/control-flow.md @@ -0,0 +1,25 @@ +- 制御構文の変更 + - each, for, loop, do-while, while, return, break, continueは式になりました。 + - each式、for式、loop式、do-while式、while式の評価値はnullまたはbreakで返された値になります。 + - return式、break式、continue式は評価するとブロックから脱出するため、値はありません。 + - **Breaking Change** 内部に文や式を記述できる以下の構文に波括弧が必須になりました。 + - if式において、then節、elif節、else節 + - match式のcase節、default節の右辺において、文を指定する場合 (式を指定する場合、波括弧は不要) + - each式、for式、loop式、do-while式、while式の処理内容 + - **Breaking Change** return式、break式、continue式の挙動が変更されました。 + - return式は関数スコープ内でないと文法エラーになります。 + - ラベルが指定されていないbreak式およびcontinue式は反復処理式(for式、each式、loop式、while式、do-while式)のスコープ内でないと文法エラーになります。 + - return式は常に関数から脱出します。 + - ラベルが指定されていないbreak式は常に最も内側の反復処理文の処理を中断し、ループから脱出します。 + - ラベルが指定されていないcontinue式は常に最も内側の反復処理文の処理を中断し、ループの先頭に戻ります。 + - break式、continue式にラベルを付けて処理を中断するブロックを指定できるようになりました。 + - 処理を中断したいif式、match式、each式、for式、loop式、do-while式、while式にラベルをつけることができます。 + - break式、continue式に処理を中断したい式につけたラベルを指定することができます。 + - ラベルが指定されているbreak式およびcontinue式は同一ラベルが付与された式内にないとエラーになります。 + - if式、match式はbreak式で脱出された場合、その評価値はbreakで返された値になります。 + - if式、match式にcontinue式を用いることはできません。(ラベルを対応させると文法エラー) + - break式に値を指定できるようになりました。 + - 指定された値は脱出した式の評価値になります。 + - 値を省略した場合、nullが指定されたものとみなします。 + - return式の値を省略できるようになりました。 + - 値を省略した場合、nullが指定されたものとみなします。 diff --git a/unreleased/jump-statements.md b/unreleased/jump-statements.md deleted file mode 100644 index eb1c1233..00000000 --- a/unreleased/jump-statements.md +++ /dev/null @@ -1,6 +0,0 @@ -- **Breaking Change** return文、break文、continue文の挙動が変更されました。 - - return文は関数スコープ内でないと文法エラーになります。 - - break文およびcontinue文は反復処理文(for, each, while, do-while, loop)のスコープ内でないと文法エラーになります。 - - return文は常に関数から脱出します。 - - break文は常に最も内側の反復処理文の処理を中断し、ループから脱出します。 - - continue文は常に最も内側の反復処理文の処理を中断し、ループの先頭に戻ります。