Skip to content
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

enhance: 変数宣言で分割代入に対応 #763

Merged
merged 10 commits into from
Aug 27, 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
14 changes: 14 additions & 0 deletions docs/syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,20 @@ let ai_kun = {
<: b // 'kakkoii'
```
```js
// 宣言で分割代入を使うことも可能
let [hoge, fuga] = ['hoge', 'fuga']

// 再宣言を含む宣言は不可
var a = 0
let [a, b] = [1, 'piyo'] // Runtime Error

// 名前空間での宣言では使用不可
:: Ai {
// Runtime Error
let [chan, kun] = ['kawaii', 'kakkoii']
}
```
```js
// 代入値が分割できる型でなければエラー
[a, b] = 1 // Runtime Error
{ zero: a, one: b } = ['hoge', 'fuga'] // Runtime Error
Expand Down
2 changes: 1 addition & 1 deletion etc/aiscript.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ type Continue = NodeBase & {
// @public (undocumented)
type Definition = NodeBase & {
type: 'def';
name: string;
dest: Expression;
varType?: TypeSource;
expr: Expression;
mut: boolean;
Expand Down
122 changes: 79 additions & 43 deletions src/interpreter/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,15 +197,15 @@
for (const node of ns.members) {
switch (node.type) {
case 'def': {
if (node.dest.type !== 'identifier') {
throw new AiScriptNamespaceError('Destructuring assignment is invalid in namespace declarations.', node.loc.start);
}
if (node.mut) {
throw new AiScriptNamespaceError('No "var" in namespace declaration: ' + node.name, node.loc.start);
throw new AiScriptNamespaceError('No "var" in namespace declaration: ' + node.dest.name, node.loc.start);
}

const variable: Variable = {
isMutable: node.mut,
value: await this._eval(node.expr, nsScope),
};
nsScope.add(node.name, variable);
const value = await this._eval(node.expr, nsScope);
this.define(nsScope, node.dest, value, node.mut);

break;
}
Expand Down Expand Up @@ -286,7 +286,7 @@
if (cond.value) {
return this._eval(node.then, scope);
} else {
if (node.elseif && node.elseif.length > 0) {

Check warning on line 289 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 @@ -320,7 +320,7 @@

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

Check warning on line 323 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 @@ -396,10 +396,7 @@
}
value.attr = attrs;
}
scope.add(node.name, {
isMutable: node.mut,
value: value,
});
this.define(scope, node.dest, value, node.mut);
return NULL;
}

Expand Down Expand Up @@ -726,42 +723,81 @@
this.abortHandlers = [];
}

@autobind
private async define(scope: Scope, dest: Ast.Expression, value: Value, isMutable: boolean): Promise<void> {
switch (dest.type) {
case 'identifier': {
scope.add(dest.name, { isMutable, value });
break;
}
case 'arr': {
assertArray(value);
await Promise.all(dest.value.map(
(item, index) => this.define(scope, item, value.value[index] ?? NULL, isMutable),
));
break;
}
case 'obj': {
assertObject(value);
await Promise.all([...dest.value].map(
([key, item]) => this.define(scope, item, value.value.get(key) ?? NULL, isMutable),
));
break;
}
default: {
throw new AiScriptRuntimeError('The left-hand side of an definition expression must be a variable.');
}
}
}

@autobind
private async assign(scope: Scope, dest: Ast.Expression, value: Value): Promise<void> {
if (dest.type === 'identifier') {
scope.assign(dest.name, value);
} else if (dest.type === 'index') {
const assignee = await this._eval(dest.target, scope);
const i = await this._eval(dest.index, scope);
if (isArray(assignee)) {
assertNumber(i);
if (assignee.value[i.value] === undefined) {
throw new AiScriptIndexOutOfRangeError(`Index out of range. index: ${i.value} max: ${assignee.value.length - 1}`);
switch (dest.type) {
case 'identifier': {
scope.assign(dest.name, value);
break;
}
case 'index': {
const assignee = await this._eval(dest.target, scope);
const i = await this._eval(dest.index, scope);
if (isArray(assignee)) {
assertNumber(i);
if (assignee.value[i.value] === undefined) {
throw new AiScriptIndexOutOfRangeError(`Index out of range. index: ${i.value} max: ${assignee.value.length - 1}`);
}
assignee.value[i.value] = value;
} else if (isObject(assignee)) {
assertString(i);
assignee.value.set(i.value, value);
} else {
throw new AiScriptRuntimeError(`Cannot read prop (${reprValue(i)}) of ${assignee.type}.`);
}
assignee.value[i.value] = value;
} else if (isObject(assignee)) {
assertString(i);
assignee.value.set(i.value, value);
} else {
throw new AiScriptRuntimeError(`Cannot read prop (${reprValue(i)}) of ${assignee.type}.`);
}
} else if (dest.type === 'prop') {
const assignee = await this._eval(dest.target, scope);
assertObject(assignee);

assignee.value.set(dest.name, value);
} else if (dest.type === 'arr') {
assertArray(value);
await Promise.all(dest.value.map(
(item, index) => this.assign(scope, item, value.value[index] ?? NULL)
));
} else if (dest.type === 'obj') {
assertObject(value);
await Promise.all([...dest.value].map(
([key, item]) => this.assign(scope, item, value.value.get(key) ?? NULL)
));
} else {
throw new AiScriptRuntimeError('The left-hand side of an assignment expression must be a variable or a property/index access.');
break;
}
case 'prop': {
const assignee = await this._eval(dest.target, scope);
assertObject(assignee);

assignee.value.set(dest.name, value);
break;
}
case 'arr': {
assertArray(value);
await Promise.all(dest.value.map(
(item, index) => this.assign(scope, item, value.value[index] ?? NULL),
));
break;
}
case 'obj': {
assertObject(value);
await Promise.all([...dest.value].map(
([key, item]) => this.assign(scope, item, value.value.get(key) ?? NULL),
));
break;
}
default: {
throw new AiScriptRuntimeError('The left-hand side of an assignment expression must be a variable or a property/index access.');
}
}
}
}
2 changes: 1 addition & 1 deletion src/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export function isStatement(x: Node): x is Statement {

export type Definition = NodeBase & {
type: 'def'; // 変数宣言文
name: string; // 変数名
dest: Expression; // 宣言式
varType?: TypeSource; // 変数の型
expr: Expression; // 式
mut: boolean; // ミュータブルか否か
Expand Down
10 changes: 9 additions & 1 deletion src/parser/plugins/validate-keyword.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,16 @@ function throwReservedWordError(name: string, loc: Ast.Loc): void {

function validateNode(node: Ast.Node): Ast.Node {
switch (node.type) {
case 'def': {
visitNode(node, node => {
if (node.type === 'identifier' && reservedWord.includes(node.name)) {
throwReservedWordError(node.name, node.loc);
}
return node;
});
break;
}
case 'ns':
case 'def':
case 'attr':
case 'identifier':
case 'prop': {
Expand Down
19 changes: 14 additions & 5 deletions src/parser/syntaxes/statements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,9 +122,16 @@ function parseVarDef(s: ITokenStream): Ast.Definition {
}
s.next();

s.expect(TokenKind.Identifier);
const name = s.getTokenValue();
s.next();
let dest: Ast.Expression;
// 全部parseExprに任せるとparseReferenceが型注釈を巻き込んでしまうためIdentifierのみ個別に処理。
FineArchs marked this conversation as resolved.
Show resolved Hide resolved
if (s.is(TokenKind.Identifier)) {
const nameStartPos = s.getPos();
const name = s.getTokenValue();
s.next();
dest = NODE('identifier', { name }, nameStartPos, s.getPos());
} else {
dest = parseExpr(s, false);
}

let type: Ast.TypeSource | undefined;
if (s.is(TokenKind.Colon)) {
Expand All @@ -141,7 +148,7 @@ function parseVarDef(s: ITokenStream): Ast.Definition {

const expr = parseExpr(s, false);

return NODE('def', { name, varType: type, expr, mut, attr: [] }, startPos, s.getPos());
return NODE('def', { dest, varType: type, expr, mut, attr: [] }, startPos, s.getPos());
}

/**
Expand All @@ -156,8 +163,10 @@ function parseFnDef(s: ITokenStream): Ast.Definition {
s.next();

s.expect(TokenKind.Identifier);
const nameStartPos = s.getPos();
const name = s.getTokenValue();
s.next();
const dest = NODE('identifier', { name }, nameStartPos, s.getPos());

const params = parseParams(s);

Expand All @@ -172,7 +181,7 @@ function parseFnDef(s: ITokenStream): Ast.Definition {
const endPos = s.getPos();

return NODE('def', {
name,
dest,
expr: NODE('fn', {
args: params,
retType: type,
Expand Down
12 changes: 6 additions & 6 deletions test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -831,8 +831,8 @@ describe('Attribute', () => {
`);
assert.equal(nodes.length, 1);
node = nodes[0];
if (node.type !== 'def') assert.fail();
assert.equal(node.name, 'onRecieved');
if (node.type !== 'def' || node.dest.type !== 'identifier') assert.fail();
assert.equal(node.dest.name, 'onRecieved');
assert.equal(node.attr.length, 1);
// attribute 1
attr = node.attr[0];
Expand All @@ -856,8 +856,8 @@ describe('Attribute', () => {
`);
assert.equal(nodes.length, 1);
node = nodes[0];
if (node.type !== 'def') assert.fail();
assert.equal(node.name, 'createNote');
if (node.type !== 'def' || node.dest.type !== 'identifier') assert.fail();
assert.equal(node.dest.name, 'createNote');
assert.equal(node.attr.length, 3);
// attribute 1
attr = node.attr[0];
Expand Down Expand Up @@ -901,8 +901,8 @@ describe('Attribute', () => {
`);
assert.equal(nodes.length, 1);
node = nodes[0];
if (node.type !== 'def') assert.fail();
assert.equal(node.name, 'data');
if (node.type !== 'def' || node.dest.type !== 'identifier') assert.fail();
assert.equal(node.dest.name, 'data');
assert.equal(node.attr.length, 1);
// attribute 1
attr = node.attr[0];
Expand Down
21 changes: 21 additions & 0 deletions test/syntax.ts
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,13 @@ describe('Variable declaration', () => {

assert.ok(err instanceof AiScriptRuntimeError);
});
test.concurrent('destructuring declaration', async () => {
const res = await exe(`
let [a, { value: b }] = [1, { value: 2 }]
<: [a, b]
`);
eq(res, ARR([NUM(1), NUM(2)]));
});
test.concurrent('empty function', async () => {
const res = await exe(`
@hoge() { }
Expand Down Expand Up @@ -1014,6 +1021,20 @@ describe('namespace', () => {
assert.fail();
});

test.concurrent('cannot destructuring declaration', async () => {
try {
await exe(`
:: Foo {
let [a, b] = [1, 2]
}
`);
} catch {
assert.ok(true);
return;
}
assert.fail();
});

test.concurrent('nested', async () => {
const res = await exe(`
<: Foo:Bar:baz()
Expand Down
1 change: 1 addition & 0 deletions unreleased/destr-define.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- 変数宣言で分割代入ができるように(名前空間内では分割代入を使用した宣言はできません。)
Loading