Skip to content

Commit

Permalink
fix: 複合代入文の左辺が1回だけ評価されるように (#847)
Browse files Browse the repository at this point in the history
* 複合代入文の左辺が1回だけ評価されるように

* APIレポートの更新

* get,setメソッドに変更
  • Loading branch information
takejohn authored Nov 9, 2024
1 parent d2f4965 commit 2a8abf6
Show file tree
Hide file tree
Showing 5 changed files with 154 additions and 34 deletions.
2 changes: 1 addition & 1 deletion etc/aiscript.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -866,7 +866,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/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)
Expand Down
55 changes: 22 additions & 33 deletions src/interpreter/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { assertNumber, assertString, assertFunction, assertBoolean, assertObject
import { NULL, RETURN, unWrapRet, FN_NATIVE, BOOL, NUM, STR, ARR, OBJ, FN, BREAK, CONTINUE, ERROR } from './value.js';
import { getPrimProp } from './primitive-props.js';
import { Variable } from './variable.js';
import { Reference } from './reference.js';
import type { JsValue } from './util.js';
import type { Value, VFn } from './value.js';

Expand Down Expand Up @@ -452,30 +453,33 @@ export class Interpreter {
}

case 'assign': {
const target = await this.getReference(node.dest, scope, callStack);
const v = await this._eval(node.expr, scope, callStack);

await this.assign(scope, node.dest, v, callStack);
target.set(v);

return NULL;
}

case 'addAssign': {
const target = await this._eval(node.dest, scope, callStack);
assertNumber(target);
const target = await this.getReference(node.dest, scope, callStack);
const v = await this._eval(node.expr, scope, callStack);
assertNumber(v);
const targetValue = target.get();
assertNumber(targetValue);

await this.assign(scope, node.dest, NUM(target.value + v.value), callStack);
target.set(NUM(targetValue.value + v.value));
return NULL;
}

case 'subAssign': {
const target = await this._eval(node.dest, scope, callStack);
assertNumber(target);
const target = await this.getReference(node.dest, scope, callStack);
const v = await this._eval(node.expr, scope, callStack);
assertNumber(v);
const targetValue = target.get();
assertNumber(targetValue);

await this.assign(scope, node.dest, NUM(target.value - v.value), callStack);
target.set(NUM(targetValue.value - v.value));
return NULL;
}

Expand Down Expand Up @@ -803,54 +807,39 @@ export class Interpreter {
}

@autobind
private async assign(
scope: Scope,
dest: Ast.Expression,
value: Value,
callStack: readonly CallInfo[],
): Promise<void> {
private async getReference(dest: Ast.Expression, scope: Scope, callStack: readonly CallInfo[]): Promise<Reference> {
switch (dest.type) {
case 'identifier': {
scope.assign(dest.name, value);
break;
return Reference.variable(dest.name, scope);
}
case 'index': {
const assignee = await this._eval(dest.target, scope, callStack);
const i = await this._eval(dest.index, scope, callStack);
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;
return Reference.index(assignee, i.value);
} else if (isObject(assignee)) {
assertString(i);
assignee.value.set(i.value, value);
return Reference.prop(assignee, i.value);
} else {
throw new AiScriptRuntimeError(`Cannot read prop (${reprValue(i)}) of ${assignee.type}.`);
}
break;
}
case 'prop': {
const assignee = await this._eval(dest.target, scope, callStack);
assertObject(assignee);

assignee.value.set(dest.name, value);
break;
return Reference.prop(assignee, dest.name);
}
case 'arr': {
assertArray(value);
await Promise.all(dest.value.map(
(item, index) => this.assign(scope, item, value.value[index] ?? NULL, callStack),
));
break;
const items = await Promise.all(dest.value.map((item) => this.getReference(item, scope, callStack)));
return Reference.arr(items);
}
case 'obj': {
assertObject(value);
await Promise.all([...dest.value].map(
([key, item]) => this.assign(scope, item, value.value.get(key) ?? NULL, callStack),
));
break;
const entries = new Map(await Promise.all([...dest.value].map(
async ([key, item]) => [key, await this.getReference(item, scope, callStack)] as const
)));
return Reference.obj(entries);
}
default: {
throw new AiScriptRuntimeError('The left-hand side of an assignment expression must be a variable or a property/index access.');
Expand Down
108 changes: 108 additions & 0 deletions src/interpreter/reference.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { AiScriptIndexOutOfRangeError } from '../error.js';
import { assertArray, assertObject } from './util.js';
import { ARR, NULL, OBJ } from './value.js';
import type { VArr, VObj, Value } from './value.js';
import type { Scope } from './scope.js';

export interface Reference {
get(): Value;

set(value: Value): void;
}

export const Reference = {
variable(name: string, scope: Scope): Reference {
return new VariableReference(name, scope);
},

index(target: VArr, index: number): Reference {
return new IndexReference(target.value, index);
},

prop(target: VObj, name: string): Reference {
return new PropReference(target.value, name);
},

arr(dest: readonly Reference[]): Reference {
return new ArrReference(dest);
},

obj(dest: ReadonlyMap<string, Reference>): Reference {
return new ObjReference(dest);
},
};

class VariableReference implements Reference {
constructor(private name: string, private scope: Scope) {}

get(): Value {
return this.scope.get(this.name);
}

set(value: Value): void {
this.scope.assign(this.name, value);
}
}

class IndexReference implements Reference {
constructor(private target: Value[], private index: number) {}

get(): Value {
this.assertIndexInRange();
return this.target[this.index]!;
}

set(value: Value): void {
this.assertIndexInRange();
this.target[this.index] = value;
}

private assertIndexInRange(): void {
const index = this.index;
if (index < 0 || this.target.length <= index) {
throw new AiScriptIndexOutOfRangeError(`Index out of range. index: ${this.index} max: ${this.target.length - 1}`);
}
}
}

class PropReference implements Reference {
constructor(private target: Map<string, Value>, private index: string) {}

get(): Value {
return this.target.get(this.index) ?? NULL;
}

set(value: Value): void {
this.target.set(this.index, value);
}
}

class ArrReference implements Reference {
constructor(private items: readonly Reference[]) {}

get(): Value {
return ARR(this.items.map((item) => item.get()));
}

set(value: Value): void {
assertArray(value);
for (const [index, item] of this.items.entries()) {
item.set(value.value[index] ?? NULL);
}
}
}

class ObjReference implements Reference {
constructor(private entries: ReadonlyMap<string, Reference>) {}

get(): Value {
return OBJ(new Map([...this.entries].map(([key, item]) => [key, item.get()])));
}

set(value: Value): void {
assertObject(value);
for (const [key, item] of this.entries.entries()) {
item.set(value.value.get(key) ?? NULL);
}
}
}
22 changes: 22 additions & 0 deletions test/syntax.ts
Original file line number Diff line number Diff line change
Expand Up @@ -650,6 +650,28 @@ describe('Variable assignment', () => {
<: [hoge, fuga]
`), ARR([STR('bar'), STR('foo')]));
});

describe('eval left hand once', () => {
test.concurrent('add', async () => {
const res = await exe(`
var index = -1
let array = [0, 0]
array[eval { index += 1; index }] += 1
<: array
`);
eq(res, ARR([NUM(1), NUM(0)]));
});

test.concurrent('sub', async () => {
const res = await exe(`
var index = -1
let array = [0, 0]
array[eval { index += 1; index }] -= 1
<: array
`);
eq(res, ARR([NUM(-1), NUM(0)]));
});
});
});

describe('for', () => {
Expand Down
1 change: 1 addition & 0 deletions unreleased/assign-left-eval-once.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **Breaking Change** 複合代入文(`+=`, `-=`)の左辺が1回だけ評価されるようになりました。

0 comments on commit 2a8abf6

Please sign in to comment.