diff --git a/console.js b/console.js index 13a18711..bff29bfa 100644 --- a/console.js +++ b/console.js @@ -27,6 +27,7 @@ const getInterpreter = () => new Interpreter({}, { }, err(e) { console.log(chalk.red(`${e}`)); + interpreter = getInterpreter(); }, log(type, params) { switch (type) { @@ -36,17 +37,22 @@ const getInterpreter = () => new Interpreter({}, { } }); -let interpreter; +let interpreter = getInterpreter(); async function main(){ let a = await i.question('> '); - interpreter?.abort(); if (a === 'exit') return false; + if (a === 'reset') { + interpreter.abort(); + interpreter = getInterpreter(); + return true; + } try { let ast = Parser.parse(a); - interpreter = getInterpreter(); await interpreter.exec(ast); } catch(e) { console.log(chalk.red(`${e}`)); + interpreter.abort(); + interpreter = getInterpreter(); } return true; }; diff --git a/docs/get-started.md b/docs/get-started.md index 77f8a467..781f0efe 100644 --- a/docs/get-started.md +++ b/docs/get-started.md @@ -49,6 +49,7 @@ this is a comment 真理値booltrue/false 配列arr["ai" "chan" "cute"] オブジェクトobj{ foo: "bar"; a: 42; } + 連想配列dicdic { [true]: "apple"; [[1, 2]]: 42; } nullnullnull 関数fn@(x) { x } エラーerror(TODO) @@ -84,7 +85,7 @@ print(message) ``` ## 配列 -`[]`の中に式をスペースで区切って列挙します。 +`[]`の中に式をコンマ(または改行)で区切って列挙します。 ``` ["ai", "chan", "kawaii"] ``` @@ -121,6 +122,45 @@ let obj = {foo: "bar", answer: 42} <: obj["answer"] // 42 ``` +## 連想配列 +オブジェクトと似た文法ですが、`{`の前にキーワード`dic`を置く必要があります。 +また、キーにはプロパティ名の代わりに任意の式を利用します。 +そして、全てのキーを`[]`で囲う必要があります。 +アクセスの方法は`yourdic[]`です。(ドット記法は使えません) + +AiScriptにおいてオブジェクトは文字列のみをキーとしますが、連想配列は全ての値をキーとします。 +```js +var mydic = dic { + [null]: 42 + [true]: 'foo' + [57]: false + ['bar']: null +} +<: mydic['bar'] // null +mydic['bar'] = 12 +<: mydic['bar'] // 12 +``` + +キーを配列、オブジェクト、または連想配列にした場合はdeep-equalで検索が行われます。 +```js +var mydic = dic { + [[1, 2, 3]]: Math:Infinity +} +<: mydic[[1, 2, 3]] // Infinity +mydic[[1, 2, 3]] = -0.083 +<: mydic[[1, 2, 3]] // -0.083 +``` +関数は参照比較です。 +```js +let key = @(){} +var mydic = dic { + [@(){}]: 1 + [key]: 2 +} +<: mydic[@(){}] // null (not found) +<: mydic[key] // 2 +``` + ## 演算 演算は、 ``` diff --git a/docs/keywords.md b/docs/keywords.md index 4f3e2aa7..2db754bb 100644 --- a/docs/keywords.md +++ b/docs/keywords.md @@ -18,7 +18,7 @@ let match=null // エラー ## 一覧 以下の単語が予約語として登録されています。 ### 使用中の語 -`null`, `true`, `false`, `each`, `for`, `loop`, `break`, `continue`, `match`, `case`, `default`, `if`, `elif`, `else`, `return`, `eval`, `var`, `let`, `exists` +`null`, `true`, `false`, `each`, `for`, `loop`, `break`, `continue`, `match`, `case`, `default`, `if`, `elif`, `else`, `return`, `eval`, `var`, `let`, `exists`, `dic` ### 使用予定の語 -`as`, `async`, `attr`, `attribute`, `await`, `catch`, `class`, `component`, `constructor`, `dictionary`, `do`, `enum`, `export`, `finally`, `fn`, `hash`, `in`, `interface`, `out`, `private`, `public`, `ref`, `static`, `struct`, `table`, `this`, `throw`, `trait`, `try`, `undefined`, `use`, `using`, `when`, `while`, `yield`, `import`, `is`, `meta`, `module`, `namespace`, `new` +`as`, `async`, `attr`, `attribute`, `await`, `catch`, `class`, `component`, `constructor`, `do`, `enum`, `export`, `finally`, `fn`, `hash`, `in`, `interface`, `out`, `private`, `public`, `ref`, `static`, `struct`, `table`, `this`, `throw`, `trait`, `try`, `undefined`, `use`, `using`, `when`, `while`, `yield`, `import`, `is`, `meta`, `module`, `namespace`, `new` diff --git a/docs/literals.md b/docs/literals.md index 4e58fa0f..049bca41 100644 --- a/docs/literals.md +++ b/docs/literals.md @@ -105,6 +105,19 @@ Previous statement is { !true }.` {a: 12; b: 'hoge'} // Syntax Error ``` +### 連想配列 +```js +dic {} // 空の連想配列 +dic { + [null]: 'foo' + [1]: true + ['bar']: [1, 2, 3] + [[4, 5, 6]]: { a: 1, b: 2 } + [dic { [{}]: 42 }]: 57 +} +dic{['ai']:'chan',['kawa']:'ii'} // ワンライナー +``` + ### 関数 関数のリテラルは「無名関数」と呼ばれており、[関数の宣言](./syntax.md#%E9%96%A2%E6%95%B0)とよく似た形をしていますが、関数名がありません。(そして、リテラルなので当然ながら、文ではなく式です) ```js diff --git a/etc/aiscript.api.md b/etc/aiscript.api.md index 34eb9211..0b72536e 100644 --- a/etc/aiscript.api.md +++ b/etc/aiscript.api.md @@ -114,6 +114,11 @@ function assertObject(val: Value | null | undefined): asserts val is VObj; // @public (undocumented) function assertString(val: Value | null | undefined): asserts val is VStr; +// @public (undocumented) +function assertValue(val: Value | null | undefined, label: TLabel): asserts val is Value & { + type: TLabel; +}; + // @public (undocumented) type Assign = NodeBase & { type: 'assign'; @@ -170,6 +175,7 @@ declare namespace Ast { Null, Obj, Arr, + Dic, Identifier, Call, Index, @@ -244,6 +250,39 @@ type Definition = NodeBase & { attr: Attribute[]; }; +// @public (undocumented) +const DIC: { + fromNode: (dic: DicNode) => VDic; + fromEntries: (kvs?: [Value, Value][] | undefined) => VDic; +}; + +// @public (undocumented) +type Dic = NodeBase & { + type: 'dic'; + value: [Expression, Expression][]; +}; + +// @public (undocumented) +class DicNode { + constructor(kvs?: [Value, Value][]); + // (undocumented) + get(key: Value): Value; + // Warning: (ae-forgotten-export) The symbol "SeriExpToken" needs to be exported by the entry point index.d.ts + // + // (undocumented) + getRaw(keyGen: Generator): Value | undefined; + // (undocumented) + has(key: Value): boolean; + // (undocumented) + kvs(): Generator<[Value, Value], void, undefined>; + // (undocumented) + serializedKvs(keyPrefix?: SeriExpToken[]): Generator<[SeriExpToken[], Value], void, undefined>; + // (undocumented) + set(key: Value, val: Value): void; + // (undocumented) + setRaw(keyGen: Generator, val: Value): void; +} + // @public (undocumented) type Div = NodeBase & { type: 'div'; @@ -296,7 +335,7 @@ type Exists = NodeBase & { function expectAny(val: Value | null | undefined): asserts val is Value; // @public (undocumented) -type Expression = If | Fn | Match | Block | Exists | Tmpl | Str | Num | Bool | Null | Obj | Arr | Not | Pow | Mul | Div | Rem | Add | Sub | Lt | Lteq | Gt | Gteq | Eq | Neq | And | Or | Identifier | Call | Index | Prop; +type Expression = If | Fn | Match | Block | Exists | Tmpl | Str | Num | Bool | Null | Obj | Arr | Dic | Not | Pow | Mul | Div | Rem | Add | Sub | Lt | Lteq | Gt | Gteq | Eq | Neq | And | Or | Identifier | Call | Index | Prop; // @public (undocumented) const FALSE: { @@ -434,6 +473,11 @@ function isStatement(x: Node_2): x is Statement; // @public (undocumented) function isString(val: Value): val is VStr; +// @public (undocumented) +function isValue(val: Value, label: TLabel): val is Value & { + type: TLabel; +}; + // @public (undocumented) function jsToVal(val: unknown): Value; @@ -699,12 +743,14 @@ declare namespace utils { assertNumber, assertObject, assertArray, + assertValue, isBoolean, isFunction, isString, isNumber, isObject, isArray, + isValue, eq, valToString, valToJs, @@ -723,16 +769,18 @@ function valToJs(val: Value): JsValue; function valToString(val: Value, simple?: boolean): string; // @public (undocumented) -type Value = (VNull | VBool | VNum | VStr | VArr | VObj | VFn | VReturn | VBreak | VContinue | VError) & Attr_2; +type Value = (VNull | VBool | VNum | VStr | VArr | VObj | VDic | VFn | VReturn | VBreak | VContinue | VError) & Attr_2; declare namespace values { export { + DicNode, VNull, VBool, VNum, VStr, VArr, VObj, + VDic, VFn, VUserFn, VFnArg, @@ -751,6 +799,7 @@ declare namespace values { BOOL, OBJ, ARR, + DIC, FN, FN_NATIVE, RETURN, @@ -786,6 +835,12 @@ type VContinue = { value: null; }; +// @public (undocumented) +type VDic = { + type: 'dic'; + value: DicNode; +}; + // @public (undocumented) type VError = { type: 'error'; @@ -854,8 +909,8 @@ type VUserFn = VFnBase & { // Warnings were encountered during analysis: // -// src/interpreter/index.ts:39:4 - (ae-forgotten-export) The symbol "LogObject" needs to be exported by the entry point index.d.ts -// src/interpreter/value.ts:46:2 - (ae-forgotten-export) The symbol "Type" needs to be exported by the entry point index.d.ts +// src/interpreter/index.ts:40:4 - (ae-forgotten-export) The symbol "LogObject" needs to be exported by the entry point index.d.ts +// src/interpreter/value.ts:53:2 - (ae-forgotten-export) The symbol "Type" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/src/interpreter/dic.ts b/src/interpreter/dic.ts new file mode 100644 index 00000000..af073ced --- /dev/null +++ b/src/interpreter/dic.ts @@ -0,0 +1,58 @@ +/* + * このコードではJavaScriptの反復処理プロトコル及びジェネレーター関数を利用しています。 + * 詳細は https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/function* + * を参照して下さい。 + */ + +import { serialize, deserialize } from './serial-expression.js'; +import { NULL } from './value.js'; +import type { SeriExpToken } from './serial-expression.js'; +import type { Value } from './value.js'; + +// TODO: 同時書き込みが発生した場合の衝突の解決 +export class DicNode { + private data?: Value; + private children = new Map(); + + constructor(kvs?: [Value, Value][]) { + if (!kvs) return; + for (const [key, val] of kvs) this.set(key, val); + } + + get(key: Value): Value { + return this.getRaw(serialize(key)) ?? NULL; // キーが見つからなかった場合の挙動を変えやすいように設計しています + } + has(key: Value): boolean { + return this.getRaw(serialize(key)) ? true : false; + } + getRaw(keyGen: Generator): Value | undefined { + const { value: key, done } = keyGen.next(); + if (done) return this.data; + else return this.children.get(key)?.getRaw(keyGen); + } + + set(key: Value, val: Value): void { + this.setRaw(serialize(key), val); + } + setRaw(keyGen: Generator, val: Value): void { + const { value: key, done } = keyGen.next(); + if (done) this.data = val; + else { + if (!this.children.has(key)) this.children.set(key, new DicNode()); + this.children.get(key)!.setRaw(keyGen, val); + } + } + + *kvs(): Generator<[Value, Value], void, undefined> { + for (const [seriExp, val] of this.serializedKvs()) { + yield [deserialize(seriExp), val]; + } + } + *serializedKvs(keyPrefix?: SeriExpToken[]): Generator<[SeriExpToken[], Value], void, undefined> { + const kp = keyPrefix ?? []; + if (this.data) yield [kp, this.data]; + for (const [key, childNode] of this.children) { + yield* childNode.serializedKvs([...kp, key]); + } + } +} diff --git a/src/interpreter/index.ts b/src/interpreter/index.ts index 9f8c772f..851159ad 100644 --- a/src/interpreter/index.ts +++ b/src/interpreter/index.ts @@ -6,10 +6,11 @@ import { autobind } from '../utils/mini-autobind.js'; import { AiScriptError, NonAiScriptError, AiScriptNamespaceError, AiScriptIndexOutOfRangeError, AiScriptRuntimeError } from '../error.js'; import { Scope } from './scope.js'; import { std } from './lib/std.js'; -import { assertNumber, assertString, assertFunction, assertBoolean, assertObject, assertArray, eq, isObject, isArray, expectAny, reprValue } from './util.js'; -import { NULL, RETURN, unWrapRet, FN_NATIVE, BOOL, NUM, STR, ARR, OBJ, FN, BREAK, CONTINUE, ERROR } from './value.js'; +import { assertNumber, assertString, assertFunction, assertBoolean, assertObject, assertArray, assertValue, eq, isObject, isArray, isValue, expectAny, reprValue } from './util.js'; +import { NULL, RETURN, unWrapRet, FN_NATIVE, BOOL, NUM, STR, ARR, DIC, OBJ, FN, BREAK, CONTINUE, ERROR } from './value.js'; import { getPrimProp } from './primitive-props.js'; import { Variable } from './variable.js'; +import { DicNode } from './dic.js'; import type { JsValue } from './util.js'; import type { Value, VFn } from './value.js'; import type * as Ast from '../node.js'; @@ -435,6 +436,13 @@ export class Interpreter { case 'arr': return ARR(await Promise.all(node.value.map(item => this._eval(item, scope)))); + case 'dic': return DIC.fromEntries(await Promise.all( + node.value.map(async ([key, val]) => await Promise.all([ + this._eval(key, scope), + this._eval(val, scope), + ])), + )); + case 'obj': { const obj = new Map(); for (const [key, value] of node.value) { @@ -473,6 +481,8 @@ export class Interpreter { } else { return NULL; } + } else if (isValue(target, 'dic')) { + return target.value.get(i); } else { throw new AiScriptRuntimeError(`Cannot read prop (${reprValue(i)}) of ${target.type}.`); } @@ -737,6 +747,13 @@ export class Interpreter { )); break; } + case 'dic': { + assertValue(value, 'dic'); + await Promise.all([...dest.value].map( + async ([key, item]) => this.define(scope, item, value.value.get(await this._eval(key, scope)) ?? NULL, isMutable), + )); + break; + } default: { throw new AiScriptRuntimeError('The left-hand side of an definition expression must be a variable.'); } @@ -762,6 +779,8 @@ export class Interpreter { } else if (isObject(assignee)) { assertString(i); assignee.value.set(i.value, value); + } else if (isValue(assignee, 'dic')) { + assignee.value.set(i, value); } else { throw new AiScriptRuntimeError(`Cannot read prop (${reprValue(i)}) of ${assignee.type}.`); } @@ -788,6 +807,13 @@ export class Interpreter { )); break; } + case 'dic': { + assertValue(value, 'dic'); + await Promise.all([...dest.value].map( + async ([key, item]) => this.assign(scope, item, value.value.get(await this._eval(key, scope)) ?? NULL), + )); + break; + } default: { throw new AiScriptRuntimeError('The left-hand side of an assignment expression must be a variable or a property/index access.'); } diff --git a/src/interpreter/serial-expression.ts b/src/interpreter/serial-expression.ts new file mode 100644 index 00000000..6ab6a6ca --- /dev/null +++ b/src/interpreter/serial-expression.ts @@ -0,0 +1,167 @@ +/* + * Serialization and deserialization of aiscript Value for uses in dictionaries (and sets, in future). + */ + +/* + * このコードではJavaScriptのジェネレーター関数を利用しています。 + * 詳細は https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/function* + * を参照して下さい。 + */ + +// TODO: ループ構造対策 + +import { mustBeNever } from '../utils/mustbenever.js'; +import { NULL, BOOL, NUM, STR, ARR, OBJ, DIC, ERROR, RETURN, BREAK, CONTINUE } from './value.js'; +import { DicNode } from './dic.js'; +import type { Value, VFn } from './value.js'; + +export type SeriExpToken = + | null + | boolean + | number + | string + | VFn + | typeof SeriExpSymbols[keyof typeof SeriExpSymbols] +; + +export const SeriExpSymbols = { + break: Symbol('SeriExpSymbols.break'), + continue: Symbol('SeriExpSymbols.continue'), + error: Symbol('SeriExpSymbols.error'), + return: Symbol('SeriExpSymbols.return'), + arr: Symbol('SeriExpSymbols.arr'), + obj: Symbol('SeriExpSymbols.obj'), + dic: Symbol('SeriExpSymbols.dic'), + end: Symbol('SeriExpSymbols.end'), +}; + +export function* serialize(val: Value): Generator { + switch (val.type) { + case 'null': + yield null; + break; + case 'bool': + case 'num': + case 'str': + yield val.value; + break; + case 'fn': + yield val; // nativeを比較する処理はdeserialize時に新しいNATIVE_FNオブジェクトを生成しなければならないというコストを鑑み廃止しました + break; + case 'break': + case 'continue': + yield SeriExpSymbols[val.type]; + break; + case 'return': + yield SeriExpSymbols[val.type]; + yield* serialize(val.value); + break; + case 'error': + yield SeriExpSymbols[val.type]; + yield* serialize(val.info ?? NULL); + break; + case 'arr': + yield SeriExpSymbols[val.type]; + for (const v of val.value) yield* serialize(v); + yield SeriExpSymbols.end; + break; + case 'obj': + yield SeriExpSymbols[val.type]; + for (const [k, v] of val.value) { + yield k; + yield* serialize(v); + } + yield SeriExpSymbols.end; + break; + case 'dic': + yield SeriExpSymbols[val.type]; + for (const [k, v] of val.value.serializedKvs()) { + yield* k as Iterable; // it's array actually + yield* serialize(v); + } + yield SeriExpSymbols.end; + break; + default: + mustBeNever(val, 'serializing unknown type'); + } +} + +const END = Symbol('end token of serial expression'); + +export function deserialize(seriExp: Iterable | Iterator): Value { + return deserializeInnerValue((seriExp as any)[Symbol.iterator] ? (seriExp as Iterable)[Symbol.iterator]() : seriExp as Iterator); +} + +function deserializeInnerValue(iterator: Iterator): Value { + const result = deserializeInnerValueOrEnd(iterator); + if (typeof result === 'symbol') throw new Error('unexpected value of serial expression: ' + result.description); + else return result; +} + +function deserializeInnerValueOrEnd(iterator: Iterator): Value | typeof END { + const { value: token, done } = iterator.next(); + if (done) throw new Error('unexpected end of serial expression'); + const nextValue = () => deserializeInnerValue(iterator); + const nextValueOrEnd = () => deserializeInnerValueOrEnd(iterator); + const nextString = () => { + const token = nextStringOrEnd(); + if (typeof token !== 'string') throw new Error('unexpected token of serial expression: end'); + return token; + }; + const nextStringOrEnd = () => { + const { value: token, done } = iterator.next(); + if (done) throw new Error('unexpected end of serial expression'); + if (token !== SeriExpSymbols.end || typeof token !== 'string') throw new Error(`unexpected token of serial expression: ${token as string}`); + return token; + }; + + switch (typeof token) { + case 'boolean': return BOOL(token); + case 'number': return NUM(token); + case 'string': return STR(token); + case 'object': + if (token === null) return NULL; + if (token.type === 'fn') return token; + } + + if (typeof token !== 'symbol') { + // 網羅性チェック、何故かVFnが残っている + mustBeNever(token, `unknown SeriExpToken type: ${token}`); + } + + switch (token) { + case SeriExpSymbols.break: return BREAK(); + case SeriExpSymbols.continue: return CONTINUE(); + case SeriExpSymbols.return: return RETURN(nextValue()); + case SeriExpSymbols.error: return ERROR( + nextString(), + nextValue(), + ); + case SeriExpSymbols.arr: { + const elems: Value[] = []; + while (true) { + const valueOrEnd = nextValueOrEnd(); + if (valueOrEnd === END) return ARR(elems); + elems.push(valueOrEnd); + } + } + case SeriExpSymbols.obj: { + const elems = new Map(); + while (true) { + const key = nextStringOrEnd(); + if (key === SeriExpSymbols.end) return OBJ(elems); + elems.set(key, nextValue()); + } + } + case SeriExpSymbols.dic: { + const elems = new DicNode(); + while (true) { + const key = nextValueOrEnd(); + if (key === END) return DIC.fromNode(elems); + elems.set(key, nextValue()); + } + } + case SeriExpSymbols.end: return END; + default: throw new Error('unknown symbol in SeriExp'); + } +} diff --git a/src/interpreter/util.ts b/src/interpreter/util.ts index 6619309a..37735c30 100644 --- a/src/interpreter/util.ts +++ b/src/interpreter/util.ts @@ -62,6 +62,22 @@ export function assertArray(val: Value | null | undefined): asserts val is VArr } } +export function assertValue< + TLabel extends Value['type'], +>( + val: Value | null | undefined, + label: TLabel, +): asserts val is Value & { + type: TLabel; +} { + if (val == null) { + throw new AiScriptRuntimeError(`Expect ${label}, but got nothing.`); + } + if (val.type !== label) { + throw new AiScriptRuntimeError(`Expect ${label}, but got ${val.type}.`); + } +} + export function isBoolean(val: Value): val is VBool { return val.type === 'bool'; } @@ -86,6 +102,15 @@ export function isArray(val: Value): val is VArr { return val.type === 'arr'; } +export function isValue< + TLabel extends Value['type'], +>( + val: Value, + label: TLabel, +): val is Value & { type: TLabel } { + return val.type === label; +} + export function eq(a: Value, b: Value): boolean { if (a.type === 'fn' && b.type === 'fn') return a.native && b.native ? a.native === b.native : a === b; if (a.type === 'fn' || b.type === 'fn') return false; @@ -188,6 +213,16 @@ export function reprValue(value: Value, literalLike = false, processedObjects = return '{ ' + content.join(', ') + ' }'; } + if (value.type === 'dic') { + processedObjects.add(value.value); + const content = []; + + for (const [key, val] of value.value.kvs()) { + content.push(`[${reprValue(key, true, processedObjects)}]: ${reprValue(val, true, processedObjects)}`); + } + + return 'dic { ' + content.join(', ') + ' }'; + } if (value.type === 'bool') return value.value.toString(); if (value.type === 'null') return 'null'; if (value.type === 'fn') { diff --git a/src/interpreter/value.ts b/src/interpreter/value.ts index a5d271a9..aa86a77a 100644 --- a/src/interpreter/value.ts +++ b/src/interpreter/value.ts @@ -1,3 +1,5 @@ +import { DicNode } from './dic.js'; +export { DicNode }; import type { Expression, Node } from '../node.js'; import type { Type } from '../type.js'; import type { Scope } from './scope.js'; @@ -31,6 +33,11 @@ export type VObj = { value: Map; }; +export type VDic = { + type: 'dic'; + value: DicNode; +}; + export type VFn = VUserFn | VNativeFn; type VFnBase = { type: 'fn'; @@ -86,7 +93,7 @@ export type Attr = { }[]; }; -export type Value = (VNull | VBool | VNum | VStr | VArr | VObj | VFn | VReturn | VBreak | VContinue | VError) & Attr; +export type Value = (VNull | VBool | VNum | VStr | VArr | VObj | VDic | VFn | VReturn | VBreak | VContinue | VError) & Attr; export const NULL = { type: 'null' as const, @@ -127,6 +134,17 @@ export const ARR = (arr: VArr['value']): VArr => ({ value: arr, }); +export const DIC = { + fromNode: (dic: DicNode): VDic => ({ + type: 'dic' as const, + value: dic, + }), + fromEntries: (...dic: ConstructorParameters): VDic => ({ + type: 'dic' as const, + value: new DicNode(...dic), + }), +}; + export const FN = (args: VUserFn['args'], statements: VUserFn['statements'], scope: VUserFn['scope']): VUserFn => ({ type: 'fn' as const, args: args, diff --git a/src/node.ts b/src/node.ts index 2961905a..7e285a27 100644 --- a/src/node.ts +++ b/src/node.ts @@ -133,6 +133,7 @@ export type Expression = Null | Obj | Arr | + Dic | Not | Pow | Mul | @@ -154,7 +155,7 @@ export type Expression = Prop; const expressionTypes = [ - 'if', 'fn', 'match', 'block', 'exists', 'tmpl', 'str', 'num', 'bool', 'null', 'obj', 'arr', + 'if', 'fn', 'match', 'block', 'exists', 'tmpl', 'str', 'num', 'bool', 'null', 'obj', 'arr', 'dic', 'not', 'pow', 'mul', 'div', 'rem', 'add', 'sub', 'lt', 'lteq', 'gt', 'gteq', 'eq', 'neq', 'and', 'or', 'identifier', 'call', 'index', 'prop', ]; @@ -328,6 +329,11 @@ export type Arr = NodeBase & { value: Expression[]; // アイテム }; +export type Dic = NodeBase & { + type: 'dic'; // 連想配列 + value: [Expression, Expression][]; // アイテム +}; + export type Identifier = NodeBase & { type: 'identifier'; // 変数などの識別子 name: string; // 変数名 diff --git a/src/parser/plugins/validate-keyword.ts b/src/parser/plugins/validate-keyword.ts index 1198193f..8015d25d 100644 --- a/src/parser/plugins/validate-keyword.ts +++ b/src/parser/plugins/validate-keyword.ts @@ -18,7 +18,7 @@ const reservedWord = [ 'component', 'constructor', // 'def', - 'dictionary', + // 'dictionary', 'enum', 'export', 'finally', diff --git a/src/parser/scanner.ts b/src/parser/scanner.ts index 75602166..a2512b3a 100644 --- a/src/parser/scanner.ts +++ b/src/parser/scanner.ts @@ -406,6 +406,9 @@ export class Scanner implements ITokenStream { case 'exists': { return TOKEN(TokenKind.ExistsKeyword, pos, { hasLeftSpacing }); } + case 'dic': { + return TOKEN(TokenKind.DicKeyword, pos, { hasLeftSpacing }); + } default: { return TOKEN(TokenKind.Identifier, pos, { hasLeftSpacing, value }); } diff --git a/src/parser/syntaxes/expressions.ts b/src/parser/syntaxes/expressions.ts index 342644ab..a4d23e43 100644 --- a/src/parser/syntaxes/expressions.ts +++ b/src/parser/syntaxes/expressions.ts @@ -278,6 +278,9 @@ function parseAtom(s: ITokenStream, isStatic: boolean): Ast.Expression { case TokenKind.OpenBracket: { return parseArray(s, isStatic); } + case TokenKind.DicKeyword: { + return parseDictionary(s, isStatic); + } case TokenKind.Identifier: { if (isStatic) break; return parseReference(s); @@ -648,6 +651,63 @@ function parseArray(s: ITokenStream, isStatic: boolean): Ast.Arr { return NODE('arr', { value }, startPos, s.getPos()); } +/** + * ```abnf + * Dictionary = "dic" "{" ["[" Expr "]" ":" Expr *(SEP Expr ":" Expr) [SEP]] "}" + * ``` +*/ +function parseDictionary(s: ITokenStream, isStatic: boolean): Ast.Dic { + const startPos = s.getPos(); + + s.expect(TokenKind.DicKeyword); + s.next(); + s.expect(TokenKind.OpenBrace); + s.next(); + + while (s.is(TokenKind.NewLine)) { + s.next(); + } + + const value: [Ast.Expression, Ast.Expression][] = []; + while (!s.is(TokenKind.CloseBrace)) { + s.expect(TokenKind.OpenBracket); + s.next(); + const k = parseExpr(s, isStatic); + s.expect(TokenKind.CloseBracket); + s.next(); + + s.expect(TokenKind.Colon); + s.next(); + + const v = parseExpr(s, isStatic); + + value.push([k, v]); + + // separator + switch (s.getTokenKind()) { + case TokenKind.NewLine: + case TokenKind.Comma: { + s.next(); + while (s.is(TokenKind.NewLine)) { + s.next(); + } + break; + } + case TokenKind.CloseBrace: { + break; + } + default: { + throw new AiScriptSyntaxError('separator expected', s.getPos()); + } + } + } + + s.expect(TokenKind.CloseBrace); + s.next(); + + return NODE('dic', { value }, startPos, s.getPos()); +} + //#region Pratt parsing type PrefixInfo = { opKind: 'prefix', kind: TokenKind, bp: number }; diff --git a/src/parser/token.ts b/src/parser/token.ts index d4bdaf49..c16468b2 100644 --- a/src/parser/token.ts +++ b/src/parser/token.ts @@ -34,6 +34,7 @@ export enum TokenKind { VarKeyword, LetKeyword, ExistsKeyword, + DicKeyword, /** "!" */ Not, diff --git a/src/utils/mustbenever.ts b/src/utils/mustbenever.ts new file mode 100644 index 00000000..02296609 --- /dev/null +++ b/src/utils/mustbenever.ts @@ -0,0 +1,7 @@ +// exhaustiveness checker function +export function mustBeNever(value: NoInfer, errormes: string): never { + throw new Error(errormes); +} + +// https://stackoverflow.com/questions/56687668/a-way-to-disable-type-argument-inference-in-generics +type NoInfer = [T][T extends unknown ? 0 : never]; diff --git a/src/utils/random/genrng.ts b/src/utils/random/genrng.ts index f6a6a4ad..799308fe 100644 --- a/src/utils/random/genrng.ts +++ b/src/utils/random/genrng.ts @@ -1,9 +1,9 @@ import seedrandom from 'seedrandom'; import { FN_NATIVE, NULL, NUM } from '../../interpreter/value.js'; +import { textEncoder } from '../../const.js'; import { SeedRandomWrapper } from './seedrandom.js'; import { ChaCha20 } from './chacha20.js'; import type { VNativeFn, VNull, Value } from '../../interpreter/value.js'; -import { textEncoder } from '../../const.js'; export function GenerateLegacyRandom(seed: Value | undefined) : VNativeFn | VNull { if (!seed || seed.type !== 'num' && seed.type !== 'str') return NULL; diff --git a/test/index.ts b/test/index.ts index 6a1c3611..0c2cdee7 100644 --- a/test/index.ts +++ b/test/index.ts @@ -6,7 +6,7 @@ import * as assert from 'assert'; import { describe, test } from 'vitest'; import { Parser, Interpreter, Ast } from '../src'; -import { NUM, STR, NULL, ARR, OBJ, BOOL, TRUE, FALSE, ERROR ,FN_NATIVE } from '../src/interpreter/value'; +import { NUM, STR, NULL, ARR, OBJ, DIC, BOOL, TRUE, FALSE, ERROR, FN_NATIVE } from '../src/interpreter/value'; import { AiScriptSyntaxError, AiScriptRuntimeError, AiScriptIndexOutOfRangeError } from '../src/error'; import { exe, eq } from './testutils'; @@ -313,6 +313,104 @@ describe('Array', () => { }); }); +describe('Dictionary', () => { + describe('simple keys', () => { + test.concurrent('basic', () => exe(` + let dic1 = dic { + [null]: false + [true]: 'fuga' + [3]: 57 + ['hoge']: null + } + <: [dic1, dic1[null], dic1[true], dic1[3], dic1['hoge']] + `).then(res => eq(res, ARR([ + DIC.fromEntries([ + [NULL, FALSE], + [TRUE, STR('fuga')], + [NUM(3), NUM(57)], + [STR('hoge'), NULL], + ]), + FALSE, + STR('fuga'), + NUM(57), + NULL, + ])))); + + test.concurrent('assignment', () => exe(` + var a = dic { [null]: 1 , [true]: true } + a[true] = false + a[22] = 'bar' + <: [a[null], a[true], a[22]] + `).then(res => eq(res, ARR([ + NUM(1), FALSE, STR('bar') + ])))); + }); + + describe('fn keys', () => { + test.concurrent('basic', () => exe(` + let key1 = @(a) {a} + let key2 = Core:add + let dic1 = dic { + [key1]: 1 + [Core:add]: 2 + } + <: [dic1[key1], dic1[key2]] + `).then(res => eq(res, ARR([NUM(1), NUM(2)])))); + + test.concurrent('same form', () => exe(` + let key1 = @() {} + let key2 = @() {} + let dic1 = dic { + [key1]: 1 + [key2]: 2 + } + <: [dic1[key1], dic1[key2]] + `).then(res => eq(res, ARR([NUM(1), NUM(2)])))); + }); + + describe('structure keys', () => { + test.concurrent('basic', () => exe(` + let dic1 = dic { + [[]]: 1 + [[1, 2]]: 2 + [{}]: 3 + [{a: 1}]: 4 + [dic {}]: 5 + [dic {[[]]: null}]: 6 + } + <: [dic1[[]], dic1[[1, 2]], dic1[{}], dic1[{a: 1}], dic1[dic {}], dic1[dic {[[]]: null}]] + `).then(res => eq(res, ARR([NUM(1), NUM(2), NUM(3), NUM(4), NUM(5), NUM(6)])))); + + test.concurrent('assignment', () => exe(` + var dic1 = dic { + [[0, 1]]: 'kept-arr' + [[0]]: 'overwritten-arr' + [{a: 0, b: 1}]: 'kept-obj' + [{a: 0}]: 'overwritten-obj' + [dic {[{}]: 'hoge', [{a: 0}]: 'fuga'}]: 'kept-dic' + [dic {[{}]: 'hoge'}]: 'overwritten-dic' + } + dic1[[0]] = 'set-arr' + dic1[[0, 1, 2]] = 'added-arr' + dic1[{a: 0, b: 1, c: 2}] = 'added-obj' + dic1[{a: 0}] = 'set-obj' + dic1[dic {[{}]: 'hoge'}] = 'set-dic' + dic1[dic {[{}]: 'hoge', [{a: 0}]: 'fuga', [dic {}]: 'piyo'}] = 'added-dic' + <: [ + dic1[[0]], dic1[[0, 1]], dic1[[0, 1, 2]], + dic1[{a: 0}], dic1[{a: 0, b: 1}], dic1[{a: 0, b: 1, c: 2}], + dic1[dic {[{}]: 'hoge'}], + dic1[dic {[{}]: 'hoge', [{a: 0}]: 'fuga'}], + dic1[dic {[{}]: 'hoge', [{a: 0}]: 'fuga', [dic {}]: 'piyo'}], + ] + `).then(res => eq(res, ARR([ + STR('set-arr'), STR('kept-arr'), STR('added-arr'), + STR('set-obj'), STR('kept-obj'), STR('added-obj'), + STR('set-dic'), STR('kept-dic'), STR('added-dic'), + ])))); + }); +}); + describe('chain', () => { test.concurrent('chain access (prop + index + call)', async () => { const res = await exe(` diff --git a/test/literals.ts b/test/literals.ts index 731d11b4..616cded5 100644 --- a/test/literals.ts +++ b/test/literals.ts @@ -1,7 +1,7 @@ import * as assert from 'assert'; import { describe, test } from 'vitest'; import { } from '../src'; -import { NUM, STR, NULL, ARR, OBJ, BOOL, TRUE, FALSE, ERROR ,FN_NATIVE } from '../src/interpreter/value'; +import { NUM, STR, NULL, ARR, OBJ, DIC, BOOL, TRUE, FALSE, ERROR ,FN_NATIVE } from '../src/interpreter/value'; import { } from '../src/error'; import { exe, eq } from './testutils'; @@ -58,78 +58,41 @@ describe('literal', () => { eq(res, NUM(0.5)); }); - test.concurrent('arr (separated by comma)', async () => { - const res = await exe(` - <: [1, 2, 3] - `); - eq(res, ARR([NUM(1), NUM(2), NUM(3)])); - }); - - test.concurrent('arr (separated by comma) (with trailing comma)', async () => { - const res = await exe(` - <: [1, 2, 3,] - `); - eq(res, ARR([NUM(1), NUM(2), NUM(3)])); - }); - - test.concurrent('arr (separated by line break)', async () => { - const res = await exe(` - <: [ - 1 - 2 - 3 - ] - `); - eq(res, ARR([NUM(1), NUM(2), NUM(3)])); - }); - - test.concurrent('arr (separated by line break and comma)', async () => { - const res = await exe(` - <: [ - 1, - 2, - 3 - ] - `); - eq(res, ARR([NUM(1), NUM(2), NUM(3)])); - }); - - test.concurrent('arr (separated by line break and comma) (with trailing comma)', async () => { - const res = await exe(` - <: [ - 1, - 2, - 3, - ] - `); - eq(res, ARR([NUM(1), NUM(2), NUM(3)])); + describe.each([[ + 'arr', + (cm, tcm, lb) => `<: [${lb}1${cm}${lb}2${cm}${lb}3${tcm}${lb}]`, + ARR([NUM(1), NUM(2), NUM(3)]), + ], [ + 'obj', + (cm, tcm, lb) => `<: {${lb}a: 1${cm}${lb}b: 2${cm}${lb}c: 3${tcm}${lb}}`, + OBJ(new Map([['a', NUM(1)], ['b', NUM(2)], ['c', NUM(3)]])), + ], [ + 'dic', + (cm, tcm, lb) => `<: dic {${lb}[null]: 1${cm}${lb}[2]: 2${cm}${lb}["c"]: 3${tcm}${lb}}`, + DIC.fromEntries([[NULL, NUM(1)], [NUM(2), NUM(2)], [STR('c'), NUM(3)]]), + ]])('%s', (_, script, result) => { + test.concurrent.each([[ + 'separated by comma', + [', ', '', ''], + ], [ + 'separated by comma, with trailing comma', + [', ', ',', ''], + ], [ + 'separated by line break', + ['', '', '\n'], + ], [ + 'separated by line break and comma', + [',', '', '\n'], + ], [ + 'separated by line break and comma, with trailing comma', + [',', ',', '\n'], + ]])('%s', async (_, [cm, tcm, lb]) => { + eq( + result, + await exe(script(cm, tcm, lb)), + ); + }); }); - - test.concurrent('obj (separated by comma)', async () => { - const res = await exe(` - <: { a: 1, b: 2, c: 3 } - `); - eq(res, OBJ(new Map([['a', NUM(1)], ['b', NUM(2)], ['c', NUM(3)]]))); - }); - - test.concurrent('obj (separated by comma) (with trailing comma)', async () => { - const res = await exe(` - <: { a: 1, b: 2, c: 3, } - `); - eq(res, OBJ(new Map([['a', NUM(1)], ['b', NUM(2)], ['c', NUM(3)]]))); - }); - - test.concurrent('obj (separated by line break)', async () => { - const res = await exe(` - <: { - a: 1 - b: 2 - c: 3 - } - `); - eq(res, OBJ(new Map([['a', NUM(1)], ['b', NUM(2)], ['c', NUM(3)]]))); - }); - test.concurrent('obj and arr (separated by line break)', async () => { const res = await exe(` <: {