From 2ab9777738bcc34dba4c0227c5087ce75cc5ebfb Mon Sep 17 00:00:00 2001 From: Fine Archs Date: Wed, 5 Jun 2024 09:47:22 +0900 Subject: [PATCH 01/11] create dic --- console.js | 12 +- src/interpreter/dic.ts | 57 +++++++++ src/interpreter/index.ts | 22 +++- src/interpreter/serial-expression.ts | 170 +++++++++++++++++++++++++ src/interpreter/util.ts | 37 +++++- src/interpreter/value.ts | 13 +- src/node.ts | 8 +- src/parser/plugins/validate-keyword.ts | 2 +- src/parser/scanner.ts | 3 + src/parser/syntaxes/expressions.ts | 54 ++++++++ src/parser/token.ts | 1 + 11 files changed, 370 insertions(+), 9 deletions(-) create mode 100644 src/interpreter/dic.ts create mode 100644 src/interpreter/serial-expression.ts 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/src/interpreter/dic.ts b/src/interpreter/dic.ts new file mode 100644 index 00000000..88764e92 --- /dev/null +++ b/src/interpreter/dic.ts @@ -0,0 +1,57 @@ +/* + * このコードではJavaScriptの反復処理プロトコル及びジェネレーター関数を利用しています。 + * 詳細は https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/function* + * を参照して下さい。 + */ + +import { SeriExpToken, serialize, deserialize } from './serial-expression.js' +import { Value, VFn, NULL } 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 bc77a996..08393dc8 100644 --- a/src/interpreter/index.ts +++ b/src/interpreter/index.ts @@ -6,11 +6,12 @@ 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 type { Value, VFn } from './value.js'; +import { DicNode } from './dic.js'; import type * as Ast from '../node.js'; const IRQ_RATE = 300; @@ -110,6 +111,7 @@ export class Interpreter { function nodeToJs(node: Ast.Node): any { switch (node.type) { case 'arr': return node.value.map(item => nodeToJs(item)); + case 'dic': return new DicNode(node.value.map(([key, val]) => [nodeToJs(key), nodeToJs(val)])); case 'bool': return node.value; case 'null': return null; case 'num': return node.value; @@ -437,6 +439,13 @@ export class Interpreter { case 'arr': return ARR(await Promise.all(node.value.map(item => this._eval(item, scope)))); + case 'dic': return DIC(new DicNode(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() as Map; for (const k of node.value.keys()) { @@ -475,6 +484,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}.`); } @@ -628,6 +639,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}.`); } @@ -646,6 +659,11 @@ export class Interpreter { await Promise.all([...dest.value].map( ([key, item]) => this.assign(scope, item, value.value.get(key) ?? NULL) )); + } else if (dest.type === '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) + )); } else { 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..df40fa3b --- /dev/null +++ b/src/interpreter/serial-expression.ts @@ -0,0 +1,170 @@ +/* + * 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 type { Value, VFn } from './value.js'; +import { NULL, BOOL, NUM, STR, ARR, OBJ, DIC, ERROR, RETURN, BREAK, CONTINUE } from './value.js'; +import { DicNode } from './dic.js'; +import { isFunction } from './util.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: + const mustBeNever: never = val; + throw new Error('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が残っている + // const mustBeNever: never = token; + const mustBeNever: VFn = token; + throw new Error(`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(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 f3397dfc..76a986dd 100644 --- a/src/interpreter/util.ts +++ b/src/interpreter/util.ts @@ -1,6 +1,6 @@ import { AiScriptRuntimeError } from '../error.js'; import { STR, NUM, ARR, OBJ, NULL, BOOL } from './value.js'; -import type { Value, VStr, VNum, VBool, VFn, VObj, VArr } from './value.js'; +import type { Value, VStr, VNum, VBool, VFn, VObj, VArr, VDic } from './value.js'; export function expectAny(val: Value | null | undefined): asserts val is Value { if (val == null) { @@ -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; @@ -186,6 +211,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 90c531b0..91444254 100644 --- a/src/interpreter/value.ts +++ b/src/interpreter/value.ts @@ -1,5 +1,6 @@ import type { Node } from '../node.js'; import type { Scope } from './scope.js'; +import { DicNode } from './dic.js'; export type VNull = { type: 'null'; @@ -30,6 +31,11 @@ export type VObj = { value: Map; }; +export type VDic = { + type: 'dic'; + value: DicNode; +}; + export type VFn = VUserFn | VNativeFn; type VFnBase = { type: 'fn'; @@ -80,7 +86,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, @@ -121,6 +127,11 @@ export const ARR = (arr: VArr['value']): VArr => ({ value: arr, }); +export const DIC = (dic: DicNode): VDic => ({ + type: 'dic' as const, + value: 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 caa33196..62ac8b48 100644 --- a/src/node.ts +++ b/src/node.ts @@ -128,6 +128,7 @@ export type Expression = Null | Obj | Arr | + Dic | Not | And | Or | @@ -137,7 +138,7 @@ export type Expression = Prop; const expressionTypes = [ - 'if', 'fn', 'match', 'block', 'exists', 'tmpl', 'str', 'num', 'bool', 'null', 'obj', 'arr', 'not', 'and', 'or', 'identifier', 'call', 'index', 'prop', + 'if', 'fn', 'match', 'block', 'exists', 'tmpl', 'str', 'num', 'bool', 'null', 'obj', 'arr', 'dic', 'not', 'and', 'or', 'identifier', 'call', 'index', 'prop', ]; export function isExpression(x: Node): x is Expression { return expressionTypes.includes(x.type); @@ -235,6 +236,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 a179aba7..edff1fc4 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', 'do', 'enum', 'export', diff --git a/src/parser/scanner.ts b/src/parser/scanner.ts index 3b3aa6cd..ce79e9e9 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, loc, { hasLeftSpacing }); } + case 'dic': { + return TOKEN(TokenKind.DicKeyword, loc, { hasLeftSpacing }); + } default: { return TOKEN(TokenKind.Identifier, loc, { hasLeftSpacing, value }); } diff --git a/src/parser/syntaxes/expressions.ts b/src/parser/syntaxes/expressions.ts index 439b8fc1..f8a0fd57 100644 --- a/src/parser/syntaxes/expressions.ts +++ b/src/parser/syntaxes/expressions.ts @@ -272,6 +272,9 @@ function parseAtom(s: ITokenStream, isStatic: boolean): Ast.Node { case TokenKind.OpenBracket: { return parseArray(s, isStatic); } + case TokenKind.DicKeyword: { + return parseDictionary(s, isStatic); + } case TokenKind.Identifier: { if (isStatic) break; return parseReference(s); @@ -621,6 +624,57 @@ function parseArray(s: ITokenStream, isStatic: boolean): Ast.Node { return NODE('arr', { value }, loc); } +/** + * ```abnf + * Dictionary = "dic" "{" ["[" Expr "]" ":" Expr *(SEP Expr ":" Expr) [SEP]] "}" + * ``` +*/ +function parseDictionary(s: ITokenStream, isStatic: boolean): Ast.Node { + const loc = s.token.loc; + + s.nextWith(TokenKind.DicKeyword); + s.nextWith(TokenKind.OpenBrace); + + while (s.getKind() === TokenKind.NewLine) { + s.next(); + } + + const value: [Ast.Node, Ast.Node][] = []; + while (s.getKind() !== TokenKind.CloseBrace) { + s.nextWith(TokenKind.OpenBracket); + const k = parseExpr(s, isStatic); + s.nextWith(TokenKind.CloseBracket); + + s.nextWith(TokenKind.Colon); + + const v = parseExpr(s, isStatic); + + value.push([k, v]); + + // separator + switch (s.getKind()) { + case TokenKind.NewLine: + case TokenKind.Comma: { + s.next(); + while (s.getKind() === TokenKind.NewLine) { + s.next(); + } + break; + } + case TokenKind.CloseBrace: { + break; + } + default: { + throw new AiScriptSyntaxError('separator expected', s.token.loc); + } + } + } + + s.nextWith(TokenKind.CloseBrace); + + return NODE('dic', { value }, loc); +} + //#region Pratt parsing type PrefixInfo = { opKind: 'prefix', kind: TokenKind, bp: number }; diff --git a/src/parser/token.ts b/src/parser/token.ts index 67aca6b6..09b61828 100644 --- a/src/parser/token.ts +++ b/src/parser/token.ts @@ -32,6 +32,7 @@ export enum TokenKind { VarKeyword, LetKeyword, ExistsKeyword, + DicKeyword, /** "!" */ Not, From 0c6050509e52413bf6b351c2b7121058d1dc6d80 Mon Sep 17 00:00:00 2001 From: Fine Archs Date: Thu, 6 Jun 2024 01:04:11 +0900 Subject: [PATCH 02/11] lint and api --- etc/aiscript.api.md | 36 ++++++++++++++++++++++++++-- src/interpreter/dic.ts | 7 +++--- src/interpreter/index.ts | 10 ++++---- src/interpreter/primitive-props.ts | 10 ++++---- src/interpreter/serial-expression.ts | 27 ++++++++++----------- src/interpreter/util.ts | 2 +- src/interpreter/value.ts | 2 +- src/utils/mustbenever.ts | 7 ++++++ 8 files changed, 69 insertions(+), 32 deletions(-) create mode 100644 src/utils/mustbenever.ts diff --git a/etc/aiscript.api.md b/etc/aiscript.api.md index caf7f7e2..d2895c91 100644 --- a/etc/aiscript.api.md +++ b/etc/aiscript.api.md @@ -108,6 +108,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'; @@ -151,6 +156,7 @@ declare namespace Ast { Null, Obj, Arr, + Dic, Identifier, Call, Index, @@ -225,6 +231,17 @@ type Definition = NodeBase & { attr: Attribute[]; }; +// Warning: (ae-forgotten-export) The symbol "DicNode" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +const DIC: (dic: DicNode) => VDic; + +// @public (undocumented) +type Dic = NodeBase & { + type: 'dic'; + value: [Expression, Expression][]; +}; + // @public (undocumented) type Each = NodeBase & { type: 'each'; @@ -263,7 +280,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 | And | Or | Identifier | Call | Index | Prop; +type Expression = If | Fn | Match | Block | Exists | Tmpl | Str | Num | Bool | Null | Obj | Arr | Dic | Not | And | Or | Identifier | Call | Index | Prop; // @public (undocumented) const FALSE: { @@ -384,6 +401,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: any): Value; @@ -590,12 +612,14 @@ declare namespace utils { assertNumber, assertObject, assertArray, + assertValue, isBoolean, isFunction, isString, isNumber, isObject, isArray, + isValue, eq, valToString, valToJs, @@ -613,7 +637,7 @@ function valToJs(val: Value): any; 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 { @@ -623,6 +647,7 @@ declare namespace values { VStr, VArr, VObj, + VDic, VFn, VUserFn, VNativeFn, @@ -640,6 +665,7 @@ declare namespace values { BOOL, OBJ, ARR, + DIC, FN, FN_NATIVE, RETURN, @@ -675,6 +701,12 @@ type VContinue = { value: null; }; +// @public (undocumented) +type VDic = { + type: 'dic'; + value: DicNode; +}; + // @public (undocumented) type VError = { type: 'error'; diff --git a/src/interpreter/dic.ts b/src/interpreter/dic.ts index 88764e92..af073ced 100644 --- a/src/interpreter/dic.ts +++ b/src/interpreter/dic.ts @@ -4,9 +4,10 @@ * を参照して下さい。 */ -import { SeriExpToken, serialize, deserialize } from './serial-expression.js' -import { Value, VFn, NULL } from './value.js'; - +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 { diff --git a/src/interpreter/index.ts b/src/interpreter/index.ts index a791482a..1ac03f63 100644 --- a/src/interpreter/index.ts +++ b/src/interpreter/index.ts @@ -10,8 +10,8 @@ import { assertNumber, assertString, assertFunction, assertBoolean, assertObject 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 type { Value, VFn } from './value.js'; import { DicNode } from './dic.js'; +import type { Value, VFn } from './value.js'; import type * as Ast from '../node.js'; const IRQ_RATE = 300; @@ -443,7 +443,7 @@ export class Interpreter { node.value.map(async ([key, val]) => await Promise.all([ this._eval(key, scope), this._eval(val, scope), - ])) + ])), ))); case 'obj': { @@ -652,17 +652,17 @@ export class Interpreter { } else if (dest.type === 'arr') { assertArray(value); await Promise.all(dest.value.map( - (item, index) => this.assign(scope, item, value.value[index] ?? NULL) + (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) + ([key, item]) => this.assign(scope, item, value.value.get(key) ?? NULL), )); } else if (dest.type === '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) + async ([key, item]) => this.assign(scope, item, value.value.get(await this._eval(key, scope)) ?? NULL), )); } else { 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/primitive-props.ts b/src/interpreter/primitive-props.ts index 90cc65d4..7bb0b457 100644 --- a/src/interpreter/primitive-props.ts +++ b/src/interpreter/primitive-props.ts @@ -334,12 +334,12 @@ const PRIMITIVE_PROPS: { splice: (target: VArr): VFn => FN_NATIVE(async ([idx, rc, vs], opts) => { assertNumber(idx); const index = (idx.value < -target.value.length) ? 0 - : (idx.value < 0) ? target.value.length + idx.value - : (idx.value >= target.value.length) ? target.value.length - : idx.value; + : (idx.value < 0) ? target.value.length + idx.value + : (idx.value >= target.value.length) ? target.value.length + : idx.value; const remove_count = (rc != null) ? (assertNumber(rc), rc.value) - : target.value.length - index; + : target.value.length - index; const items = (vs != null) ? (assertArray(vs), vs.value) : []; @@ -378,7 +378,7 @@ const PRIMITIVE_PROPS: { }); const mapped_vals = await Promise.all(vals); return ARR(mapped_vals.flat()); - }), + }), every: (target: VArr): VFn => FN_NATIVE(async ([fn], opts) => { assertFunction(fn); diff --git a/src/interpreter/serial-expression.ts b/src/interpreter/serial-expression.ts index df40fa3b..62b258d6 100644 --- a/src/interpreter/serial-expression.ts +++ b/src/interpreter/serial-expression.ts @@ -10,10 +10,10 @@ // TODO: ループ構造対策 -import type { Value, VFn } from './value.js'; +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 { isFunction } from './util.js'; +import type { Value, VFn } from './value.js'; export type SeriExpToken = | null @@ -59,7 +59,7 @@ export function* serialize(val: Value): Generator case 'error': yield SeriExpSymbols[val.type]; yield* serialize(val.info ?? NULL); - break; + break; case 'arr': yield SeriExpSymbols[val.type]; for (const v of val.value) yield* serialize(v); @@ -82,8 +82,7 @@ export function* serialize(val: Value): Generator yield SeriExpSymbols.end; break; default: - const mustBeNever: never = val; - throw new Error('unknown type'); + mustBeNever(val, 'serializing unknown type'); } } @@ -106,15 +105,15 @@ function deserializeInnerValueOrEnd(iterator: Iterator): Value | t const nextValueOrEnd = () => deserializeInnerValueOrEnd(iterator); const nextString = () => { const token = nextStringOrEnd(); - if (typeof token !== 'string') throw new Error(`unexpected token of serial expression: end`); + 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); @@ -127,9 +126,7 @@ function deserializeInnerValueOrEnd(iterator: Iterator): Value | t if (typeof token !== 'symbol') { // 網羅性チェック、何故かVFnが残っている - // const mustBeNever: never = token; - const mustBeNever: VFn = token; - throw new Error(`unknown SeriExpToken type: ${token}`); + mustBeNever(token, `unknown SeriExpToken type: ${token}`); } switch (token) { @@ -141,8 +138,8 @@ function deserializeInnerValueOrEnd(iterator: Iterator): Value | t nextValue(), ); case SeriExpSymbols.arr: { - const elems: Value[] = [] - while(true) { + const elems: Value[] = []; + while (true) { const valueOrEnd = nextValueOrEnd(); if (valueOrEnd === END) return ARR(elems); elems.push(valueOrEnd); @@ -150,7 +147,7 @@ function deserializeInnerValueOrEnd(iterator: Iterator): Value | t } case SeriExpSymbols.obj: { const elems = new Map(); - while(true) { + while (true) { const key = nextStringOrEnd(); if (key === SeriExpSymbols.end) return OBJ(elems); elems.set(key, nextValue()); @@ -158,7 +155,7 @@ function deserializeInnerValueOrEnd(iterator: Iterator): Value | t } case SeriExpSymbols.dic: { const elems = new DicNode(); - while(true) { + while (true) { const key = nextValueOrEnd(); if (key === END) return DIC(elems); elems.set(key, nextValue()); diff --git a/src/interpreter/util.ts b/src/interpreter/util.ts index 76a986dd..46585044 100644 --- a/src/interpreter/util.ts +++ b/src/interpreter/util.ts @@ -1,6 +1,6 @@ import { AiScriptRuntimeError } from '../error.js'; import { STR, NUM, ARR, OBJ, NULL, BOOL } from './value.js'; -import type { Value, VStr, VNum, VBool, VFn, VObj, VArr, VDic } from './value.js'; +import type { Value, VStr, VNum, VBool, VFn, VObj, VArr } from './value.js'; export function expectAny(val: Value | null | undefined): asserts val is Value { if (val == null) { diff --git a/src/interpreter/value.ts b/src/interpreter/value.ts index 91444254..2b3110a0 100644 --- a/src/interpreter/value.ts +++ b/src/interpreter/value.ts @@ -1,6 +1,6 @@ +import type { DicNode } from './dic.js'; import type { Node } from '../node.js'; import type { Scope } from './scope.js'; -import { DicNode } from './dic.js'; export type VNull = { type: 'null'; diff --git a/src/utils/mustbenever.ts b/src/utils/mustbenever.ts new file mode 100644 index 00000000..b39f1176 --- /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 any ? 0 : never]; From 8d7e44620c4af509e584d15c1a76387d71245856 Mon Sep 17 00:00:00 2001 From: Fine Archs Date: Mon, 17 Jun 2024 08:51:47 +0900 Subject: [PATCH 03/11] change api --- etc/aiscript.api.md | 29 +++++++++++++++++++++++++--- src/interpreter/index.ts | 4 ++-- src/interpreter/serial-expression.ts | 2 +- src/interpreter/value.ts | 17 +++++++++++----- 4 files changed, 41 insertions(+), 11 deletions(-) diff --git a/etc/aiscript.api.md b/etc/aiscript.api.md index d2895c91..a0881af9 100644 --- a/etc/aiscript.api.md +++ b/etc/aiscript.api.md @@ -231,10 +231,11 @@ type Definition = NodeBase & { attr: Attribute[]; }; -// Warning: (ae-forgotten-export) The symbol "DicNode" needs to be exported by the entry point index.d.ts -// // @public (undocumented) -const DIC: (dic: DicNode) => VDic; +const DIC: { + fromNode: (dic: DicNode) => VDic; + fromEntries: (kvs?: [Value, Value][] | undefined) => VDic; +}; // @public (undocumented) type Dic = NodeBase & { @@ -242,6 +243,27 @@ type Dic = NodeBase & { 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 Each = NodeBase & { type: 'each'; @@ -641,6 +663,7 @@ type Value = (VNull | VBool | VNum | VStr | VArr | VObj | VDic | VFn | VReturn | declare namespace values { export { + DicNode, VNull, VBool, VNum, diff --git a/src/interpreter/index.ts b/src/interpreter/index.ts index 1ac03f63..2920ac5e 100644 --- a/src/interpreter/index.ts +++ b/src/interpreter/index.ts @@ -439,12 +439,12 @@ export class Interpreter { case 'arr': return ARR(await Promise.all(node.value.map(item => this._eval(item, scope)))); - case 'dic': return DIC(new DicNode(await Promise.all( + 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() as Map; diff --git a/src/interpreter/serial-expression.ts b/src/interpreter/serial-expression.ts index 62b258d6..6ab6a6ca 100644 --- a/src/interpreter/serial-expression.ts +++ b/src/interpreter/serial-expression.ts @@ -157,7 +157,7 @@ function deserializeInnerValueOrEnd(iterator: Iterator): Value | t const elems = new DicNode(); while (true) { const key = nextValueOrEnd(); - if (key === END) return DIC(elems); + if (key === END) return DIC.fromNode(elems); elems.set(key, nextValue()); } } diff --git a/src/interpreter/value.ts b/src/interpreter/value.ts index 2b3110a0..29b04f79 100644 --- a/src/interpreter/value.ts +++ b/src/interpreter/value.ts @@ -1,4 +1,5 @@ -import type { DicNode } from './dic.js'; +import { DicNode } from './dic.js'; +export { DicNode }; import type { Node } from '../node.js'; import type { Scope } from './scope.js'; @@ -127,10 +128,16 @@ export const ARR = (arr: VArr['value']): VArr => ({ value: arr, }); -export const DIC = (dic: DicNode): VDic => ({ - type: 'dic' as const, - value: dic, -}); +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, From 06ed2e79258ad0359fdc205030c8010a8614a438 Mon Sep 17 00:00:00 2001 From: Fine Archs Date: Wed, 19 Jun 2024 19:01:50 +0900 Subject: [PATCH 04/11] add test --- test/index.ts | 100 ++++++++++++++++++++++++++++++++++++++++++- test/literals.ts | 107 ++++++++++++++++------------------------------- 2 files changed, 134 insertions(+), 73 deletions(-) diff --git a/test/index.ts b/test/index.ts index 22baee6d..4dbf957a 100644 --- a/test/index.ts +++ b/test/index.ts @@ -6,7 +6,7 @@ import * as assert from 'assert'; import { expect, test } from '@jest/globals'; import { Parser, Interpreter, utils, errors, 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'; @@ -321,6 +321,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 77a8ab33..a3f4945c 100644 --- a/test/literals.ts +++ b/test/literals.ts @@ -1,7 +1,7 @@ import * as assert from 'assert'; import { expect, test } from '@jest/globals'; 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(` <: { From 3a9c869cf5ef071c9242beb60bab0d5ad740c0d6 Mon Sep 17 00:00:00 2001 From: Fine Archs Date: Wed, 19 Jun 2024 20:12:44 +0900 Subject: [PATCH 05/11] api report --- etc/aiscript.api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/etc/aiscript.api.md b/etc/aiscript.api.md index f76bfafe..31440235 100644 --- a/etc/aiscript.api.md +++ b/etc/aiscript.api.md @@ -801,7 +801,7 @@ type VUserFn = VFnBase & { // 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 +// 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) From cc454e5205a2b378beae06fa4c7420f9126af798 Mon Sep 17 00:00:00 2001 From: FineArchs <133759614+FineArchs@users.noreply.github.com> Date: Thu, 20 Jun 2024 22:11:43 +0900 Subject: [PATCH 06/11] Update get-started.md --- docs/get-started.md | 42 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/docs/get-started.md b/docs/get-started.md index cf2f975d..9e0f90c3 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 +``` + ## 演算 演算は、 ``` From 474cc10db9d5e5ee0ea7e2273b11d88c80465084 Mon Sep 17 00:00:00 2001 From: FineArchs <133759614+FineArchs@users.noreply.github.com> Date: Thu, 20 Jun 2024 22:19:07 +0900 Subject: [PATCH 07/11] Update keywords.md --- docs/keywords.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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` From 53883bae36134799531d3da78b88bf389859790e Mon Sep 17 00:00:00 2001 From: FineArchs <133759614+FineArchs@users.noreply.github.com> Date: Fri, 21 Jun 2024 20:47:35 +0900 Subject: [PATCH 08/11] Update literals.md --- docs/literals.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/literals.md b/docs/literals.md index 604440f3..4238483a 100644 --- a/docs/literals.md +++ b/docs/literals.md @@ -94,6 +94,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 From 0879f13184f30a66b0a12eb01520d1901d4677fd Mon Sep 17 00:00:00 2001 From: FineArchs Date: Tue, 30 Jul 2024 17:28:40 +0900 Subject: [PATCH 09/11] loc -> pos --- src/parser/scanner.ts | 2 +- src/parser/syntaxes/expressions.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/parser/scanner.ts b/src/parser/scanner.ts index c86d73ad..e6d3a213 100644 --- a/src/parser/scanner.ts +++ b/src/parser/scanner.ts @@ -425,7 +425,7 @@ export class Scanner implements ITokenStream { return TOKEN(TokenKind.ExistsKeyword, pos, { hasLeftSpacing }); } case 'dic': { - return TOKEN(TokenKind.DicKeyword, loc, { hasLeftSpacing }); + 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 2e46fb35..d1d158bd 100644 --- a/src/parser/syntaxes/expressions.ts +++ b/src/parser/syntaxes/expressions.ts @@ -637,7 +637,7 @@ function parseArray(s: ITokenStream, isStatic: boolean): Ast.Node { * ``` */ function parseDictionary(s: ITokenStream, isStatic: boolean): Ast.Node { - const loc = s.token.loc; + const pos = s.token.pos; s.nextWith(TokenKind.DicKeyword); s.nextWith(TokenKind.OpenBrace); @@ -672,14 +672,14 @@ function parseDictionary(s: ITokenStream, isStatic: boolean): Ast.Node { break; } default: { - throw new AiScriptSyntaxError('separator expected', s.token.loc); + throw new AiScriptSyntaxError('separator expected', s.token.pos); } } } s.nextWith(TokenKind.CloseBrace); - return NODE('dic', { value }, loc); + return NODE('dic', { value }, pos); } //#region Pratt parsing From 0a0179858c2bc8469567d56a2e53647275967afd Mon Sep 17 00:00:00 2001 From: FineArchs Date: Tue, 30 Jul 2024 17:36:44 +0900 Subject: [PATCH 10/11] add end pos --- src/parser/syntaxes/expressions.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/parser/syntaxes/expressions.ts b/src/parser/syntaxes/expressions.ts index d1d158bd..f6957e7b 100644 --- a/src/parser/syntaxes/expressions.ts +++ b/src/parser/syntaxes/expressions.ts @@ -637,7 +637,7 @@ function parseArray(s: ITokenStream, isStatic: boolean): Ast.Node { * ``` */ function parseDictionary(s: ITokenStream, isStatic: boolean): Ast.Node { - const pos = s.token.pos; + const startPos = s.token.pos; s.nextWith(TokenKind.DicKeyword); s.nextWith(TokenKind.OpenBrace); @@ -679,7 +679,7 @@ function parseDictionary(s: ITokenStream, isStatic: boolean): Ast.Node { s.nextWith(TokenKind.CloseBrace); - return NODE('dic', { value }, pos); + return NODE('dic', { value }, startPos, s.getPos()); } //#region Pratt parsing From f4a2cb2a41585e726727bcc475f9f4ec706d2940 Mon Sep 17 00:00:00 2001 From: FineArchs Date: Wed, 18 Sep 2024 11:00:30 +0900 Subject: [PATCH 11/11] reset unnecessary change --- src/interpreter/primitive-props.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/interpreter/primitive-props.ts b/src/interpreter/primitive-props.ts index df3c18cc..bc3541b0 100644 --- a/src/interpreter/primitive-props.ts +++ b/src/interpreter/primitive-props.ts @@ -334,12 +334,12 @@ const PRIMITIVE_PROPS: { splice: (target: VArr): VFn => FN_NATIVE(async ([idx, rc, vs], opts) => { assertNumber(idx); const index = (idx.value < -target.value.length) ? 0 - : (idx.value < 0) ? target.value.length + idx.value - : (idx.value >= target.value.length) ? target.value.length - : idx.value; + : (idx.value < 0) ? target.value.length + idx.value + : (idx.value >= target.value.length) ? target.value.length + : idx.value; const remove_count = (rc != null) ? (assertNumber(rc), rc.value) - : target.value.length - index; + : target.value.length - index; const items = (vs != null) ? (assertArray(vs), vs.value) : []; @@ -378,7 +378,7 @@ const PRIMITIVE_PROPS: { }); const mapped_vals = await Promise.all(vals); return ARR(mapped_vals.flat()); - }), + }), every: (target: VArr): VFn => FN_NATIVE(async ([fn], opts) => { assertFunction(fn);