diff --git a/docs/syntax.md b/docs/syntax.md index 9962bb74..4d491f1d 100644 --- a/docs/syntax.md +++ b/docs/syntax.md @@ -71,7 +71,29 @@ let add2 = @(x, y) { ) { x + y } -@add5(x,y){x+y} // ワンライナー +// 省略可能引数 +@func1(a, b?) { + <: a + <: b // 省略されるとnullになる +} +func1('hoge') // 'hoge' null +// 初期値を設定された引数(省略可能引数と組み合わせて使用可能) +@func2(a, b?, c = 'piyo', d?) { + <: a + <: b + <: c + <: d +} +func2('hoge', 'fuga') // 'hoge' 'fuga' 'piyo' null +// 初期値には変数を使用可能(値は宣言時点で固定) +var v = 'hoge' +@func3(a = v) { + <: a +} +v = 'fuga' +func3() // 'hoge' +// ワンライナー +@func4(a,b?,c=1){<:a;<:b;<:c} ``` ```js // match等の予約語は関数名として使用できない @@ -87,6 +109,8 @@ var func = null @func() { // Runtime Error 'hoge' } +// 省略可能引数構文と初期値構文は併用できない +@func(a? = 1) {} // Syntax Error ``` ### 代入 diff --git a/etc/aiscript.api.md b/etc/aiscript.api.md index caf7f7e2..9423f773 100644 --- a/etc/aiscript.api.md +++ b/etc/aiscript.api.md @@ -279,6 +279,8 @@ type Fn = NodeBase & { type: 'fn'; args: { name: string; + optional: boolean; + default?: Expression; argType?: TypeSource; }[]; retType?: TypeSource; @@ -625,6 +627,7 @@ declare namespace values { VObj, VFn, VUserFn, + VFnArg, VNativeFn, VReturn, VBreak, @@ -685,6 +688,13 @@ type VError = { // @public (undocumented) type VFn = VUserFn | VNativeFn; +// @public (undocumented) +type VFnArg = { + name: string; + type?: Type; + default?: Value; +}; + // @public type VNativeFn = VFnBase & { native: (args: (Value | undefined)[], opts: { @@ -729,10 +739,15 @@ type VStr = { // @public (undocumented) type VUserFn = VFnBase & { native?: undefined; + args: VFnArg[]; statements: Node_2[]; scope: Scope; }; +// Warnings were encountered during analysis: +// +// src/interpreter/value.ts:46:2 - (ae-forgotten-export) The symbol "Type" needs to be exported by the entry point index.d.ts + // (No @packageDocumentation comment for this package) ``` diff --git a/src/interpreter/index.ts b/src/interpreter/index.ts index 3401b6d5..750c108c 100644 --- a/src/interpreter/index.ts +++ b/src/interpreter/index.ts @@ -231,10 +231,12 @@ export class Interpreter { return result ?? NULL; } else { const _args = new Map(); - for (let i = 0; i < (fn.args ?? []).length; i++) { - _args.set(fn.args![i]!, { + for (const i of fn.args.keys()) { + const argdef = fn.args[i]!; + if (!argdef.default) expectAny(args[i]); + _args.set(argdef.name, { isMutable: true, - value: args[i] ?? NULL, + value: args[i] ?? argdef.default!, }); } const fnScope = fn.scope!.createChildScope(_args); @@ -487,7 +489,17 @@ export class Interpreter { } case 'fn': { - return FN(node.args.map(arg => arg.name), node.children, scope); + return FN( + await Promise.all(node.args.map(async (arg) => { + return { + name: arg.name, + default: arg.default ? await this._eval(arg.default, scope) : arg.optional ? NULL : undefined, + // type: (TODO) + }; + })), + node.children, + scope, + ); } case 'block': { diff --git a/src/interpreter/util.ts b/src/interpreter/util.ts index f3397dfc..f2ba2ce6 100644 --- a/src/interpreter/util.ts +++ b/src/interpreter/util.ts @@ -189,7 +189,12 @@ export function reprValue(value: Value, literalLike = false, processedObjects = if (value.type === 'bool') return value.value.toString(); if (value.type === 'null') return 'null'; if (value.type === 'fn') { - return `@( ${(value.args ?? []).join(', ')} ) { ... }`; + if (value.native) { + // そのうちネイティブ関数の引数も表示できるようにしたいが、ホスト向けの破壊的変更を伴うと思われる + return '@( ?? ) { native code }'; + } else { + return `@( ${(value.args.map(v => v.name)).join(', ')} ) { ... }`; + } } return '?'; diff --git a/src/interpreter/value.ts b/src/interpreter/value.ts index 90c531b0..3250afd7 100644 --- a/src/interpreter/value.ts +++ b/src/interpreter/value.ts @@ -1,4 +1,5 @@ import type { Node } from '../node.js'; +import type { Type } from '../type.js'; import type { Scope } from './scope.js'; export type VNull = { @@ -33,13 +34,18 @@ export type VObj = { export type VFn = VUserFn | VNativeFn; type VFnBase = { type: 'fn'; - args?: string[]; }; export type VUserFn = VFnBase & { native?: undefined; // if (vfn.native) で型アサーション出来るように + args: VFnArg[]; statements: Node[]; scope: Scope; }; +export type VFnArg = { + name: string; + type?: Type; + default?: Value; +} /** * When your AiScript NATIVE function passes VFn.call to other caller(s) whose error thrown outside the scope, use VFn.topCall instead to keep it under AiScript error control system. */ diff --git a/src/node.ts b/src/node.ts index 03d737d2..43b556fd 100644 --- a/src/node.ts +++ b/src/node.ts @@ -177,6 +177,8 @@ export type Fn = NodeBase & { type: 'fn'; // 関数 args: { name: string; // 引数名 + optional: boolean; + default?: Expression; // 引数の初期値 argType?: TypeSource; // 引数の型 }[]; retType?: TypeSource; // 戻り値の型 diff --git a/src/parser/scanner.ts b/src/parser/scanner.ts index 1bba9fa7..fb2d4c17 100644 --- a/src/parser/scanner.ts +++ b/src/parser/scanner.ts @@ -268,6 +268,11 @@ export class Scanner implements ITokenStream { } break; } + case '?': { + this.stream.next(); + token = TOKEN(TokenKind.Question, loc, { hasLeftSpacing }); + break; + } case '@': { this.stream.next(); token = TOKEN(TokenKind.At, loc, { hasLeftSpacing }); diff --git a/src/parser/syntaxes/common.ts b/src/parser/syntaxes/common.ts index 9f32c970..f185a158 100644 --- a/src/parser/syntaxes/common.ts +++ b/src/parser/syntaxes/common.ts @@ -2,6 +2,7 @@ import { TokenKind } from '../token.js'; import { AiScriptSyntaxError } from '../../error.js'; import { NODE } from '../utils.js'; import { parseStatement } from './statements.js'; +import { parseExpr } from './expressions.js'; import type { ITokenStream } from '../streams/token-stream.js'; import type * as Ast from '../../node.js'; @@ -12,7 +13,7 @@ import type * as Ast from '../../node.js'; * ``` */ export function parseParams(s: ITokenStream): { name: string, argType?: Ast.Node }[] { - const items: { name: string, argType?: Ast.Node }[] = []; + const items: { name: string, optional?: boolean, default?: Ast.Node, argType?: Ast.Node }[] = []; s.nextWith(TokenKind.OpenParen); @@ -25,13 +26,22 @@ export function parseParams(s: ITokenStream): { name: string, argType?: Ast.Node const name = s.token.value!; s.next(); + let optional = false; + let defaultExpr; + if ((s.getKind() as TokenKind) === TokenKind.Question) { + s.next(); + optional = true; + } else if ((s.getKind() as TokenKind) === TokenKind.Eq) { + s.next(); + defaultExpr = parseExpr(s, false); + } let type; if (s.getKind() === TokenKind.Colon) { s.next(); type = parseType(s); } - items.push({ name, argType: type }); + items.push({ name, optional, default: defaultExpr, argType: type }); // separator switch (s.getKind()) { diff --git a/src/parser/token.ts b/src/parser/token.ts index c07030c7..99af4099 100644 --- a/src/parser/token.ts +++ b/src/parser/token.ts @@ -89,6 +89,8 @@ export enum TokenKind { Gt, /** ">=" */ GtEq, + /** "?" */ + Question, /** "@" */ At, /** "[" */ diff --git a/src/parser/visit.ts b/src/parser/visit.ts index 29b5cbdf..17d11f0f 100644 --- a/src/parser/visit.ts +++ b/src/parser/visit.ts @@ -61,6 +61,11 @@ export function visitNode(node: Ast.Node, fn: (node: Ast.Node) => Ast.Node): Ast break; } case 'fn': { + for (const i of result.args.keys()) { + if (result.args[i]!.default) { + result.args[i]!.default = visitNode(result.args[i]!.default!, fn) as Ast.Fn['args'][number]['default']; + } + } for (let i = 0; i < result.children.length; i++) { result.children[i] = visitNode(result.children[i]!, fn) as Ast.Fn['children'][number]; } diff --git a/test/index.ts b/test/index.ts index 22baee6d..9cb52495 100644 --- a/test/index.ts +++ b/test/index.ts @@ -591,10 +591,43 @@ describe('Function call', () => { eq(res, NUM(2)); }); - test.concurrent('std: throw AiScript error when required arg missing', async () => { + test.concurrent('optional args', async () => { + const res = await exe(` + @f(x, y?, z?) { + [x, y, z] + } + <: f(true) + `); + eq(res, ARR([TRUE, NULL, NULL])); + }); + + test.concurrent('args with default value', async () => { + const res = await exe(` + @f(x, y=1, z=2) { + [x, y, z] + } + <: f(5, 3) + `); + eq(res, ARR([NUM(5), NUM(3), NUM(2)])); + }); + + test.concurrent('args must not be both optional and default-valued', async () => { + try { + Parser.parse(` + @func(a? = 1){} + `); + } catch (e) { + assert.ok(e instanceof AiScriptSyntaxError); + return; + } + assert.fail(); + }); + + test.concurrent('missing arg', async () => { try { await exe(` - <: Core:eq(1) + @func(a){} + func() `); } catch (e) { assert.ok(e instanceof AiScriptRuntimeError); @@ -603,14 +636,16 @@ describe('Function call', () => { assert.fail(); }); - test.concurrent('omitted args', async () => { - const res = await exe(` - @f(x, y) { - [x, y] + test.concurrent('std: throw AiScript error when required arg missing', async () => { + try { + await exe(` + <: Core:eq(1) + `); + } catch (e) { + assert.ok(e instanceof AiScriptRuntimeError); + return; } - <: f(1) - `); - eq(res, ARR([NUM(1), NULL])); + assert.fail(); }); }); diff --git a/unreleased/optional-args.md b/unreleased/optional-args.md new file mode 100644 index 00000000..99ffc49d --- /dev/null +++ b/unreleased/optional-args.md @@ -0,0 +1,2 @@ +- 省略可能引数と初期値付き引数を追加。引数名に`?`を後置することでその引数は省略可能となります。引数に`=<式>`を後置すると引数に初期値を設定できます。省略可能引数は初期値`null`の引数と同等です。 + - BREAKING: いずれでもない引数が省略されると即時エラーとなるようになりました。