From 3ad21bafc17f5b7a74a7eb6a504eabe3848c4680 Mon Sep 17 00:00:00 2001 From: takejohn Date: Mon, 4 Nov 2024 15:59:41 +0900 Subject: [PATCH 01/12] =?UTF-8?q?return,break,continue=E3=82=92Value?= =?UTF-8?q?=E3=81=A8=E5=88=A5=E3=81=AE=E6=89=B1=E3=81=84=E3=81=AB=E3=81=99?= =?UTF-8?q?=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- etc/aiscript.api.md | 40 +++++- src/interpreter/index.ts | 282 ++++++++++++++++++++++++++------------- src/interpreter/value.ts | 101 +++++++++++++- test/syntax.ts | 182 +++++++++++++++++++++++++ 4 files changed, 499 insertions(+), 106 deletions(-) diff --git a/etc/aiscript.api.md b/etc/aiscript.api.md index a7d93753..d7e9948f 100644 --- a/etc/aiscript.api.md +++ b/etc/aiscript.api.md @@ -121,6 +121,9 @@ function assertObject(val: Value | null | undefined): asserts val is VObj; // @public (undocumented) function assertString(val: Value | null | undefined): asserts val is VStr; +// @public (undocumented) +function assertValue(v: Value | Control): asserts v is Value; + // @public (undocumented) type Assign = NodeBase & { type: 'assign'; @@ -219,7 +222,7 @@ type Bool = NodeBase & { }; // @public (undocumented) -const BREAK: () => Value; +const BREAK: () => VBreak; // @public (undocumented) type Break = NodeBase & { @@ -234,13 +237,16 @@ type Call = NodeBase & { }; // @public (undocumented) -const CONTINUE: () => Value; +const CONTINUE: () => VContinue; // @public (undocumented) type Continue = NodeBase & { type: 'continue'; }; +// @public (undocumented) +type Control = VReturn | VBreak | VContinue; + // @public (undocumented) type Definition = NodeBase & { type: 'def'; @@ -306,6 +312,15 @@ 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 | Pow | Mul | Div | Rem | Add | Sub | Lt | Lteq | Gt | Gteq | Eq | Neq | And | Or | Identifier | Call | Index | Prop; +// @public (undocumented) +function extractControl(v: (Value | Control)[]): { + type: 'values'; + values: Value[]; +} | { + type: 'control'; + control: Control; +}; + // @public (undocumented) const FALSE: { type: "bool"; @@ -426,6 +441,9 @@ function isArray(val: Value): val is VArr; // @public (undocumented) function isBoolean(val: Value): val is VBool; +// @public (undocumented) +function isControl(v: Value | Control): v is Control; + // @public (undocumented) function isExpression(x: Node_2): x is Expression; @@ -444,6 +462,9 @@ function isStatement(x: Node_2): x is Statement; // @public (undocumented) function isString(val: Value): val is VStr; +// @public (undocumented) +function isValue(v: Value | Control): v is Value; + // @public (undocumented) function jsToVal(val: unknown): Value; @@ -623,7 +644,7 @@ type Rem = NodeBase & { function reprValue(value: Value, literalLike?: boolean, processedObjects?: Set): string; // @public (undocumented) -const RETURN: (v: VReturn["value"]) => Value; +const RETURN: (v: VReturn["value"]) => VReturn; // @public (undocumented) type Return = NodeBase & { @@ -699,7 +720,7 @@ const TRUE: { type TypeSource = NamedTypeSource | FnTypeSource; // @public (undocumented) -const unWrapRet: (v: Value) => Value; +function unWrapRet(v: Value | Control): Value; declare namespace utils { export { @@ -734,10 +755,15 @@ function valToJs(val: Value): JsValue; function valToString(val: Value, simple?: boolean): string; // @public (undocumented) -type Value = (VNull | VBool | VNum | VStr | VArr | VObj | VFn | VReturn | VBreak | VContinue | VError) & Attr_2; +type Value = (VNull | VBool | VNum | VStr | VArr | VObj | VFn | VError) & Attr_2; declare namespace values { export { + unWrapRet, + assertValue, + isValue, + isControl, + extractControl, VNull, VBool, VNum, @@ -754,6 +780,7 @@ declare namespace values { VError, Attr_2 as Attr, Value, + Control, NULL, TRUE, FALSE, @@ -767,7 +794,6 @@ declare namespace values { RETURN, BREAK, CONTINUE, - unWrapRet, ERROR } } @@ -867,7 +893,7 @@ type VUserFn = VFnBase & { // Warnings were encountered during analysis: // // src/interpreter/index.ts:43:4 - (ae-forgotten-export) The symbol "LogObject" needs to be exported by the entry point index.d.ts -// 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:48: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 490b8aa4..3290d555 100644 --- a/src/interpreter/index.ts +++ b/src/interpreter/index.ts @@ -8,11 +8,11 @@ import * as Ast from '../node.js'; import { Scope } from './scope.js'; import { std } from './lib/std.js'; import { assertNumber, assertString, assertFunction, assertBoolean, assertObject, assertArray, eq, isObject, isArray, expectAny, reprValue, isFunction } from './util.js'; -import { NULL, RETURN, unWrapRet, FN_NATIVE, BOOL, NUM, STR, ARR, OBJ, FN, BREAK, CONTINUE, ERROR } from './value.js'; +import { NULL, RETURN, unWrapRet, FN_NATIVE, BOOL, NUM, STR, ARR, OBJ, FN, BREAK, CONTINUE, ERROR, assertValue, isControl, extractControl } from './value.js'; import { getPrimProp } from './primitive-props.js'; import { Variable } from './variable.js'; import type { JsValue } from './util.js'; -import type { Value, VFn } from './value.js'; +import type { Control, Value, VFn, VUserFn } from './value.js'; export type LogObject = { scope?: string; @@ -103,6 +103,7 @@ export class Interpreter { try { await this.collectNs(script); const result = await this._run(script, this.scope, []); + assertValue(result); this.log('end', { val: result }); } catch (e) { this.handleError(e); @@ -230,6 +231,7 @@ export class Interpreter { } const value = await this._eval(node.expr, nsScope, []); + assertValue(value); if ( node.expr.type === 'fn' && isFunction(value) @@ -282,12 +284,27 @@ export class Interpreter { } @autobind - private _evalClause(node: Ast.Statement | Ast.Expression, scope: Scope, callStack: readonly CallInfo[]): Promise { + 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 _eval(node: Ast.Node, scope: Scope, callStack: readonly CallInfo[]): Promise { + private async _evalBinaryOperation(op: string, leftExpr: Ast.Expression, rightExpr: Ast.Expression, scope: Scope, callStack: readonly CallInfo[]): Promise { + const callee = scope.get(op); + assertFunction(callee); + const left = await this._eval(leftExpr, scope, callStack); + if (isControl(left)) { + return left; + } + const right = await this._eval(rightExpr, scope, callStack); + if (isControl(right)) { + return right; + } + return this._fn(callee, [left, right], callStack); + } + + @autobind + private _eval(node: Ast.Node, scope: Scope, callStack: readonly CallInfo[]): Promise { return this.__eval(node, scope, callStack).catch(e => { if (e.pos) throw e; else { @@ -308,7 +325,7 @@ export class Interpreter { } @autobind - private async __eval(node: Ast.Node, scope: Scope, callStack: readonly CallInfo[]): Promise { + private async __eval(node: Ast.Node, scope: Scope, callStack: readonly CallInfo[]): Promise { if (this.stop) return NULL; // irqRateが小数の場合は不等間隔になる if (this.irqRate !== 0 && this.stepCount % this.irqRate >= this.irqRate - 1) { @@ -322,19 +339,31 @@ export class Interpreter { switch (node.type) { case 'call': { const callee = await this._eval(node.target, scope, callStack); + if (isControl(callee)) { + return callee; + } assertFunction(callee); - const args = await Promise.all(node.args.map(expr => this._eval(expr, scope, callStack))); - return this._fn(callee, args, callStack, node.loc.start); + const args = extractControl(await Promise.all(node.args.map(expr => this._eval(expr, scope, callStack)))); + if (args.type === 'control') { + return args.control; + } + return this._fn(callee, args.values, callStack, node.loc.start); } case 'if': { const cond = await this._eval(node.cond, scope, callStack); + if (isControl(cond)) { + return cond; + } assertBoolean(cond); if (cond.value) { return this._evalClause(node.then, scope, callStack); } for (const elseif of node.elseif) { const cond = await this._eval(elseif.cond, scope, callStack); + if (isControl(cond)) { + return cond; + } assertBoolean(cond); if (cond.value) { return this._evalClause(elseif.then, scope, callStack); @@ -348,8 +377,14 @@ export class Interpreter { case 'match': { const about = await this._eval(node.about, scope, callStack); + if (isControl(about)) { + return about; + } for (const qa of node.qs) { const q = await this._eval(qa.q, scope, callStack); + if (isControl(q)) { + return q; + } if (eq(about, q)) { return await this._evalClause(qa.a, scope, callStack); } @@ -376,6 +411,9 @@ export class Interpreter { case 'for': { if (node.times) { const times = await this._eval(node.times, scope, callStack); + if (isControl(times)) { + return times; + } assertNumber(times); for (let i = 0; i < times.value; i++) { const v = await this._evalClause(node.for, scope, callStack); @@ -387,7 +425,13 @@ export class Interpreter { } } else { const from = await this._eval(node.from!, scope, callStack); + if (isControl(from)) { + return from; + } const to = await this._eval(node.to!, scope, callStack); + if (isControl(to)) { + return to; + } assertNumber(from); assertNumber(to); for (let i = from.value; i < from.value + to.value; i++) { @@ -409,6 +453,9 @@ export class Interpreter { case 'each': { const items = await this._eval(node.items, scope, callStack); + if (isControl(items)) { + return items; + } assertArray(items); for (const item of items.value) { const eachScope = scope.createChildScope(); @@ -425,12 +472,19 @@ export class Interpreter { case 'def': { const value = await this._eval(node.expr, scope, callStack); + if (isControl(value)) { + return value; + } if (node.attr.length > 0) { const attrs: Value['attr'] = []; for (const nAttr of node.attr) { + const value = await this._eval(nAttr.value, scope, callStack); + if (isControl(value)) { + return value; + } attrs.push({ name: nAttr.name, - value: await this._eval(nAttr.value, scope, callStack), + value, }); } value.attr = attrs; @@ -453,29 +507,56 @@ export class Interpreter { case 'assign': { const v = await this._eval(node.expr, scope, callStack); + if (isControl(v)) { + return v; + } - await this.assign(scope, node.dest, v, callStack); + const control = await this.assign(scope, node.dest, v, callStack); + if (control != null) { + return control; + } return NULL; } case 'addAssign': { const target = await this._eval(node.dest, scope, callStack); + if (isControl(target)) { + return target; + } assertNumber(target); const v = await this._eval(node.expr, scope, callStack); + if (isControl(v)) { + return v; + } assertNumber(v); + if (isControl(v)) { + return v; + } - await this.assign(scope, node.dest, NUM(target.value + v.value), callStack); + const control = await this.assign(scope, node.dest, NUM(target.value + v.value), callStack); + if (control != null) { + return control; + } return NULL; } case 'subAssign': { const target = await this._eval(node.dest, scope, callStack); + if (isControl(target)) { + return target; + } assertNumber(target); const v = await this._eval(node.expr, scope, callStack); + if (isControl(v)) { + return v; + } assertNumber(v); - await this.assign(scope, node.dest, NUM(target.value - v.value), callStack); + const control = await this.assign(scope, node.dest, NUM(target.value - v.value), callStack); + if (control != null) { + return control; + } return NULL; } @@ -487,20 +568,33 @@ export class Interpreter { case 'str': return STR(node.value); - case 'arr': return ARR(await Promise.all( - node.value.map(item => this._eval(item, scope, callStack)) - )); + case 'arr': { + const value = extractControl(await Promise.all( + node.value.map(item => this._eval(item, scope, callStack)) + )); + if (value.type === 'control') { + return value.control; + } + return ARR(value.values); + } case 'obj': { const obj = new Map(); - for (const [key, value] of node.value) { - obj.set(key, await this._eval(value, scope, callStack)); + for (const [key, valueExpr] of node.value) { + const value = await this._eval(valueExpr, scope, callStack); + if (isControl(value)) { + return value; + } + obj.set(key, value); } return OBJ(obj); } case 'prop': { const target = await this._eval(node.target, scope, callStack); + if (isControl(target)) { + return target; + } if (isObject(target)) { if (target.value.has(node.name)) { return target.value.get(node.name)!; @@ -514,7 +608,13 @@ export class Interpreter { case 'index': { const target = await this._eval(node.target, scope, callStack); + if (isControl(target)) { + return target; + } const i = await this._eval(node.index, scope, callStack); + if (isControl(i)) { + return i; + } if (isArray(target)) { assertNumber(i); const item = target.value[i.value]; @@ -536,22 +636,33 @@ export class Interpreter { case 'not': { const v = await this._eval(node.expr, scope, callStack); + if (isControl(v)) { + return v; + } assertBoolean(v); return BOOL(!v.value); } case 'fn': { + const params = await Promise.all(node.params.map(async (param) => { + return { + dest: param.dest, + default: + param.default ? await this._eval(param.default, scope, callStack) : + param.optional ? NULL : + undefined, + // type: (TODO) + }; + })); + const control = params + .map((param) => param.default) + .filter((value) => value != null) + .find(isControl); + if (control != null) { + return control; + } return FN( - await Promise.all(node.params.map(async (param) => { - return { - dest: param.dest, - default: - param.default ? await this._eval(param.default, scope, callStack) : - param.optional ? NULL : - undefined, - // type: (TODO) - }; - })), + params as VUserFn['params'], node.children, scope, ); @@ -572,6 +683,9 @@ export class Interpreter { str += x; } else { const v = await this._eval(x, scope, callStack); + if (isControl(v)) { + return v; + } str += reprValue(v); } } @@ -580,6 +694,9 @@ export class Interpreter { case 'return': { const val = await this._eval(node.expr, scope, callStack); + if (isControl(val)) { + return val; + } this.log('block:return', { scope: scope.name, val: val }); return RETURN(val); } @@ -603,109 +720,67 @@ export class Interpreter { } case 'pow': { - const callee = scope.get('Core:pow'); - assertFunction(callee); - const left = await this._eval(node.left, scope, callStack); - const right = await this._eval(node.right, scope, callStack); - return this._fn(callee, [left, right], callStack); + return this._evalBinaryOperation('Core:pow', node.left, node.right, scope, callStack); } case 'mul': { - const callee = scope.get('Core:mul'); - assertFunction(callee); - const left = await this._eval(node.left, scope, callStack); - const right = await this._eval(node.right, scope, callStack); - return this._fn(callee, [left, right], callStack); + return this._evalBinaryOperation('Core:mul', node.left, node.right, scope, callStack); } case 'div': { - const callee = scope.get('Core:div'); - assertFunction(callee); - const left = await this._eval(node.left, scope, callStack); - const right = await this._eval(node.right, scope, callStack); - return this._fn(callee, [left, right], callStack); + return this._evalBinaryOperation('Core:div', node.left, node.right, scope, callStack); } case 'rem': { - const callee = scope.get('Core:mod'); - assertFunction(callee); - const left = await this._eval(node.left, scope, callStack); - const right = await this._eval(node.right, scope, callStack); - return this._fn(callee, [left, right], callStack); + return this._evalBinaryOperation('Core:mod', node.left, node.right, scope, callStack); } case 'add': { - const callee = scope.get('Core:add'); - assertFunction(callee); - const left = await this._eval(node.left, scope, callStack); - const right = await this._eval(node.right, scope, callStack); - return this._fn(callee, [left, right], callStack); + return this._evalBinaryOperation('Core:add', node.left, node.right, scope, callStack); } case 'sub': { - const callee = scope.get('Core:sub'); - assertFunction(callee); - const left = await this._eval(node.left, scope, callStack); - const right = await this._eval(node.right, scope, callStack); - return this._fn(callee, [left, right], callStack); + return this._evalBinaryOperation('Core:sub', node.left, node.right, scope, callStack); } case 'lt': { - const callee = scope.get('Core:lt'); - assertFunction(callee); - const left = await this._eval(node.left, scope, callStack); - const right = await this._eval(node.right, scope, callStack); - return this._fn(callee, [left, right], callStack); + return this._evalBinaryOperation('Core:lt', node.left, node.right, scope, callStack); } case 'lteq': { - const callee = scope.get('Core:lteq'); - assertFunction(callee); - const left = await this._eval(node.left, scope, callStack); - const right = await this._eval(node.right, scope, callStack); - return this._fn(callee, [left, right], callStack); + return this._evalBinaryOperation('Core:lteq', node.left, node.right, scope, callStack); } case 'gt': { - const callee = scope.get('Core:gt'); - assertFunction(callee); - const left = await this._eval(node.left, scope, callStack); - const right = await this._eval(node.right, scope, callStack); - return this._fn(callee, [left, right], callStack); + return this._evalBinaryOperation('Core:gt', node.left, node.right, scope, callStack); } case 'gteq': { - const callee = scope.get('Core:gteq'); - assertFunction(callee); - const left = await this._eval(node.left, scope, callStack); - const right = await this._eval(node.right, scope, callStack); - return this._fn(callee, [left, right], callStack); + return this._evalBinaryOperation('Core:gteq', node.left, node.right, scope, callStack); } case 'eq': { - const callee = scope.get('Core:eq'); - assertFunction(callee); - const left = await this._eval(node.left, scope, callStack); - const right = await this._eval(node.right, scope, callStack); - return this._fn(callee, [left, right], callStack); + return this._evalBinaryOperation('Core:eq', node.left, node.right, scope, callStack); } case 'neq': { - const callee = scope.get('Core:neq'); - assertFunction(callee); - const left = await this._eval(node.left, scope, callStack); - const right = await this._eval(node.right, scope, callStack); - return this._fn(callee, [left, right], callStack); + return this._evalBinaryOperation('Core:neq', node.left, node.right, scope, callStack); } case 'and': { const leftValue = await this._eval(node.left, scope, callStack); + if (isControl(leftValue)) { + return leftValue; + } assertBoolean(leftValue); if (!leftValue.value) { return leftValue; } else { const rightValue = await this._eval(node.right, scope, callStack); + if (isControl(rightValue)) { + return rightValue; + } assertBoolean(rightValue); return rightValue; } @@ -713,12 +788,18 @@ export class Interpreter { case 'or': { const leftValue = await this._eval(node.left, scope, callStack); + if (isControl(leftValue)) { + return leftValue; + } assertBoolean(leftValue); if (leftValue.value) { return leftValue; } else { const rightValue = await this._eval(node.right, scope, callStack); + if (isControl(rightValue)) { + return rightValue; + } assertBoolean(rightValue); return rightValue; } @@ -731,10 +812,10 @@ export class Interpreter { } @autobind - private async _run(program: Ast.Node[], scope: Scope, callStack: readonly CallInfo[]): Promise { + private async _run(program: Ast.Node[], scope: Scope, callStack: readonly CallInfo[]): Promise { this.log('block:enter', { scope: scope.name }); - let v: Value = NULL; + let v: Value | Control = NULL; for (let i = 0; i < program.length; i++) { const node = program[i]!; @@ -808,7 +889,7 @@ export class Interpreter { dest: Ast.Expression, value: Value, callStack: readonly CallInfo[], - ): Promise { + ): Promise { switch (dest.type) { case 'identifier': { scope.assign(dest.name, value); @@ -816,7 +897,13 @@ export class Interpreter { } case 'index': { const assignee = await this._eval(dest.target, scope, callStack); + if (isControl(assignee)) { + return assignee; + } const i = await this._eval(dest.index, scope, callStack); + if (isControl(i)) { + return i; + } if (isArray(assignee)) { assertNumber(i); if (assignee.value[i.value] === undefined) { @@ -833,6 +920,9 @@ export class Interpreter { } case 'prop': { const assignee = await this._eval(dest.target, scope, callStack); + if (isControl(assignee)) { + return assignee; + } assertObject(assignee); assignee.value.set(dest.name, value); @@ -840,16 +930,22 @@ export class Interpreter { } case 'arr': { assertArray(value); - await Promise.all(dest.value.map( + const control = (await Promise.all(dest.value.map( (item, index) => this.assign(scope, item, value.value[index] ?? NULL, callStack), - )); + ))).find((control) => control != null); + if (control != null) { + return control; + } break; } case 'obj': { assertObject(value); - await Promise.all([...dest.value].map( + const control = (await Promise.all([...dest.value].map( ([key, item]) => this.assign(scope, item, value.value.get(key) ?? NULL, callStack), - )); + ))).find((control) => control != null); + if (control != null) { + return control; + } break; } default: { diff --git a/src/interpreter/value.ts b/src/interpreter/value.ts index 6e038822..f2d321ac 100644 --- a/src/interpreter/value.ts +++ b/src/interpreter/value.ts @@ -1,3 +1,4 @@ +import { AiScriptRuntimeError } from '../error.js'; import type { Expression, Node } from '../node.js'; import type { Type } from '../type.js'; import type { Scope } from './scope.js'; @@ -87,7 +88,9 @@ export type Attr = { }[]; }; -export type Value = (VNull | VBool | VNum | VStr | VArr | VObj | VFn | VReturn | VBreak | VContinue | VError) & Attr; +export type Value = (VNull | VBool | VNum | VStr | VArr | VObj | VFn | VError) & Attr; + +export type Control = VReturn | VBreak | VContinue; export const NULL = { type: 'null' as const, @@ -141,25 +144,111 @@ export const FN_NATIVE = (fn: VNativeFn['native']): VNativeFn => ({ }); // Return文で値が返されたことを示すためのラッパー -export const RETURN = (v: VReturn['value']): Value => ({ +export const RETURN = (v: VReturn['value']): VReturn => ({ type: 'return' as const, value: v, }); -export const BREAK = (): Value => ({ +export const BREAK = (): VBreak => ({ type: 'break' as const, value: null, }); -export const CONTINUE = (): Value => ({ +export const CONTINUE = (): VContinue => ({ type: 'continue' as const, value: null, }); -export const unWrapRet = (v: Value): Value => v.type === 'return' ? v.value : v; - export const ERROR = (name: string, info?: Value): Value => ({ type: 'error' as const, value: name, info: info, }); + +export function unWrapRet(v: Value | Control): Value { + switch (v.type) { + case 'return': + return v.value; + default: { + assertValue(v); + return v; + } + } +} + +export function assertValue(v: Value | Control): asserts v is Value { + switch (v.type) { + case 'return': + throw new AiScriptRuntimeError('Invalid return'); + case 'break': + throw new AiScriptRuntimeError('Invalid break'); + case 'continue': + throw new AiScriptRuntimeError('Invalid continue'); + default: + v satisfies Value; + } +} + +export function isValue(v: Value | Control): v is Value { + switch (v.type) { + case 'null': + case 'bool': + case 'num': + case 'str': + case 'arr': + case 'obj': + case 'fn': + case 'error': + return true; + case 'return': + case 'break': + case 'continue': + return false; + } + // exhaustive check + v satisfies never; + throw new TypeError('expected value or control'); +} + +export function isControl(v: Value | Control): v is Control { + switch (v.type) { + case 'null': + case 'bool': + case 'num': + case 'str': + case 'arr': + case 'obj': + case 'fn': + case 'error': + return false; + case 'return': + case 'break': + case 'continue': + return true; + } + // exhaustive check + v satisfies never; + throw new TypeError('expected value or control'); +} + +export function extractControl(v: (Value | Control)[]): { + type: 'values', + values: Value[] +} | { + type: 'control', + control: Control +} { + const control = v.find(isControl); + if (control != null) { + return { + type: 'control', + control, + }; + } else { + // vの中にControlが見つからなかったのでvの要素は全てValueであるはず + return { + type: 'values', + values: v as Value[], + }; + } +} diff --git a/test/syntax.ts b/test/syntax.ts index 913dc32b..1d96e84f 100644 --- a/test/syntax.ts +++ b/test/syntax.ts @@ -884,6 +884,188 @@ describe('loop', () => { }); }); +describe('return', () => { + test.concurrent('as statement', async () => { + const res = await exe(` + @f() { + return 1 + } + <: f() + `); + eq(res, NUM(1)); + assert.rejects(() => exe('return 1')); + }); + + test.concurrent('in eval', async () => { + const res = await exe(` + @f() { + let a = eval { + return 1 + 2 + } + return 3 + } + <: f() + `); + eq(res, NUM(1)); + assert.rejects(() => exe('<: eval { return 1 }')); + }); + + test.concurrent('in if', async () => { + const res = await exe(` + @f() { + let a = if true { + return 1 + 2 + } + return 3 + } + <: f() + `); + eq(res, NUM(1)); + assert.rejects(() => exe('<: if true { return 1 }')); + }); + + test.concurrent('in match', async () => { + const res = await exe(` + @f() { + let a = match 0 { + default => { + return 1 + 2 + } + } + return 3 + } + <: f() + `); + eq(res, NUM(1)); + assert.rejects(() => exe('<: match 0 { default => { return 1 } }')) + }); +}); + +describe('break', () => { + test.concurrent('as statement', async () => { + const res = await exe(` + var x = 0 + for 1 { + break + x += 1 + } + <: x + `); + eq(res, NUM(0)); + assert.rejects(() => exe('break')); + assert.rejects(() => exe('@() { break }()')); + }); + + test.concurrent('in eval', async () => { + const res = await exe(` + var x = 0 + for 1 { + let a = eval { + break + } + x += 1 + } + <: x + `); + eq(res, NUM(0)); + assert.rejects(() => exe('<: eval { break }')); + }); + + test.concurrent('in if', async () => { + const res = await exe(` + var x = 0 + for 1 { + let a = if true { + break + } + x += 1 + } + <: x + `); + eq(res, NUM(0)); + assert.rejects(() => exe('<: if true { break }')); + }); + + test.concurrent('in match', async () => { + const res = await exe(` + var x = 0 + for 1 { + let a = match 0 { + default => break + } + x += 1 + } + <: x + `); + eq(res, NUM(0)); + assert.rejects(() => exe('<: if true { break }')); + }); +}); + +describe('continue', () => { + test.concurrent('as statement', async () => { + const res = await exe(` + var x = 0 + for 1 { + continue + x += 1 + } + <: x + `); + eq(res, NUM(0)); + assert.rejects(() => exe('continue')); + assert.rejects(() => exe('@() { continue }()')); + }); + + test.concurrent('in eval', async () => { + const res = await exe(` + var x = 0 + for 1 { + let a = eval { + continue + } + x += 1 + } + <: x + `); + eq(res, NUM(0)); + assert.rejects(() => exe('<: eval { continue }')); + }); + + test.concurrent('in if', async () => { + const res = await exe(` + var x = 0 + for 1 { + let a = if true { + continue + } + x += 1 + } + <: x + `); + eq(res, NUM(0)); + assert.rejects(() => exe('<: if true { continue }')); + }); + + test.concurrent('in match', async () => { + const res = await exe(` + var x = 0 + for 1 { + let a = match 0 { + default => continue + } + x += 1 + } + <: x + `); + eq(res, NUM(0)); + assert.rejects(() => exe('<: if true { continue }')); + }); +}); + /* * Global statements */ From 7b4ff3b2ab7b850e90763e06e4fd5bfb7260b306 Mon Sep 17 00:00:00 2001 From: takejohn Date: Mon, 4 Nov 2024 20:08:07 +0900 Subject: [PATCH 02/12] =?UTF-8?q?=E9=85=8D=E5=88=97=E3=82=84=E5=BC=95?= =?UTF-8?q?=E6=95=B0=E3=81=A7=E6=9C=80=E5=88=9D=E3=81=ABreturn=E3=81=AA?= =?UTF-8?q?=E3=81=A9=E3=81=8C=E5=87=BA=E3=81=9F=E6=99=82=E7=82=B9=E3=81=A7?= =?UTF-8?q?=E3=83=AA=E3=82=BF=E3=83=BC=E3=83=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/interpreter/index.ts | 28 +++++++++++++++++----------- src/interpreter/value.ts | 22 ---------------------- 2 files changed, 17 insertions(+), 33 deletions(-) diff --git a/src/interpreter/index.ts b/src/interpreter/index.ts index 3290d555..e1db8109 100644 --- a/src/interpreter/index.ts +++ b/src/interpreter/index.ts @@ -8,7 +8,7 @@ import * as Ast from '../node.js'; import { Scope } from './scope.js'; import { std } from './lib/std.js'; import { assertNumber, assertString, assertFunction, assertBoolean, assertObject, assertArray, eq, isObject, isArray, expectAny, reprValue, isFunction } from './util.js'; -import { NULL, RETURN, unWrapRet, FN_NATIVE, BOOL, NUM, STR, ARR, OBJ, FN, BREAK, CONTINUE, ERROR, assertValue, isControl, extractControl } from './value.js'; +import { NULL, RETURN, unWrapRet, FN_NATIVE, BOOL, NUM, STR, ARR, OBJ, FN, BREAK, CONTINUE, ERROR, assertValue, isControl } from './value.js'; import { getPrimProp } from './primitive-props.js'; import { Variable } from './variable.js'; import type { JsValue } from './util.js'; @@ -343,11 +343,15 @@ export class Interpreter { return callee; } assertFunction(callee); - const args = extractControl(await Promise.all(node.args.map(expr => this._eval(expr, scope, callStack)))); - if (args.type === 'control') { - return args.control; + const args = []; + for (const expr of node.args) { + const arg = await this._eval(expr, scope, callStack); + if (isControl(arg)) { + return arg; + } + args.push(arg); } - return this._fn(callee, args.values, callStack, node.loc.start); + return this._fn(callee, args, callStack, node.loc.start); } case 'if': { @@ -569,13 +573,15 @@ export class Interpreter { case 'str': return STR(node.value); case 'arr': { - const value = extractControl(await Promise.all( - node.value.map(item => this._eval(item, scope, callStack)) - )); - if (value.type === 'control') { - return value.control; + const value = []; + for (const item of node.value) { + const valueItem = await this._eval(item, scope, callStack); + if (isControl(valueItem)) { + return valueItem; + } + value.push(valueItem); } - return ARR(value.values); + return ARR(value); } case 'obj': { diff --git a/src/interpreter/value.ts b/src/interpreter/value.ts index f2d321ac..3b1e5492 100644 --- a/src/interpreter/value.ts +++ b/src/interpreter/value.ts @@ -230,25 +230,3 @@ export function isControl(v: Value | Control): v is Control { v satisfies never; throw new TypeError('expected value or control'); } - -export function extractControl(v: (Value | Control)[]): { - type: 'values', - values: Value[] -} | { - type: 'control', - control: Control -} { - const control = v.find(isControl); - if (control != null) { - return { - type: 'control', - control, - }; - } else { - // vの中にControlが見つからなかったのでvの要素は全てValueであるはず - return { - type: 'values', - values: v as Value[], - }; - } -} From 339486b7f2e3fce1bf8ad4802a971129fab4695e Mon Sep 17 00:00:00 2001 From: takejohn Date: Mon, 4 Nov 2024 21:45:02 +0900 Subject: [PATCH 03/12] =?UTF-8?q?=E4=B8=8D=E6=AD=A3=E3=81=AA=E4=BD=8D?= =?UTF-8?q?=E7=BD=AE=E3=81=AEreturn=E6=96=87=E3=81=AA=E3=81=A9=E3=82=92?= =?UTF-8?q?=E6=96=87=E6=B3=95=E3=82=A8=E3=83=A9=E3=83=BC=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/parser/index.ts | 2 + .../plugins/validate-jump-statements.ts | 35 ++++++++ src/parser/visit.ts | 82 ++++++++++--------- 3 files changed, 81 insertions(+), 38 deletions(-) create mode 100644 src/parser/plugins/validate-jump-statements.ts diff --git a/src/parser/index.ts b/src/parser/index.ts index 07092dbb..74f535fd 100644 --- a/src/parser/index.ts +++ b/src/parser/index.ts @@ -1,6 +1,7 @@ import { Scanner } from './scanner.js'; import { parseTopLevel } from './syntaxes/toplevel.js'; +import { validateJumpStatements } from './plugins/validate-jump-statements.js'; import { validateKeyword } from './plugins/validate-keyword.js'; import { validateType } from './plugins/validate-type.js'; @@ -21,6 +22,7 @@ export class Parser { validate: [ validateKeyword, validateType, + validateJumpStatements, ], transform: [ ], diff --git a/src/parser/plugins/validate-jump-statements.ts b/src/parser/plugins/validate-jump-statements.ts new file mode 100644 index 00000000..81b9cdb2 --- /dev/null +++ b/src/parser/plugins/validate-jump-statements.ts @@ -0,0 +1,35 @@ +import { visitNode } from '../visit.js'; +import { AiScriptSyntaxError } from '../../error.js'; + +import type * as Ast from '../../node.js'; + +function validateNode(node: Ast.Node, ancestors: Ast.Node[]): Ast.Node { + switch (node.type) { + case 'return': { + if (!ancestors.some(({ type }) => type === 'fn')) { + throw new AiScriptSyntaxError('return must be inside function', node.loc.start); + } + break; + } + case 'break': { + if (!ancestors.some(({ type }) => type === 'loop' || type === 'for' || type === 'each')) { + throw new AiScriptSyntaxError('break must be inside loop / for / each', node.loc.start); + } + break; + } + case 'continue': { + if (!ancestors.some(({ type }) => type === 'loop' || type === 'for' || type === 'each')) { + throw new AiScriptSyntaxError('continue must be inside loop / for / each', node.loc.start); + } + break; + } + } + return node; +} + +export function validateJumpStatements(nodes: Ast.Node[]): Ast.Node[] { + for (const node of nodes) { + visitNode(node, validateNode); + } + return nodes; +} diff --git a/src/parser/visit.ts b/src/parser/visit.ts index 84c30e34..52bbf48f 100644 --- a/src/parser/visit.ts +++ b/src/parser/visit.ts @@ -1,137 +1,142 @@ import type * as Ast from '../node.js'; -export function visitNode(node: Ast.Node, fn: (node: Ast.Node) => Ast.Node): Ast.Node { - const result = fn(node); +export function visitNode(node: Ast.Node, fn: (node: Ast.Node, ancestors: Ast.Node[]) => Ast.Node): Ast.Node { + return visitNodeInner(node, fn, []); +} + +function visitNodeInner(node: Ast.Node, fn: (node: Ast.Node, ancestors: Ast.Node[]) => Ast.Node, ancestors: Ast.Node[]): Ast.Node { + const result = fn(node, ancestors); + ancestors.push(node); // nested nodes switch (result.type) { case 'def': { - result.expr = visitNode(result.expr, fn) as Ast.Definition['expr']; + result.expr = visitNodeInner(result.expr, fn, ancestors) as Ast.Definition['expr']; break; } case 'return': { - result.expr = visitNode(result.expr, fn) as Ast.Return['expr']; + result.expr = visitNodeInner(result.expr, fn, ancestors) as Ast.Return['expr']; break; } case 'each': { - result.items = visitNode(result.items, fn) as Ast.Each['items']; - result.for = visitNode(result.for, fn) as Ast.Each['for']; + result.items = visitNodeInner(result.items, fn, ancestors) as Ast.Each['items']; + result.for = visitNodeInner(result.for, fn, ancestors) as Ast.Each['for']; break; } case 'for': { if (result.from != null) { - result.from = visitNode(result.from, fn) as Ast.For['from']; + result.from = visitNodeInner(result.from, fn, ancestors) as Ast.For['from']; } if (result.to != null) { - result.to = visitNode(result.to, fn) as Ast.For['to']; + result.to = visitNodeInner(result.to, fn, ancestors) as Ast.For['to']; } if (result.times != null) { - result.times = visitNode(result.times, fn) as Ast.For['times']; + result.times = visitNodeInner(result.times, fn, ancestors) as Ast.For['times']; } - result.for = visitNode(result.for, fn) as Ast.For['for']; + result.for = visitNodeInner(result.for, fn, ancestors) 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 Ast.Loop['statements'][number]; + result.statements[i] = visitNodeInner(result.statements[i]!, fn, ancestors) as Ast.Loop['statements'][number]; } break; } case 'addAssign': case 'subAssign': case 'assign': { - result.expr = visitNode(result.expr, fn) as Ast.Assign['expr']; - result.dest = visitNode(result.dest, fn) as Ast.Assign['dest']; + result.expr = visitNodeInner(result.expr, fn, ancestors) as Ast.Assign['expr']; + result.dest = visitNodeInner(result.dest, fn, ancestors) as Ast.Assign['dest']; break; } case 'not': { - result.expr = visitNode(result.expr, fn) as Ast.Return['expr']; + result.expr = visitNodeInner(result.expr, fn, ancestors) as Ast.Return['expr']; break; } case 'if': { - result.cond = visitNode(result.cond, fn) as Ast.If['cond']; - result.then = visitNode(result.then, fn) as Ast.If['then']; + result.cond = visitNodeInner(result.cond, fn, ancestors) as Ast.If['cond']; + result.then = visitNodeInner(result.then, fn, ancestors) as Ast.If['then']; for (const prop of result.elseif) { - prop.cond = visitNode(prop.cond, fn) as Ast.If['elseif'][number]['cond']; - prop.then = visitNode(prop.then, fn) as Ast.If['elseif'][number]['then']; + prop.cond = visitNodeInner(prop.cond, fn, ancestors) as Ast.If['elseif'][number]['cond']; + prop.then = visitNodeInner(prop.then, fn, ancestors) as Ast.If['elseif'][number]['then']; } if (result.else != null) { - result.else = visitNode(result.else, fn) as Ast.If['else']; + result.else = visitNodeInner(result.else, fn, ancestors) as Ast.If['else']; } break; } case 'fn': { for (const param of result.params) { if (param.default) { - param.default = visitNode(param.default!, fn) as Ast.Fn['params'][number]['default']; + param.default = visitNodeInner(param.default!, fn, ancestors) as Ast.Fn['params'][number]['default']; } } for (let i = 0; i < result.children.length; i++) { - result.children[i] = visitNode(result.children[i]!, fn) as Ast.Fn['children'][number]; + result.children[i] = visitNodeInner(result.children[i]!, fn, ancestors) as Ast.Fn['children'][number]; } break; } case 'match': { - result.about = visitNode(result.about, fn) as Ast.Match['about']; + result.about = visitNodeInner(result.about, fn, ancestors) as Ast.Match['about']; for (const prop of result.qs) { - prop.q = visitNode(prop.q, fn) as Ast.Match['qs'][number]['q']; - prop.a = visitNode(prop.a, fn) as Ast.Match['qs'][number]['a']; + prop.q = visitNodeInner(prop.q, fn, ancestors) as Ast.Match['qs'][number]['q']; + prop.a = visitNodeInner(prop.a, fn, ancestors) as Ast.Match['qs'][number]['a']; } if (result.default != null) { - result.default = visitNode(result.default, fn) as Ast.Match['default']; + result.default = visitNodeInner(result.default, fn, ancestors) 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 Ast.Block['statements'][number]; + result.statements[i] = visitNodeInner(result.statements[i]!, fn, ancestors) as Ast.Block['statements'][number]; } break; } case 'exists': { - result.identifier = visitNode(result.identifier,fn) as Ast.Exists['identifier']; + result.identifier = visitNodeInner(result.identifier, fn, ancestors) 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 Ast.Tmpl['tmpl'][number]; + result.tmpl[i] = visitNodeInner(item, fn, ancestors) as Ast.Tmpl['tmpl'][number]; } } break; } case 'obj': { for (const item of result.value) { - result.value.set(item[0], visitNode(item[1], fn) as Ast.Expression); + result.value.set(item[0], visitNodeInner(item[1], fn, ancestors) as Ast.Expression); } break; } case 'arr': { for (let i = 0; i < result.value.length; i++) { - result.value[i] = visitNode(result.value[i]!, fn) as Ast.Arr['value'][number]; + result.value[i] = visitNodeInner(result.value[i]!, fn, ancestors) as Ast.Arr['value'][number]; } break; } case 'call': { - result.target = visitNode(result.target, fn) as Ast.Call['target']; + result.target = visitNodeInner(result.target, fn, ancestors) as Ast.Call['target']; for (let i = 0; i < result.args.length; i++) { - result.args[i] = visitNode(result.args[i]!, fn) as Ast.Call['args'][number]; + result.args[i] = visitNodeInner(result.args[i]!, fn, ancestors) as Ast.Call['args'][number]; } break; } case 'index': { - result.target = visitNode(result.target, fn) as Ast.Index['target']; - result.index = visitNode(result.index, fn) as Ast.Index['index']; + result.target = visitNodeInner(result.target, fn, ancestors) as Ast.Index['target']; + result.index = visitNodeInner(result.index, fn, ancestors) as Ast.Index['index']; break; } case 'prop': { - result.target = visitNode(result.target, fn) as Ast.Prop['target']; + result.target = visitNodeInner(result.target, fn, ancestors) as Ast.Prop['target']; break; } case 'ns': { for (let i = 0; i < result.members.length; i++) { - result.members[i] = visitNode(result.members[i]!, fn) as (typeof result.members)[number]; + result.members[i] = visitNodeInner(result.members[i]!, fn, ancestors) as (typeof result.members)[number]; } break; } @@ -150,7 +155,7 @@ export function visitNode(node: Ast.Node, fn: (node: Ast.Node) => Ast.Node): Ast case 'neq': case 'and': case 'or': { - result.left = visitNode(result.left, fn) as ( + result.left = visitNodeInner(result.left, fn, ancestors) as ( Ast.Pow | Ast.Mul | Ast.Div | @@ -166,7 +171,7 @@ export function visitNode(node: Ast.Node, fn: (node: Ast.Node) => Ast.Node): Ast Ast.And | Ast.Or )['left']; - result.right = visitNode(result.right, fn) as ( + result.right = visitNodeInner(result.right, fn, ancestors) as ( Ast.Pow | Ast.Mul | Ast.Div | @@ -186,5 +191,6 @@ export function visitNode(node: Ast.Node, fn: (node: Ast.Node) => Ast.Node): Ast } } + ancestors.pop(); return result; } From b1d11d5dde0fee5c7d3d741de4e58354c216e0e4 Mon Sep 17 00:00:00 2001 From: takejohn Date: Tue, 5 Nov 2024 18:42:21 +0900 Subject: [PATCH 04/12] =?UTF-8?q?=E3=83=86=E3=82=B9=E3=83=88=E3=81=AE?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0=E3=81=A8=E5=AE=9F=E8=A3=85=E3=81=AE=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/interpreter/index.ts | 27 +- test/jump-statements.ts | 680 +++++++++++++++++++++++++++++++++++++++ test/syntax.ts | 182 ----------- 3 files changed, 691 insertions(+), 198 deletions(-) create mode 100644 test/jump-statements.ts diff --git a/src/interpreter/index.ts b/src/interpreter/index.ts index e1db8109..74535082 100644 --- a/src/interpreter/index.ts +++ b/src/interpreter/index.ts @@ -483,9 +483,7 @@ export class Interpreter { const attrs: Value['attr'] = []; for (const nAttr of node.attr) { const value = await this._eval(nAttr.value, scope, callStack); - if (isControl(value)) { - return value; - } + assertValue(value); attrs.push({ name: nAttr.name, value, @@ -534,9 +532,6 @@ export class Interpreter { return v; } assertNumber(v); - if (isControl(v)) { - return v; - } const control = await this.assign(scope, node.dest, NUM(target.value + v.value), callStack); if (control != null) { @@ -936,21 +931,21 @@ export class Interpreter { } case 'arr': { assertArray(value); - const control = (await Promise.all(dest.value.map( - (item, index) => this.assign(scope, item, value.value[index] ?? NULL, callStack), - ))).find((control) => control != null); - if (control != null) { - return control; + for (const [index, item] of dest.value.entries()) { + const control = await this.assign(scope, item, value.value[index] ?? NULL, callStack); + if (control != null) { + return control; + } } break; } case 'obj': { assertObject(value); - const control = (await Promise.all([...dest.value].map( - ([key, item]) => this.assign(scope, item, value.value.get(key) ?? NULL, callStack), - ))).find((control) => control != null); - if (control != null) { - return control; + for (const [key, item] of dest.value.entries()) { + const control = await this.assign(scope, item, value.value.get(key) ?? NULL, callStack); + if (control != null) { + return control; + } } break; } diff --git a/test/jump-statements.ts b/test/jump-statements.ts new file mode 100644 index 00000000..03ee4360 --- /dev/null +++ b/test/jump-statements.ts @@ -0,0 +1,680 @@ +import * as assert from 'assert'; +import { describe, test } from 'vitest'; +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'; + +describe('return', () => { + test.concurrent('as statement', async () => { + const res = await exe(` + @f() { + return 1 + } + <: f() + `); + eq(res, NUM(1)); + assert.rejects(() => exe('return 1')); + }); + + test.concurrent('in eval', async () => { + const res = await exe(` + @f() { + let a = eval { + return 1 + } + } + <: f() + `); + eq(res, NUM(1)); + assert.rejects(() => exe('<: eval { return 1 }')); + }); + + describe('in if', () => { + test.concurrent('cond', async () => { + const res = await exe(` + @f() { + let a = if eval { return true } {} + } + <: f() + `); + eq(res, BOOL(true)); + assert.rejects(() => exe('<: if eval { return true } {}')); + }); + + test.concurrent('then', async () => { + const res = await exe(` + @f() { + let a = if true { + return 1 + } + } + <: f() + `); + eq(res, NUM(1)); + assert.rejects(() => exe('<: if true { return 1 }')); + }); + + test.concurrent('elif cond', async () => { + const res = await exe(` + @f() { + let a = if false {} elif eval { return true } {} + } + <: f() + `); + eq(res, BOOL(true)); + assert.rejects(() => exe('<: if false {} elif eval { return true } {}')); + }); + + test.concurrent('elif then', async () => { + const res = await exe(` + @f() { + let a = if false { + } elif true { + return 1 + } + } + <: f() + `); + eq(res, NUM(1)); + assert.rejects(() => exe('<: if false {} elif true eval { return true }')); + }); + + test.concurrent('else', async () => { + const res = await exe(` + @f() { + let a = if false { + } else { + return 1 + } + } + <: f() + `); + eq(res, NUM(1)); + assert.rejects(() => exe('<: if false {} else eval { return true }')); + }); + }); + + describe('in match', () => { + test.concurrent('about', async () => { + const res = await exe(` + @f() { + let a = match eval { return 1 } {} + } + <: f() + `); + eq(res, NUM(1)); + assert.rejects(() => exe('<: match eval { return 1 } {}')); + }); + + test.concurrent('case q', async () => { + const res = await exe(` + @f() { + let a = match 0 { + case eval { return 0 } => { + return 1 + } + } + } + <: f() + `); + eq(res, NUM(0)); + assert.rejects(() => exe('<: match 0 { case eval { return 0 } => {} }')) + }); + + test.concurrent('case a', async () => { + const res = await exe(` + @f() { + let a = match 0 { + case 0 => { + return 1 + } + } + } + <: f() + `); + eq(res, NUM(1)); + assert.rejects(() => exe('<: match 0 { case 0 => { return 1 } }')) + }); + + test.concurrent('default', async () => { + const res = await exe(` + @f() { + let a = match 0 { + default => { + return 1 + } + } + } + <: f() + `); + eq(res, NUM(1)); + assert.rejects(() => exe('<: match 0 { default => { return 1 } }')) + }); + }); + + describe('in binary operation', () => { + test.concurrent('left', async () => { + const res = await exe(` + @f() { + eval { return 1 } + 2 + } + <: f() + `); + eq(res, NUM(1)); + assert.rejects(() => exe('<: eval { return 1 } + 2')); + }); + + test.concurrent('right', async () => { + const res = await exe(` + @f() { + 1 + eval { return 2 } + } + <: f() + `); + eq(res, NUM(2)); + assert.rejects(() => exe('<: 1 + eval { return 2 }')); + }); + }); + + describe('in call', () => { + test.concurrent('callee', async () => { + const res = await exe(` + @f() { + eval { return print }('Hello, world!') + } + f()('Hi') + `); + eq(res, STR('Hi')); + assert.rejects(() => exe(`eval { return print }('Hello, world!')`)); + }); + + test.concurrent('arg', async () => { + const res = await exe(` + @f() { + print(eval { return 'Hello, world!' }) + } + <: f() + `); + eq(res, STR('Hello, world!')); + assert.rejects(() => exe(`print(eval { return 'Hello, world' })`)) + }); + }); + + describe('in for', () => { + test.concurrent('times', async () => { + const res = await exe(` + @f() { + for eval { return 1 } {} + } + <: f() + `); + eq(res, NUM(1)); + assert.rejects(() => exe('for eval { return 1 } {}')); + }); + + test.concurrent('from', async () => { + const res = await exe(` + @f() { + for let i = eval { return 1 }, 2 {} + } + <: f() + `); + eq(res, NUM(1)); + assert.rejects(() => exe('for let i = eval { return 1 }, 2 {}')); + }); + + test.concurrent('to', async () => { + const res = await exe(` + @f() { + for let i = 0, eval { return 1 } {} + } + <: f() + `); + eq(res, NUM(1)); + assert.rejects(() => exe('for let i = 0, eval { return 1 } {}')); + }); + + test.concurrent('for', async () => { + const res = await exe(` + @f() { + for 1 { + return 1 + } + } + <: f() + `); + eq(res, NUM(1)); + assert.rejects(() => exe('for 1 { return 1 }')); + }) + }); + + describe('in each', () => { + test.concurrent('items', async () => { + const res = await exe(` + @f() { + each let v, [eval { return 1 }] {} + } + <: f() + `); + eq(res, NUM(1)); + assert.rejects(() => exe('each let v, [eval { return 1 }] {}')); + }); + + test.concurrent('for', async () => { + const res = await exe(` + @f() { + each let v, [0] { + return 1 + } + } + <: f() + `); + eq(res, NUM(1)); + assert.rejects(() => exe('each let v, [0] { return 1 }')); + }); + }); + + describe('in assign', () => { + test.concurrent('expr', async () => { + const res = await exe(` + @f() { + let a = null + a = eval { return 1 } + } + <: f() + `); + eq(res, NUM(1)); + assert.rejects(() => exe('let a = null; a = eval { return 1 }')); + }); + + test.concurrent('index target', async () => { + const res = await exe(` + @f() { + let a = [null] + eval { return a }[0] = 1 + } + <: f() + `); + eq(res, ARR([NULL])); + assert.rejects(() => exe('let a = [null]; eval { return a }[0] = 1')); + }); + + test.concurrent('index', async () => { + const res = await exe(` + @f() { + let a = [null] + a[eval { return 0 }] = 1 + } + <: f() + `); + eq(res, NUM(0)); + assert.rejects(() => exe('let a = [null]; a[eval { return 0 }] = 1')); + }); + + test.concurrent('prop target', async () => { + const res = await exe(` + @f() { + let o = {} + eval { return o }.p = 1 + } + <: f() + `); + eq(res, OBJ(new Map())); + assert.rejects(() => exe('let o = {}; eval { return o }.p = 1')); + }); + + test.concurrent('arr', async () => { + const res = await exe(` + @f() { + let o = {} + [eval { return o }.p] = [1] + } + <: f() + `); + eq(res, OBJ(new Map())); + assert.rejects(() => exe('let o = {}; [eval { return o }.p] = [1]')); + }); + + test.concurrent('obj', async () => { + const res = await exe(` + @f() { + let o = {} + { a: eval { return o }.p } = { a: 1 } + } + <: f() + `); + eq(res, OBJ(new Map())); + assert.rejects(() => exe('let o = {}; { a: eval { return o }.p } = { a: 1 }')); + }); + }); + + describe('in add assign', () => { + test.concurrent('dest', async () => { + const res = await exe(` + @f() { + let a = [0] + a[eval { return 0 }] += 1 + } + <: f() + `); + eq(res, NUM(0)); + assert.rejects(() => exe('let a = [0]; a[eval { return 0 }] += 1')); + }); + + test.concurrent('expr', async () => { + const res = await exe(` + @f() { + let a = 0 + a += eval { return 1 } + } + <: f() + `); + eq(res, NUM(1)); + assert.rejects(() => exe('let a = 0; a += eval { return 1 }')); + }); + }); + + describe('in sub assign', () => { + test.concurrent('dest', async () => { + const res = await exe(` + @f() { + let a = [0] + a[eval { return 0 }] -= 1 + } + <: f() + `); + eq(res, NUM(0)); + assert.rejects(() => exe('let a = [0]; a[eval { return 0 }] -= 1')); + }); + + test.concurrent('expr', async () => { + const res = await exe(` + @f() { + let a = 0 + a -= eval { return 1 } + } + <: f() + `); + eq(res, NUM(1)); + assert.rejects(() => exe('let a = 0; a -= eval { return 1 }')); + }); + }); + + test.concurrent('in array', async () => { + const res = await exe(` + @f() { + let a = [eval { return 1 }] + } + <: f() + `); + eq(res, NUM(1)); + assert.rejects(() => exe('<: [eval { return 1 }]')); + }); + + test.concurrent('in object', async () => { + const res = await exe(` + @f() { + let o = { + p: eval { return 1 } + } + } + <: f() + `); + eq(res, NUM(1)); + assert.rejects(() => exe('<: { p: eval { return 1 } }')); + }); + + test.concurrent('in prop', async () => { + const res = await exe(` + @f() { + let p = { + p: eval { return 1 } + }.p + } + <: f() + `); + eq(res, NUM(1)); + assert.rejects(() => exe('<: { p: eval { return 1 } }.p')); + }); + + describe('in index', () => { + test.concurrent('target', async () => { + const res = await exe(` + @f() { + let v = [eval { return 1 }][0] + } + <: f() + `); + eq(res, NUM(1)); + assert.rejects(() => exe('<: [eval { return 1 }][0]')); + }); + + test.concurrent('index', async () => { + const res = await exe(` + @f() { + let v = [1][eval { return 0 }] + } + <: f() + `); + eq(res, NUM(0)); + assert.rejects(() => exe('<: [0][eval { return 1 }]')); + }); + }); + + test.concurrent('in not', async () => { + const res = await exe(` + @f() { + let b = !eval { return true } + } + <: f() + `); + eq(res, BOOL(true)); + assert.rejects(() => exe('<: !eval { return true }')); + }); + + test.concurrent('in function default param', async () => { + const res = await exe(` + @f() { + let g = @(x = eval { return 1 }) {} + } + <: f() + `); + eq(res, NUM(1)); + assert.rejects(() => exe('<: @(x = eval { return 1 }){}')); + }); + + /* TODO: #843が解決したらコメントアウト + test.concurrent('in template', async () => { + const res = await exe(` + @f() { + let s = \`{eval { return 1 }}\` + } + <: f() + `); + eq(res, NUM(1)); + assert.rejects(() => exe('<: `{eval {return 1}}`')); + }); + */ + + test.concurrent('in return', async () => { + const res = await exe(` + @f() { + return eval { return 1 } + 2 + } + <: f() + `); + eq(res, NUM(1)); + assert.rejects(() => exe('return eval { return 1 } + 2')); + }); + + describe('in and', async () => { + test.concurrent('left', async () => { + const res = await exe(` + @f() { + eval { return 1 } && false + } + <: f() + `); + eq(res, NUM(1)); + assert.rejects(() => exe('eval { return 1 } && false')); + }); + + test.concurrent('right', async () => { + const res = await exe(` + @f() { + true && eval { return 1 } + } + <: f() + `); + eq(res, NUM(1)); + assert.rejects(() => exe('true && eval { return 1 }')); + }); + }); + + describe('in or', async () => { + test.concurrent('left', async () => { + const res = await exe(` + @f() { + eval { return 1 } || false + } + <: f() + `); + eq(res, NUM(1)); + assert.rejects(() => exe('eval { return 1 } || false')); + }); + + test.concurrent('right', async () => { + const res = await exe(` + @f() { + false || eval { return 1 } + } + <: f() + `); + eq(res, NUM(1)); + assert.rejects(() => exe('false || eval { return 1 }')); + }); + }); +}); + +describe('break', () => { + test.concurrent('as statement', async () => { + const res = await exe(` + var x = 0 + for 1 { + break + x += 1 + } + <: x + `); + eq(res, NUM(0)); + assert.rejects(() => exe('break')); + assert.rejects(() => exe('@() { break }()')); + }); + + test.concurrent('in eval', async () => { + const res = await exe(` + var x = 0 + for 1 { + let a = eval { + break + } + x += 1 + } + <: x + `); + eq(res, NUM(0)); + assert.rejects(() => exe('<: eval { break }')); + }); + + test.concurrent('in if', async () => { + const res = await exe(` + var x = 0 + for 1 { + let a = if true { + break + } + x += 1 + } + <: x + `); + eq(res, NUM(0)); + assert.rejects(() => exe('<: if true { break }')); + }); + + test.concurrent('in match', async () => { + const res = await exe(` + var x = 0 + for 1 { + let a = match 0 { + default => break + } + x += 1 + } + <: x + `); + eq(res, NUM(0)); + assert.rejects(() => exe('<: if true { break }')); + }); +}); + +describe('continue', () => { + test.concurrent('as statement', async () => { + const res = await exe(` + var x = 0 + for 1 { + continue + x += 1 + } + <: x + `); + eq(res, NUM(0)); + assert.rejects(() => exe('continue')); + assert.rejects(() => exe('@() { continue }()')); + }); + + test.concurrent('in eval', async () => { + const res = await exe(` + var x = 0 + for 1 { + let a = eval { + continue + } + x += 1 + } + <: x + `); + eq(res, NUM(0)); + assert.rejects(() => exe('<: eval { continue }')); + }); + + test.concurrent('in if', async () => { + const res = await exe(` + var x = 0 + for 1 { + let a = if true { + continue + } + x += 1 + } + <: x + `); + eq(res, NUM(0)); + assert.rejects(() => exe('<: if true { continue }')); + }); + + test.concurrent('in match', async () => { + const res = await exe(` + var x = 0 + for 1 { + let a = match 0 { + default => continue + } + x += 1 + } + <: x + `); + eq(res, NUM(0)); + assert.rejects(() => exe('<: if true { continue }')); + }); +}); diff --git a/test/syntax.ts b/test/syntax.ts index 1d96e84f..913dc32b 100644 --- a/test/syntax.ts +++ b/test/syntax.ts @@ -884,188 +884,6 @@ describe('loop', () => { }); }); -describe('return', () => { - test.concurrent('as statement', async () => { - const res = await exe(` - @f() { - return 1 - } - <: f() - `); - eq(res, NUM(1)); - assert.rejects(() => exe('return 1')); - }); - - test.concurrent('in eval', async () => { - const res = await exe(` - @f() { - let a = eval { - return 1 - 2 - } - return 3 - } - <: f() - `); - eq(res, NUM(1)); - assert.rejects(() => exe('<: eval { return 1 }')); - }); - - test.concurrent('in if', async () => { - const res = await exe(` - @f() { - let a = if true { - return 1 - 2 - } - return 3 - } - <: f() - `); - eq(res, NUM(1)); - assert.rejects(() => exe('<: if true { return 1 }')); - }); - - test.concurrent('in match', async () => { - const res = await exe(` - @f() { - let a = match 0 { - default => { - return 1 - 2 - } - } - return 3 - } - <: f() - `); - eq(res, NUM(1)); - assert.rejects(() => exe('<: match 0 { default => { return 1 } }')) - }); -}); - -describe('break', () => { - test.concurrent('as statement', async () => { - const res = await exe(` - var x = 0 - for 1 { - break - x += 1 - } - <: x - `); - eq(res, NUM(0)); - assert.rejects(() => exe('break')); - assert.rejects(() => exe('@() { break }()')); - }); - - test.concurrent('in eval', async () => { - const res = await exe(` - var x = 0 - for 1 { - let a = eval { - break - } - x += 1 - } - <: x - `); - eq(res, NUM(0)); - assert.rejects(() => exe('<: eval { break }')); - }); - - test.concurrent('in if', async () => { - const res = await exe(` - var x = 0 - for 1 { - let a = if true { - break - } - x += 1 - } - <: x - `); - eq(res, NUM(0)); - assert.rejects(() => exe('<: if true { break }')); - }); - - test.concurrent('in match', async () => { - const res = await exe(` - var x = 0 - for 1 { - let a = match 0 { - default => break - } - x += 1 - } - <: x - `); - eq(res, NUM(0)); - assert.rejects(() => exe('<: if true { break }')); - }); -}); - -describe('continue', () => { - test.concurrent('as statement', async () => { - const res = await exe(` - var x = 0 - for 1 { - continue - x += 1 - } - <: x - `); - eq(res, NUM(0)); - assert.rejects(() => exe('continue')); - assert.rejects(() => exe('@() { continue }()')); - }); - - test.concurrent('in eval', async () => { - const res = await exe(` - var x = 0 - for 1 { - let a = eval { - continue - } - x += 1 - } - <: x - `); - eq(res, NUM(0)); - assert.rejects(() => exe('<: eval { continue }')); - }); - - test.concurrent('in if', async () => { - const res = await exe(` - var x = 0 - for 1 { - let a = if true { - continue - } - x += 1 - } - <: x - `); - eq(res, NUM(0)); - assert.rejects(() => exe('<: if true { continue }')); - }); - - test.concurrent('in match', async () => { - const res = await exe(` - var x = 0 - for 1 { - let a = match 0 { - default => continue - } - x += 1 - } - <: x - `); - eq(res, NUM(0)); - assert.rejects(() => exe('<: if true { continue }')); - }); -}); - /* * Global statements */ From e4b866cb3fbf04d540b58cc86a69fa020056f7f1 Mon Sep 17 00:00:00 2001 From: takejohn Date: Tue, 5 Nov 2024 18:45:46 +0900 Subject: [PATCH 05/12] =?UTF-8?q?API=E3=83=AC=E3=83=9D=E3=83=BC=E3=83=88?= =?UTF-8?q?=E3=81=AE=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- etc/aiscript.api.md | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/etc/aiscript.api.md b/etc/aiscript.api.md index d7e9948f..fac578cf 100644 --- a/etc/aiscript.api.md +++ b/etc/aiscript.api.md @@ -312,15 +312,6 @@ 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 | Pow | Mul | Div | Rem | Add | Sub | Lt | Lteq | Gt | Gteq | Eq | Neq | And | Or | Identifier | Call | Index | Prop; -// @public (undocumented) -function extractControl(v: (Value | Control)[]): { - type: 'values'; - values: Value[]; -} | { - type: 'control'; - control: Control; -}; - // @public (undocumented) const FALSE: { type: "bool"; @@ -763,7 +754,6 @@ declare namespace values { assertValue, isValue, isControl, - extractControl, VNull, VBool, VNum, From 77f114da2aed3c754e6a1dca8d5ff5d25f2c8193 Mon Sep 17 00:00:00 2001 From: takejohn Date: Tue, 5 Nov 2024 19:30:27 +0900 Subject: [PATCH 06/12] =?UTF-8?q?break,continue=E3=81=8C=E3=83=AB=E3=83=BC?= =?UTF-8?q?=E3=83=97=E5=86=85=E3=81=AB=E8=A6=8B=E3=81=88=E3=81=A6=E3=82=82?= =?UTF-8?q?=E9=96=A2=E6=95=B0=E3=81=8C=E3=82=88=E3=82=8A=E5=86=85=E5=81=B4?= =?UTF-8?q?=E3=81=AA=E3=82=89=E6=96=87=E6=B3=95=E3=82=A8=E3=83=A9=E3=83=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plugins/validate-jump-statements.ts | 22 +++++++++++++++---- test/jump-statements.ts | 20 +++++++++++++++++ 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/src/parser/plugins/validate-jump-statements.ts b/src/parser/plugins/validate-jump-statements.ts index 81b9cdb2..d1a346e2 100644 --- a/src/parser/plugins/validate-jump-statements.ts +++ b/src/parser/plugins/validate-jump-statements.ts @@ -3,6 +3,20 @@ import { AiScriptSyntaxError } from '../../error.js'; import type * as Ast from '../../node.js'; +function isInLoopScope(ancestors: Ast.Node[]): boolean { + for (let i = ancestors.length - 1; i >= 0; i--) { + switch (ancestors[i]!.type) { + case 'loop': + case 'for': + case 'each': + return true; + case 'fn': + return false; + } + } + return false; +} + function validateNode(node: Ast.Node, ancestors: Ast.Node[]): Ast.Node { switch (node.type) { case 'return': { @@ -12,14 +26,14 @@ function validateNode(node: Ast.Node, ancestors: Ast.Node[]): Ast.Node { break; } case 'break': { - if (!ancestors.some(({ type }) => type === 'loop' || type === 'for' || type === 'each')) { - throw new AiScriptSyntaxError('break must be inside loop / for / each', node.loc.start); + if (!isInLoopScope(ancestors)) { + throw new AiScriptSyntaxError('break must be inside for / each / while / do-while / loop', node.loc.start); } break; } case 'continue': { - if (!ancestors.some(({ type }) => type === 'loop' || type === 'for' || type === 'each')) { - throw new AiScriptSyntaxError('continue must be inside loop / for / each', node.loc.start); + if (!isInLoopScope(ancestors)) { + throw new AiScriptSyntaxError('continue must be inside for / each / while / do-while / loop', node.loc.start); } break; } diff --git a/test/jump-statements.ts b/test/jump-statements.ts index 03ee4360..c7ad5d70 100644 --- a/test/jump-statements.ts +++ b/test/jump-statements.ts @@ -616,6 +616,16 @@ describe('break', () => { eq(res, NUM(0)); assert.rejects(() => exe('<: if true { break }')); }); + + test.concurrent('in function', async () => { + assert.rejects(() => exe(` + for 1 { + @f() { + break; + } + } + `)); + }); }); describe('continue', () => { @@ -677,4 +687,14 @@ describe('continue', () => { eq(res, NUM(0)); assert.rejects(() => exe('<: if true { continue }')); }); + + test.concurrent('in function', async () => { + assert.rejects(() => exe(` + for 1 { + @f() { + continue; + } + } + `)); + }); }); From 04747e4bd19bee37b6fa7ee5450dcaca3a48f72c Mon Sep 17 00:00:00 2001 From: takejohn Date: Tue, 5 Nov 2024 19:39:24 +0900 Subject: [PATCH 07/12] CHANGELOG --- unreleased/jump-statements.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 unreleased/jump-statements.md diff --git a/unreleased/jump-statements.md b/unreleased/jump-statements.md new file mode 100644 index 00000000..eb1c1233 --- /dev/null +++ b/unreleased/jump-statements.md @@ -0,0 +1,6 @@ +- **Breaking Change** return文、break文、continue文の挙動が変更されました。 + - return文は関数スコープ内でないと文法エラーになります。 + - break文およびcontinue文は反復処理文(for, each, while, do-while, loop)のスコープ内でないと文法エラーになります。 + - return文は常に関数から脱出します。 + - break文は常に最も内側の反復処理文の処理を中断し、ループから脱出します。 + - continue文は常に最も内側の反復処理文の処理を中断し、ループの先頭に戻ります。 From d67fa4d4ac9535ea370cfe4b2e3021585cc0aa61 Mon Sep 17 00:00:00 2001 From: takejohn Date: Fri, 8 Nov 2024 10:39:49 +0900 Subject: [PATCH 08/12] =?UTF-8?q?=E3=83=86=E3=83=B3=E3=83=97=E3=83=AC?= =?UTF-8?q?=E3=83=BC=E3=83=88=E3=83=AA=E3=83=86=E3=83=A9=E3=83=AB=E5=86=85?= =?UTF-8?q?return=E3=83=86=E3=82=B9=E3=83=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/jump-statements.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/jump-statements.ts b/test/jump-statements.ts index c7ad5d70..80470d9a 100644 --- a/test/jump-statements.ts +++ b/test/jump-statements.ts @@ -484,7 +484,6 @@ describe('return', () => { assert.rejects(() => exe('<: @(x = eval { return 1 }){}')); }); - /* TODO: #843が解決したらコメントアウト test.concurrent('in template', async () => { const res = await exe(` @f() { @@ -495,7 +494,6 @@ describe('return', () => { eq(res, NUM(1)); assert.rejects(() => exe('<: `{eval {return 1}}`')); }); - */ test.concurrent('in return', async () => { const res = await exe(` From 7d21c3b6a5fab5f37733c5d40486ca28acca8fa8 Mon Sep 17 00:00:00 2001 From: takejohn Date: Fri, 8 Nov 2024 14:04:32 +0900 Subject: [PATCH 09/12] =?UTF-8?q?Control=E7=B3=BB=E3=81=AE=E5=9E=8B?= =?UTF-8?q?=E3=82=84=E9=96=A2=E6=95=B0=E3=81=AE=E7=A7=98=E5=8C=BF=E3=81=A8?= =?UTF-8?q?=E5=90=8D=E5=89=8D=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- etc/aiscript.api.md | 57 +-------------------- src/interpreter/control.ts | 101 +++++++++++++++++++++++++++++++++++++ src/interpreter/index.ts | 5 +- src/interpreter/value.ts | 100 ------------------------------------ 4 files changed, 106 insertions(+), 157 deletions(-) create mode 100644 src/interpreter/control.ts diff --git a/etc/aiscript.api.md b/etc/aiscript.api.md index fac578cf..4fab1d95 100644 --- a/etc/aiscript.api.md +++ b/etc/aiscript.api.md @@ -121,9 +121,6 @@ function assertObject(val: Value | null | undefined): asserts val is VObj; // @public (undocumented) function assertString(val: Value | null | undefined): asserts val is VStr; -// @public (undocumented) -function assertValue(v: Value | Control): asserts v is Value; - // @public (undocumented) type Assign = NodeBase & { type: 'assign'; @@ -221,9 +218,6 @@ type Bool = NodeBase & { value: boolean; }; -// @public (undocumented) -const BREAK: () => VBreak; - // @public (undocumented) type Break = NodeBase & { type: 'break'; @@ -236,17 +230,11 @@ type Call = NodeBase & { args: Expression[]; }; -// @public (undocumented) -const CONTINUE: () => VContinue; - // @public (undocumented) type Continue = NodeBase & { type: 'continue'; }; -// @public (undocumented) -type Control = VReturn | VBreak | VContinue; - // @public (undocumented) type Definition = NodeBase & { type: 'def'; @@ -432,9 +420,6 @@ function isArray(val: Value): val is VArr; // @public (undocumented) function isBoolean(val: Value): val is VBool; -// @public (undocumented) -function isControl(v: Value | Control): v is Control; - // @public (undocumented) function isExpression(x: Node_2): x is Expression; @@ -453,9 +438,6 @@ function isStatement(x: Node_2): x is Statement; // @public (undocumented) function isString(val: Value): val is VStr; -// @public (undocumented) -function isValue(v: Value | Control): v is Value; - // @public (undocumented) function jsToVal(val: unknown): Value; @@ -634,9 +616,6 @@ type Rem = NodeBase & { // @public (undocumented) function reprValue(value: Value, literalLike?: boolean, processedObjects?: Set): string; -// @public (undocumented) -const RETURN: (v: VReturn["value"]) => VReturn; - // @public (undocumented) type Return = NodeBase & { type: 'return'; @@ -710,9 +689,6 @@ const TRUE: { // @public (undocumented) type TypeSource = NamedTypeSource | FnTypeSource; -// @public (undocumented) -function unWrapRet(v: Value | Control): Value; - declare namespace utils { export { expectAny, @@ -750,10 +726,6 @@ type Value = (VNull | VBool | VNum | VStr | VArr | VObj | VFn | VError) & Attr_2 declare namespace values { export { - unWrapRet, - assertValue, - isValue, - isControl, VNull, VBool, VNum, @@ -764,13 +736,9 @@ declare namespace values { VUserFn, VFnParam, VNativeFn, - VReturn, - VBreak, - VContinue, VError, Attr_2 as Attr, Value, - Control, NULL, TRUE, FALSE, @@ -781,9 +749,6 @@ declare namespace values { ARR, FN, FN_NATIVE, - RETURN, - BREAK, - CONTINUE, ERROR } } @@ -801,18 +766,6 @@ type VBool = { value: boolean; }; -// @public (undocumented) -type VBreak = { - type: 'break'; - value: null; -}; - -// @public (undocumented) -type VContinue = { - type: 'continue'; - value: null; -}; - // @public (undocumented) type VError = { type: 'error'; @@ -857,12 +810,6 @@ type VObj = { value: Map; }; -// @public (undocumented) -type VReturn = { - type: 'return'; - value: Value; -}; - // @public (undocumented) type VStr = { type: 'str'; @@ -882,8 +829,8 @@ type VUserFn = VFnBase & { // Warnings were encountered during analysis: // -// src/interpreter/index.ts:43:4 - (ae-forgotten-export) The symbol "LogObject" needs to be exported by the entry point index.d.ts -// src/interpreter/value.ts:48:2 - (ae-forgotten-export) The symbol "Type" needs to be exported by the entry point index.d.ts +// src/interpreter/index.ts:44:4 - (ae-forgotten-export) The symbol "LogObject" needs to be exported by the entry point index.d.ts +// 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/control.ts b/src/interpreter/control.ts new file mode 100644 index 00000000..9ecfbf06 --- /dev/null +++ b/src/interpreter/control.ts @@ -0,0 +1,101 @@ +import { AiScriptRuntimeError } from '../error.js'; +import type { Value } from './value.js'; + +export type CReturn = { + type: 'return'; + value: Value; +}; + +export type CBreak = { + type: 'break'; + value: null; +}; + +export type CContinue = { + type: 'continue'; + value: null; +}; + +export type Control = CReturn | CBreak | CContinue; + +// Return文で値が返されたことを示すためのラッパー +export const RETURN = (v: CReturn['value']): CReturn => ({ + type: 'return' as const, + value: v, +}); + +export const BREAK = (): CBreak => ({ + type: 'break' as const, + value: null, +}); + +export const CONTINUE = (): CContinue => ({ + type: 'continue' as const, + value: null, +}); + +export function unWrapRet(v: Value | Control): Value { + switch (v.type) { + case 'return': + return v.value; + default: { + assertValue(v); + return v; + } + } +} + +export function assertValue(v: Value | Control): asserts v is Value { + switch (v.type) { + case 'return': + throw new AiScriptRuntimeError('Invalid return'); + case 'break': + throw new AiScriptRuntimeError('Invalid break'); + case 'continue': + throw new AiScriptRuntimeError('Invalid continue'); + default: + v satisfies Value; + } +} + +export function isValue(v: Value | Control): v is Value { + switch (v.type) { + case 'null': + case 'bool': + case 'num': + case 'str': + case 'arr': + case 'obj': + case 'fn': + case 'error': + return true; + case 'return': + case 'break': + case 'continue': + return false; + } + // exhaustive check + v satisfies never; + throw new TypeError('expected value or control'); +} + +export function isControl(v: Value | Control): v is Control { + switch (v.type) { + case 'null': + case 'bool': + case 'num': + case 'str': + case 'arr': + case 'obj': + case 'fn': + case 'error': + return false; + case 'return': + case 'break': + case 'continue': + return true; + } + // exhaustive check + v satisfies never; + throw new TypeError('expected value or control'); +} diff --git a/src/interpreter/index.ts b/src/interpreter/index.ts index 74535082..d7055610 100644 --- a/src/interpreter/index.ts +++ b/src/interpreter/index.ts @@ -7,12 +7,13 @@ import { AiScriptError, NonAiScriptError, AiScriptNamespaceError, AiScriptIndexO 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 { assertNumber, assertString, assertFunction, assertBoolean, assertObject, assertArray, eq, isObject, isArray, expectAny, reprValue, isFunction } from './util.js'; -import { NULL, RETURN, unWrapRet, FN_NATIVE, BOOL, NUM, STR, ARR, OBJ, FN, BREAK, CONTINUE, ERROR, assertValue, isControl } from './value.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 type { JsValue } from './util.js'; -import type { Control, Value, VFn, VUserFn } from './value.js'; +import type { Value, VFn, VUserFn } from './value.js'; export type LogObject = { scope?: string; diff --git a/src/interpreter/value.ts b/src/interpreter/value.ts index 3b1e5492..c10dfb9b 100644 --- a/src/interpreter/value.ts +++ b/src/interpreter/value.ts @@ -1,4 +1,3 @@ -import { AiScriptRuntimeError } from '../error.js'; import type { Expression, Node } from '../node.js'; import type { Type } from '../type.js'; import type { Scope } from './scope.js'; @@ -60,21 +59,6 @@ export type VNativeFn = VFnBase & { }) => Value | Promise | void; }; -export type VReturn = { - type: 'return'; - value: Value; -}; - -export type VBreak = { - type: 'break'; - value: null; -}; - -export type VContinue = { - type: 'continue'; - value: null; -}; - export type VError = { type: 'error'; value: string; @@ -90,8 +74,6 @@ export type Attr = { export type Value = (VNull | VBool | VNum | VStr | VArr | VObj | VFn | VError) & Attr; -export type Control = VReturn | VBreak | VContinue; - export const NULL = { type: 'null' as const, }; @@ -143,90 +125,8 @@ export const FN_NATIVE = (fn: VNativeFn['native']): VNativeFn => ({ native: fn, }); -// Return文で値が返されたことを示すためのラッパー -export const RETURN = (v: VReturn['value']): VReturn => ({ - type: 'return' as const, - value: v, -}); - -export const BREAK = (): VBreak => ({ - type: 'break' as const, - value: null, -}); - -export const CONTINUE = (): VContinue => ({ - type: 'continue' as const, - value: null, -}); - export const ERROR = (name: string, info?: Value): Value => ({ type: 'error' as const, value: name, info: info, }); - -export function unWrapRet(v: Value | Control): Value { - switch (v.type) { - case 'return': - return v.value; - default: { - assertValue(v); - return v; - } - } -} - -export function assertValue(v: Value | Control): asserts v is Value { - switch (v.type) { - case 'return': - throw new AiScriptRuntimeError('Invalid return'); - case 'break': - throw new AiScriptRuntimeError('Invalid break'); - case 'continue': - throw new AiScriptRuntimeError('Invalid continue'); - default: - v satisfies Value; - } -} - -export function isValue(v: Value | Control): v is Value { - switch (v.type) { - case 'null': - case 'bool': - case 'num': - case 'str': - case 'arr': - case 'obj': - case 'fn': - case 'error': - return true; - case 'return': - case 'break': - case 'continue': - return false; - } - // exhaustive check - v satisfies never; - throw new TypeError('expected value or control'); -} - -export function isControl(v: Value | Control): v is Control { - switch (v.type) { - case 'null': - case 'bool': - case 'num': - case 'str': - case 'arr': - case 'obj': - case 'fn': - case 'error': - return false; - case 'return': - case 'break': - case 'continue': - return true; - } - // exhaustive check - v satisfies never; - throw new TypeError('expected value or control'); -} From 738f0f9d06a5c5069cd3f4a2cb7805ca2982f4e1 Mon Sep 17 00:00:00 2001 From: takejohn Date: Sat, 9 Nov 2024 17:20:43 +0900 Subject: [PATCH 10/12] =?UTF-8?q?=E4=BD=BF=E7=94=A8=E3=81=97=E3=81=A6?= =?UTF-8?q?=E3=81=84=E3=81=AA=E3=81=84=E9=96=A2=E6=95=B0=E3=82=92=E5=89=8A?= =?UTF-8?q?=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/interpreter/control.ts | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/src/interpreter/control.ts b/src/interpreter/control.ts index 394bd35b..e748344e 100644 --- a/src/interpreter/control.ts +++ b/src/interpreter/control.ts @@ -59,27 +59,6 @@ export function assertValue(v: Value | Control): asserts v is Value { } } -export function isValue(v: Value | Control): v is Value { - switch (v.type) { - case 'null': - case 'bool': - case 'num': - case 'str': - case 'arr': - case 'obj': - case 'fn': - case 'error': - return true; - case 'return': - case 'break': - case 'continue': - return false; - } - // exhaustive check - v satisfies never; - throw new TypeError('expected value or control'); -} - export function isControl(v: Value | Control | Reference): v is Control { switch (v.type) { case 'null': From cdb06324e54e05da6864b2e4f6913d28058049f1 Mon Sep 17 00:00:00 2001 From: takejohn Date: Sat, 9 Nov 2024 17:25:23 +0900 Subject: [PATCH 11/12] =?UTF-8?q?API=E3=83=AC=E3=83=9D=E3=83=BC=E3=83=88?= =?UTF-8?q?=E3=81=AE=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- etc/aiscript.api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/etc/aiscript.api.md b/etc/aiscript.api.md index 4fab1d95..55529275 100644 --- a/etc/aiscript.api.md +++ b/etc/aiscript.api.md @@ -829,7 +829,7 @@ type VUserFn = VFnBase & { // Warnings were encountered during analysis: // -// src/interpreter/index.ts:44:4 - (ae-forgotten-export) The symbol "LogObject" needs to be exported by the entry point index.d.ts +// src/interpreter/index.ts:45:4 - (ae-forgotten-export) The symbol "LogObject" needs to be exported by the entry point index.d.ts // 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) From b458c6c1bc288d130ff1ca4a43b8865336a867e8 Mon Sep 17 00:00:00 2001 From: takejohn Date: Sun, 10 Nov 2024 18:45:25 +0900 Subject: [PATCH 12/12] =?UTF-8?q?API=E3=83=AC=E3=83=9D=E3=83=BC=E3=83=88?= =?UTF-8?q?=E3=81=AE=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- etc/aiscript.api.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/etc/aiscript.api.md b/etc/aiscript.api.md index 55529275..95be0bb1 100644 --- a/etc/aiscript.api.md +++ b/etc/aiscript.api.md @@ -405,13 +405,25 @@ export class Interpreter { execFn(fn: VFn, args: Value[]): Promise; execFnSimple(fn: VFn, args: Value[]): Promise; // (undocumented) + pause(): void; + // (undocumented) registerAbortHandler(handler: () => void): void; // (undocumented) + registerPauseHandler(handler: () => void): void; + // (undocumented) + registerUnpauseHandler(handler: () => void): void; + // (undocumented) scope: Scope; // (undocumented) stepCount: number; // (undocumented) + unpause(): void; + // (undocumented) unregisterAbortHandler(handler: () => void): void; + // (undocumented) + unregisterPauseHandler(handler: () => void): void; + // (undocumented) + unregisterUnpauseHandler(handler: () => void): void; } // @public (undocumented) @@ -789,7 +801,11 @@ type VNativeFn = VFnBase & { call: (fn: VFn, args: Value[]) => Promise; topCall: (fn: VFn, args: Value[]) => Promise; registerAbortHandler: (handler: () => void) => void; + registerPauseHandler: (handler: () => void) => void; + registerUnpauseHandler: (handler: () => void) => void; unregisterAbortHandler: (handler: () => void) => void; + unregisterPauseHandler: (handler: () => void) => void; + unregisterUnpauseHandler: (handler: () => void) => void; }) => Value | Promise | void; }; @@ -829,7 +845,7 @@ type VUserFn = VFnBase & { // Warnings were encountered during analysis: // -// src/interpreter/index.ts:45:4 - (ae-forgotten-export) The symbol "LogObject" needs to be exported by the entry point index.d.ts +// src/interpreter/index.ts:48:4 - (ae-forgotten-export) The symbol "LogObject" needs to be exported by the entry point index.d.ts // 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)