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: TypeScriptの型を厳格化 #891

Open
wants to merge 21 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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
12 changes: 7 additions & 5 deletions etc/aiscript.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -353,12 +353,14 @@ type FnTypeSource = NodeBase & {
type For = NodeBase & {
type: 'for';
label?: string;
var?: string;
from?: Expression;
to?: Expression;
times?: Expression;
for: Statement | Expression;
};
} & ({
var: string;
from: Expression;
to: Expression;
} | {
times: Expression;
});

// @public (undocumented)
function getLangVersion(input: string): string | null;
Expand Down
10 changes: 6 additions & 4 deletions src/error.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { TokenKind } from './parser/token.js';
import type { Pos } from './node.js';

export abstract class AiScriptError extends Error {
Expand All @@ -13,7 +12,7 @@
this.info = info;

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

Check warning on line 15 in src/error.ts

View workflow job for this annotation

GitHub Actions / lint

Unnecessary conditional, value is always truthy
Error.captureStackTrace(this, AiScriptError);
}
}
Expand All @@ -25,9 +24,12 @@
export class NonAiScriptError extends AiScriptError {
public name = 'Internal';
constructor(error: unknown) {
const message = String(
(error as { message?: unknown } | null | undefined)?.message ?? error,
);
let message: string;
if (error != null && typeof error === 'object' && 'message' in error) {
message = String(error.message);
} else {
message = String(error);
}
super(message, error);
}
}
Expand Down
71 changes: 34 additions & 37 deletions src/interpreter/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
import { Variable } from './variable.js';
import { Reference } from './reference.js';
import type { JsValue } from './util.js';
import type { Value, VFn, VUserFn } from './value.js';
import type { Value, VFn, VFnParam } from './value.js';

export type LogObject = {
scope?: string;
Expand Down Expand Up @@ -61,7 +61,7 @@
const q = args[0];
assertString(q);
if (this.opts.in == null) return NULL;
const a = await this.opts.in!(q.value);
const a = await this.opts.in(q.value);
return STR(a);
}),
};
Expand Down Expand Up @@ -280,15 +280,19 @@
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
return result ?? NULL;
} else {
const fnScope = fn.scope!.createChildScope();
const fnScope = fn.scope.createChildScope();
for (const [i, param] of fn.params.entries()) {
const arg = args[i];
if (!param.default) expectAny(arg);
this.define(fnScope, param.dest, arg ?? param.default!, true);
let arg = args[i];
if (!param.default) {
expectAny(arg);
} else if (!arg) {
arg = param.default;
}
this.define(fnScope, param.dest, arg, true);
}

const info: CallInfo = { name: fn.name ?? '<anonymous>', pos };
return unWrapRet(await this._run(fn.statements!, fnScope, [...callStack, info]));
return unWrapRet(await this._run(fn.statements, fnScope, [...callStack, info]));
}
}

Expand Down Expand Up @@ -411,7 +415,7 @@

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

Check warning on line 418 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(), callStack);
if (v.type === 'break') {
if (v.label != null && v.label !== node.label) {
Expand All @@ -430,7 +434,7 @@
}

case 'for': {
if (node.times) {
if ('times' in node) {
const times = await this._eval(node.times, scope, callStack);
if (isControl(times)) {
return times;
Expand All @@ -452,19 +456,19 @@
}
}
} else {
const from = await this._eval(node.from!, scope, callStack);
const from = await this._eval(node.from, scope, callStack);
if (isControl(from)) {
return from;
}
const to = await this._eval(node.to!, scope, callStack);
const to = await this._eval(node.to, scope, callStack);
if (isControl(to)) {
return to;
}
assertNumber(from);
assertNumber(to);
for (let i = from.value; i < from.value + to.value; i++) {
const v = await this._eval(node.for, scope.createChildScope(new Map([
[node.var!, {
[node.var, {
isMutable: false,
value: NUM(i),
}],
Expand Down Expand Up @@ -632,8 +636,9 @@
return target;
}
if (isObject(target)) {
if (target.value.has(node.name)) {
return target.value.get(node.name)!;
const value = target.value.get(node.name);
if (value != null) {
return value;
} else {
return NULL;
}
Expand All @@ -660,8 +665,9 @@
return item;
} else if (isObject(target)) {
assertString(i);
if (target.value.has(i.value)) {
return target.value.get(i.value)!;
const value = target.value.get(i.value);
if (value != null) {
return value;
} else {
return NULL;
}
Expand Down Expand Up @@ -698,28 +704,21 @@
}

case 'fn': {
const params = await Promise.all(node.params.map(async (param) => {
return {
const params: VFnParam[] = [];
for (const param of node.params) {
const defaultValue = param.default ? await this._eval(param.default, scope, callStack) :
param.optional ? NULL :
undefined;
if (defaultValue != null && isControl(defaultValue)) {
return defaultValue;
}
params.push({
dest: param.dest,
default:
param.default ? await this._eval(param.default, scope, callStack) :
param.optional ? NULL :
undefined,
default: defaultValue,
// type: (TODO)
};
}));
const control = params
.map((param) => param.default)
.filter((value) => value != null)
.find(isControl);
if (control != null) {
return control;
});
}
return FN(
params as VUserFn['params'],
node.children,
scope,
);
return FN(params, node.children, scope);
}

case 'block': {
Expand Down Expand Up @@ -887,9 +886,7 @@

let v: Value | Control = NULL;

for (let i = 0; i < program.length; i++) {
const node = program[i]!;

for (const node of program) {
v = await this._eval(node, scope, callStack);
if (v.type === 'return') {
this.log('block:return', { scope: scope.name, val: v.value });
Expand Down Expand Up @@ -946,7 +943,7 @@
public pause(): void {
if (this.pausing) return;
let resolve: () => void;
const promise = new Promise<void>(r => { resolve = () => r(); });

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

View workflow job for this annotation

GitHub Actions / lint

Missing return type on function
this.pausing = { promise, resolve: resolve! };
for (const handler of this.pauseHandlers) {
handler();
Expand Down
43 changes: 21 additions & 22 deletions src/interpreter/primitive-props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,21 @@
type VWithPP = VNum|VStr|VArr|VError;

const PRIMITIVE_PROPS: {
[key in VWithPP['type']]: { [key: string]: (target: Value) => Value }
[key in VWithPP['type']]: Map<string, (target: Value) => Value>;
} & {
[key in (Exclude<Value, VWithPP>)['type']]?: never;
} = {
num: {
num: new Map(Object.entries({
to_str: (target: VNum): VFn => FN_NATIVE(async (_, _opts) => {
return STR(target.value.toString());
}),

to_hex: (target: VNum): VFn => FN_NATIVE(async (_, _opts) => {
return STR(target.value.toString(16));
}),
},
})),

str: {
str: new Map(Object.entries({
to_num: (target: VStr): VFn => FN_NATIVE(async (_, _opts) => {
const parsed = parseInt(target.value, 10);
if (isNaN(parsed)) return NULL;
Expand Down Expand Up @@ -90,7 +92,7 @@
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 95 in src/interpreter/primitive-props.ts

View workflow job for this annotation

GitHub Actions / lint

Unnecessary conditional, value is always truthy
} else {
return ARR(toArray(target.value).map(s => STR(s)));
}
Expand Down Expand Up @@ -168,9 +170,9 @@

return STR(target.value.padEnd(width.value, s));
}),
},
})),

arr: {
arr: new Map(Object.entries({
len: (target: VArr): VNum => NUM(target.value.length),

push: (target: VArr): VFn => FN_NATIVE(async ([val], _opts) => {
Expand Down Expand Up @@ -219,9 +221,8 @@

filter: (target: VArr): VFn => FN_NATIVE(async ([fn], opts) => {
assertFunction(fn);
const vals = [] as Value[];
for (let i = 0; i < target.value.length; i++) {
const item = target.value[i]!;
const vals: Value[] = [];
for (const [i, item] of target.value.entries()) {
const res = await opts.call(fn, [item, NUM(i)]);
assertBoolean(res);
if (res.value) vals.push(item);
Expand All @@ -243,8 +244,7 @@

find: (target: VArr): VFn => FN_NATIVE(async ([fn], opts) => {
assertFunction(fn);
for (let i = 0; i < target.value.length; i++) {
const item = target.value[i]!;
for (const [i, item] of target.value.entries()) {
const res = await opts.call(fn, [item, NUM(i)]);
assertBoolean(res);
if (res.value) return item;
Expand Down Expand Up @@ -312,7 +312,7 @@
return target;
}),

fill: (target: VArr): VFn => FN_NATIVE(async ([val, st, ed], opts) => {

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

View workflow job for this annotation

GitHub Actions / lint

'opts' is defined but never used. Allowed unused args must match /^_/u
const value = val ?? NULL;
const start = st && (assertNumber(st), st.value);
const end = ed && (assertNumber(ed), ed.value);
Expand All @@ -320,7 +320,7 @@
return target;
}),

repeat: (target: VArr): VFn => FN_NATIVE(async ([times], opts) => {

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

View workflow job for this annotation

GitHub Actions / lint

'opts' is defined but never used. Allowed unused args must match /^_/u
assertNumber(times);
try {
return ARR(Array(times.value).fill(target.value).flat());
Expand All @@ -331,7 +331,7 @@
}
}),

splice: (target: VArr): VFn => FN_NATIVE(async ([idx, rc, vs], opts) => {

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

View workflow job for this annotation

GitHub Actions / lint

'opts' is defined but never used. Allowed unused args must match /^_/u
assertNumber(idx);
const index = (idx.value < -target.value.length) ? 0
: (idx.value < 0) ? target.value.length + idx.value
Expand All @@ -347,7 +347,7 @@
return ARR(result);
}),

flat: (target: VArr): VFn => FN_NATIVE(async ([depth], opts) => {

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

View workflow job for this annotation

GitHub Actions / lint

'opts' is defined but never used. Allowed unused args must match /^_/u
depth = depth ?? NUM(1);
assertNumber(depth);
if (!Number.isInteger(depth.value)) throw new AiScriptRuntimeError('arr.flat expected integer, got non-integer');
Expand Down Expand Up @@ -382,8 +382,7 @@

every: (target: VArr): VFn => FN_NATIVE(async ([fn], opts) => {
assertFunction(fn);
for (let i = 0; i < target.value.length; i++) {
const item = target.value[i]!;
for (const [i, item] of target.value.entries()) {
const res = await opts.call(fn, [item, NUM(i)]);
assertBoolean(res);
if (!res.value) return FALSE;
Expand All @@ -393,8 +392,7 @@

some: (target: VArr): VFn => FN_NATIVE(async ([fn], opts) => {
assertFunction(fn);
for (let i = 0; i < target.value.length; i++) {
const item = target.value[i]!;
for (const [i, item] of target.value.entries()) {
const res = await opts.call(fn, [item, NUM(i)]);
assertBoolean(res);
if (res.value) return TRUE;
Expand Down Expand Up @@ -423,20 +421,21 @@
assertNumber(index);
return target.value.at(index.value) ?? otherwise ?? NULL;
}),
},
})),

error: {
error: new Map(Object.entries({
name: (target: VError): VStr => STR(target.value),

info: (target: VError): Value => target.info ?? NULL,
},
})),
} as const;

export function getPrimProp(target: Value, name: string): Value {
if (Object.hasOwn(PRIMITIVE_PROPS, target.type)) {
const props = PRIMITIVE_PROPS[target.type as VWithPP['type']];
if (Object.hasOwn(props, name)) {
return props[name]!(target);
const props = PRIMITIVE_PROPS[target.type];
if (props != null) {
const prop = props.get(name);
if (prop != null) {
return prop(target);
} else {
throw new AiScriptRuntimeError(`No such prop (${name}) in ${target.type}.`);
}
Expand Down
23 changes: 13 additions & 10 deletions src/interpreter/scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,19 @@ import type { Value } from './value.js';
import type { Variable } from './variable.js';
import type { LogObject } from './index.js';

export type LayerdStates = [Map<string, Variable>, ...Map<string, Variable>[]]

export class Scope {
private parent?: Scope;
private layerdStates: Map<string, Variable>[];
private layerdStates: LayerdStates;
public name: string;
public opts: {
log?(type: string, params: LogObject): void;
onUpdated?(name: string, value: Value): void;
} = {};
public nsName?: string;

constructor(layerdStates: Scope['layerdStates'] = [], parent?: Scope, name?: Scope['name'], nsName?: string) {
constructor(layerdStates: Scope['layerdStates'] = [new Map()], parent?: Scope, name?: Scope['name'], nsName?: string) {
this.layerdStates = layerdStates;
this.parent = parent;
this.name = name || (layerdStates.length === 1 ? '<root>' : '<anonymous>');
Expand All @@ -41,13 +43,13 @@ export class Scope {

@autobind
public createChildScope(states: Map<string, Variable> = new Map(), name?: Scope['name']): Scope {
const layer = [states, ...this.layerdStates];
const layer: LayerdStates = [states, ...this.layerdStates];
return new Scope(layer, this, name);
}

@autobind
public createChildNamespaceScope(nsName: string, states: Map<string, Variable> = new Map(), name?: Scope['name']): Scope {
const layer = [states, ...this.layerdStates];
const layer: LayerdStates = [states, ...this.layerdStates];
return new Scope(layer, this, name, nsName);
}

Expand All @@ -58,8 +60,9 @@ export class Scope {
@autobind
public get(name: string): Value {
for (const layer of this.layerdStates) {
if (layer.has(name)) {
const state = layer.get(name)!.value;
const value = layer.get(name);
if (value) {
const state = value.value;
this.log('read', { var: name, val: state });
return state;
}
Expand Down Expand Up @@ -103,7 +106,7 @@ export class Scope {
public getAll(): Map<string, Variable> {
const vars = this.layerdStates.reduce((arr, layer) => {
return [...arr, ...layer];
}, [] as [string, Variable][]);
}, [] satisfies [string, Variable][]);
return new Map(vars);
}

Expand All @@ -115,7 +118,7 @@ export class Scope {
@autobind
public add(name: string, variable: Variable): void {
this.log('add', { var: name, val: variable });
const states = this.layerdStates[0]!;
const states = this.layerdStates[0];
if (states.has(name)) {
throw new AiScriptRuntimeError(
`Variable '${name}' already exists in scope '${this.name}'`,
Expand All @@ -135,8 +138,8 @@ export class Scope {
public assign(name: string, val: Value): void {
let i = 1;
for (const layer of this.layerdStates) {
if (layer.has(name)) {
const variable = layer.get(name)!;
const variable = layer.get(name);
if (variable != null) {
if (!variable.isMutable) {
throw new AiScriptRuntimeError(`Cannot assign to an immutable variable ${name}.`);
}
Expand Down
12 changes: 7 additions & 5 deletions src/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,12 +82,14 @@ export type Each = NodeBase & {
export type For = NodeBase & {
type: 'for'; // for文
label?: string; // ラベル
var?: string; // イテレータ変数名
from?: Expression; // 開始値
to?: Expression; // 終値
times?: Expression; // 回数
for: Statement | Expression; // 本体処理
};
} & ({
var: string; // イテレータ変数名
from: Expression; // 開始値
to: Expression; // 終値
} | {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
} | {
} | {
var: string; // イテレータ変数名

Copy link
Contributor Author

Choose a reason for hiding this comment

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

vartimesが同時に定義されることはないと思います。
for let i, 10 {}from = 0, to = 10になりますし

times: Expression; // 回数
});

export type Loop = NodeBase & {
type: 'loop'; // loop文
Expand Down
Loading
Loading