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
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion docs/syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -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等の予約語は関数名として使用できない
Expand All @@ -87,6 +109,8 @@ var func = null
@func() { // Runtime Error
'hoge'
}
// 省略可能引数構文と初期値構文は併用できない
@func(a? = 1) {} // Syntax Error
```

### 代入
Expand Down
15 changes: 15 additions & 0 deletions etc/aiscript.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,8 @@ type Fn = NodeBase & {
type: 'fn';
args: {
name: string;
optional: boolean;
default?: Expression;
argType?: TypeSource;
}[];
retType?: TypeSource;
Expand Down Expand Up @@ -625,6 +627,7 @@ declare namespace values {
VObj,
VFn,
VUserFn,
VFnArg,
VNativeFn,
VReturn,
VBreak,
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -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
Expand Up @@ -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

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
maxStep?: number;
} = {},
) {
Expand Down Expand Up @@ -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

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type

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

View workflow job for this annotation

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

View workflow job for this annotation

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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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

View workflow job for this annotation

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);
Expand Down Expand Up @@ -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

View workflow job for this annotation

GitHub Actions / lint

Unnecessary conditional, value is always truthy
const v = await this._run(node.statements, scope.createChildScope());
if (v.type === 'break') {
break;
Expand Down Expand Up @@ -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': {
Expand Down Expand Up @@ -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

View workflow job for this annotation

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

View workflow job for this annotation

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.');
Expand Down
7 changes: 6 additions & 1 deletion src/interpreter/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 '?';
Expand Down
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 = {
Expand Down Expand Up @@ -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.
*/
Expand Down
2 changes: 2 additions & 0 deletions src/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,8 @@ export type Fn = NodeBase & {
type: 'fn'; // 関数
args: {
name: string; // 引数名
optional: boolean;
default?: Expression; // 引数の初期値
argType?: TypeSource; // 引数の型
}[];
retType?: TypeSource; // 戻り値の型
Expand Down
5 changes: 5 additions & 0 deletions src/parser/scanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down
14 changes: 12 additions & 2 deletions src/parser/syntaxes/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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);

Expand All @@ -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()) {
Expand Down
2 changes: 2 additions & 0 deletions src/parser/token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ export enum TokenKind {
Gt,
/** ">=" */
GtEq,
/** "?" */
Question,
/** "@" */
At,
/** "[" */
Expand Down
5 changes: 5 additions & 0 deletions src/parser/visit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];
}
Expand Down
53 changes: 44 additions & 9 deletions test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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();
});
});

Expand Down
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: いずれでもない引数が省略されると即時エラーとなるようになりました。
Loading