Skip to content

Next: 省略可能引数と初期値付き引数を追加 #475

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Jun 8, 2024
Merged
26 changes: 25 additions & 1 deletion docs/syntax.md
Original file line number Diff line number Diff line change
@@ -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
```
### 代入
15 changes: 15 additions & 0 deletions etc/aiscript.api.md
Original file line number Diff line number Diff line change
@@ -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)

```
20 changes: 16 additions & 4 deletions src/interpreter/index.ts
Original file line number Diff line number Diff line change
@@ -29,7 +29,7 @@
in?(q: string): Promise<string>;
out?(value: Value): void;
err?(e: AiScriptError): void;
log?(type: string, params: Record<string, any>): void;

Check warning on line 32 in src/interpreter/index.ts

GitHub Actions / lint

Unexpected any. Specify a different type
maxStep?: number;
} = {},
) {
@@ -104,10 +104,10 @@
}

@autobind
public static collectMetadata(script?: Ast.Node[]): Map<any, any> | undefined {

Check warning on line 107 in src/interpreter/index.ts

GitHub Actions / lint

Unexpected any. Specify a different type

Check warning on line 107 in src/interpreter/index.ts

GitHub Actions / lint

Unexpected any. Specify a different type
if (script == null || script.length === 0) return;

function nodeToJs(node: Ast.Node): any {

Check warning on line 110 in src/interpreter/index.ts

GitHub Actions / lint

Unexpected any. Specify a different type
switch (node.type) {
case 'arr': return node.value.map(item => nodeToJs(item));
case 'bool': return node.value;
@@ -231,10 +231,12 @@
return result ?? NULL;
} else {
const _args = new Map<string, Variable>();
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);
@@ -278,7 +280,7 @@
if (cond.value) {
return this._eval(node.then, scope);
} else {
if (node.elseif && node.elseif.length > 0) {

Check warning on line 283 in src/interpreter/index.ts

GitHub Actions / lint

Unnecessary conditional, value is always truthy
for (const elseif of node.elseif) {
const cond = await this._eval(elseif.cond, scope);
assertBoolean(cond);
@@ -312,7 +314,7 @@

case 'loop': {
// eslint-disable-next-line no-constant-condition
while (true) {

Check warning on line 317 in src/interpreter/index.ts

GitHub Actions / lint

Unnecessary conditional, value is always truthy
const v = await this._run(node.statements, scope.createChildScope());
if (v.type === 'break') {
break;
@@ -487,7 +489,17 @@
}

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': {
@@ -639,12 +651,12 @@
} else if (dest.type === 'arr') {
assertArray(value);
await Promise.all(dest.value.map(
(item, index) => this.assign(scope, item, value.value[index] ?? NULL)

Check warning on line 654 in src/interpreter/index.ts

GitHub Actions / lint

Missing trailing comma
));
} else if (dest.type === 'obj') {
assertObject(value);
await Promise.all([...dest.value].map(
([key, item]) => this.assign(scope, item, value.value.get(key) ?? NULL)

Check warning on line 659 in src/interpreter/index.ts

GitHub Actions / lint

Missing trailing comma
));
} else {
throw new AiScriptRuntimeError('The left-hand side of an assignment expression must be a variable or a property/index access.');
7 changes: 6 additions & 1 deletion src/interpreter/util.ts
Original file line number Diff line number Diff line change
@@ -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 '?';
8 changes: 7 additions & 1 deletion src/interpreter/value.ts
Original file line number Diff line number Diff line change
@@ -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.
*/
2 changes: 2 additions & 0 deletions src/node.ts
Original file line number Diff line number Diff line change
@@ -177,6 +177,8 @@ export type Fn = NodeBase & {
type: 'fn'; // 関数
args: {
name: string; // 引数名
optional: boolean;
default?: Expression; // 引数の初期値
argType?: TypeSource; // 引数の型
}[];
retType?: TypeSource; // 戻り値の型
5 changes: 5 additions & 0 deletions src/parser/scanner.ts
Original file line number Diff line number Diff line change
@@ -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 });
14 changes: 12 additions & 2 deletions src/parser/syntaxes/common.ts
Original file line number Diff line number Diff line change
@@ -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()) {
2 changes: 2 additions & 0 deletions src/parser/token.ts
Original file line number Diff line number Diff line change
@@ -89,6 +89,8 @@ export enum TokenKind {
Gt,
/** ">=" */
GtEq,
/** "?" */
Question,
/** "@" */
At,
/** "[" */
5 changes: 5 additions & 0 deletions src/parser/visit.ts
Original file line number Diff line number Diff line change
@@ -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'];
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

この処理のiはindexではないと思います

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for-inなのでindexで間違いないと思いますが…

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for-inなので対象がオブジェクトだと勘違いしてました..
配列にもfor-inって使えるんですね
一般的な用法なんでしょうか

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

一般的かどうかはわからないです

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for-ofの方が推奨されているようです

for(let value of array)
for(let index of array.keys())
for(let [index, value] of array.entries())

Copy link
Member Author

@FineArchs FineArchs Apr 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for...inはプロトタイプから継承したものも列挙してしまうそうですね。
for(let index of array.keys())を使った形に修正しました。

for (let i = 0; i < result.children.length; i++) {
result.children[i] = visitNode(result.children[i]!, fn) as Ast.Fn['children'][number];
}
53 changes: 44 additions & 9 deletions test/index.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});

2 changes: 2 additions & 0 deletions unreleased/optional-args.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- 省略可能引数と初期値付き引数を追加。引数名に`?`を後置することでその引数は省略可能となります。引数に`=<式>`を後置すると引数に初期値を設定できます。省略可能引数は初期値`null`の引数と同等です。
- BREAKING: いずれでもない引数が省略されると即時エラーとなるようになりました。

Unchanged files with check annotations Beta

this.info = info;
// Maintains proper stack trace for where our error was thrown (only available on V8)
if (Error.captureStackTrace) {

Check warning on line 16 in src/error.ts

GitHub Actions / lint

Unnecessary conditional, value is always truthy
Error.captureStackTrace(this, AiScriptError);
}
}
split: (target: VStr): VFn => FN_NATIVE(async ([splitter], _opts) => {
if (splitter) assertString(splitter);
if (splitter) {
return ARR(target.value.split(splitter ? splitter.value : '').map(s => STR(s)));

Check warning on line 93 in src/interpreter/primitive-props.ts

GitHub Actions / lint

Unnecessary conditional, value is always truthy
} else {
return ARR(toArray(target.value).map(s => STR(s)));
}