diff --git a/etc/aiscript.api.md b/etc/aiscript.api.md index 4974625d..648af961 100644 --- a/etc/aiscript.api.md +++ b/etc/aiscript.api.md @@ -21,7 +21,7 @@ type AddAssign = NodeBase & { }; // @public (undocumented) -export const AISCRIPT_VERSION: "1.3.0"; +export const AISCRIPT_VERSION: "2.0.0"; // @public (undocumented) abstract class AiScriptError extends Error { @@ -416,7 +416,7 @@ type Index = NodeBase & { // @public (undocumented) export class Interpreter { - constructor(consts: Record, opts?: { + constructor(globals: Record, opts?: { in?(q: string): Promise; out?(value: Value): void; err?(e: AiScriptError): void; @@ -713,6 +713,9 @@ export class Scope { // @public (undocumented) type Statement = Definition | Return | Each | For | Loop | Break | Continue | Assign | AddAssign | SubAssign; +// @public (undocumented) +export const std: Record; + // @public (undocumented) const STR: (str: VStr["value"]) => VStr; @@ -918,7 +921,7 @@ type VUserFn = VFnBase & { // Warnings were encountered during analysis: // -// src/interpreter/index.ts:49:4 - (ae-forgotten-export) The symbol "LogObject" needs to be exported by the entry point index.d.ts +// src/interpreter/index.ts:50:4 - (ae-forgotten-export) The symbol "LogObject" needs to be exported by the entry point index.d.ts // src/interpreter/value.ts:47:2 - (ae-forgotten-export) The symbol "Type" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/package-lock.json b/package-lock.json index 67c4143d..cf44386e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@syuilo/aiscript", - "version": "1.3.0", + "version": "2.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@syuilo/aiscript", - "version": "1.3.0", + "version": "2.0.0", "license": "MIT", "dependencies": { "seedrandom": "3.0.5", diff --git a/package.json b/package.json index de429f6e..889c8f8d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "type": "module", "name": "@syuilo/aiscript", - "version": "1.3.0", + "version": "2.0.0", "description": "AiScript implementation", "author": "syuilo ", "license": "MIT", diff --git a/src/index.ts b/src/index.ts index ee600c18..0bdc3994 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,7 +2,7 @@ //export * from './interpreter/index'; //export * as utils from './interpreter/util'; //export * as values from './interpreter/value'; -import { Interpreter } from './interpreter/index.js'; +import { Interpreter, std } from './interpreter/index.js'; import { Scope } from './interpreter/scope.js'; import * as utils from './interpreter/util.js'; import * as values from './interpreter/value.js'; @@ -12,7 +12,7 @@ import * as errors from './error.js'; import * as Ast from './node.js'; import { AISCRIPT_VERSION } from './constants.js'; import type { ParserPlugin, PluginType } from './parser/index.js'; -export { Interpreter }; +export { Interpreter, std }; export { Scope }; export { utils }; export { values }; diff --git a/src/interpreter/index.ts b/src/interpreter/index.ts index 1ea3d16c..b560cbc3 100644 --- a/src/interpreter/index.ts +++ b/src/interpreter/index.ts @@ -7,16 +7,18 @@ import { AiScriptError, NonAiScriptError, AiScriptNamespaceError, AiScriptIndexO import * as Ast from '../node.js'; import { nodeToJs } from '../utils/node-to-js.js'; import { Scope } from './scope.js'; -import { std } from './lib/std.js'; import { RETURN, unWrapRet, BREAK, CONTINUE, assertValue, isControl, type Control, unWrapLabeledBreak } from './control.js'; import { assertNumber, assertString, assertFunction, assertBoolean, assertObject, assertArray, eq, isObject, isArray, expectAny, reprValue, isFunction } from './util.js'; import { NULL, FN_NATIVE, BOOL, NUM, STR, ARR, OBJ, FN, ERROR } from './value.js'; import { getPrimProp } from './primitive-props.js'; import { Variable } from './variable.js'; import { Reference } from './reference.js'; +import { stdCore } from './lib/core.js'; import type { JsValue } from './util.js'; import type { Value, VFn, VUserFn } from './value.js'; +export { std } from './lib/std.js'; + export type LogObject = { scope?: string; var?: string; @@ -36,12 +38,11 @@ export class Interpreter { private abortHandlers: (() => void)[] = []; private pauseHandlers: (() => void)[] = []; private unpauseHandlers: (() => void)[] = []; - private vars: Record = {}; private irqRate: number; private irqSleep: () => Promise; constructor( - consts: Record, + globals: Record, private opts: { in?(q: string): Promise; out?(value: Value): void; @@ -67,13 +68,12 @@ export class Interpreter { }), }; - this.vars = Object.fromEntries(Object.entries({ - ...consts, - ...std, - ...io, - }).map(([k, v]) => [k, Variable.const(v)])); - - this.scope = new Scope([new Map(Object.entries(this.vars))]); + this.scope = new Scope([ + new Map( + Object.entries({ ...globals,...stdCore, ...io }) + .map(([k, v]) => [k, Variable.const(v)]), + ), + ]); this.scope.opts.log = (type, params): void => { switch (type) { case 'add': this.log('var:add', params); break; diff --git a/src/interpreter/lib/core.ts b/src/interpreter/lib/core.ts new file mode 100644 index 00000000..fa21c0bd --- /dev/null +++ b/src/interpreter/lib/core.ts @@ -0,0 +1,138 @@ +import { NUM, STR, FN_NATIVE, FALSE, TRUE, ARR, NULL } from '../value.js'; +import { assertNumber, assertString, assertBoolean, eq, expectAny, reprValue } from '../util.js'; +import { AiScriptUserError } from '../../error.js'; +import { AISCRIPT_VERSION } from '../../constants.js'; +import type { Value } from '../value.js'; + +export const stdCore: Record<`Core:${string}`, Value> = { + 'Core:v': STR(AISCRIPT_VERSION), + + 'Core:ai': STR('kawaii'), + + 'Core:not': FN_NATIVE(([a]) => { + assertBoolean(a); + return a.value ? FALSE : TRUE; + }), + + 'Core:eq': FN_NATIVE(([a, b]) => { + expectAny(a); + expectAny(b); + return eq(a, b) ? TRUE : FALSE; + }), + + 'Core:neq': FN_NATIVE(([a, b]) => { + expectAny(a); + expectAny(b); + return eq(a, b) ? FALSE : TRUE; + }), + + 'Core:and': FN_NATIVE(([a, b]) => { + assertBoolean(a); + if (!a.value) return FALSE; + assertBoolean(b); + return b.value ? TRUE : FALSE; + }), + + 'Core:or': FN_NATIVE(([a, b]) => { + assertBoolean(a); + if (a.value) return TRUE; + assertBoolean(b); + return b.value ? TRUE : FALSE; + }), + + 'Core:add': FN_NATIVE(([a, b]) => { + assertNumber(a); + assertNumber(b); + return NUM(a.value + b.value); + }), + + 'Core:sub': FN_NATIVE(([a, b]) => { + assertNumber(a); + assertNumber(b); + return NUM(a.value - b.value); + }), + + 'Core:mul': FN_NATIVE(([a, b]) => { + assertNumber(a); + assertNumber(b); + return NUM(a.value * b.value); + }), + + 'Core:pow': FN_NATIVE(([a, b]) => { + assertNumber(a); + assertNumber(b); + const res = a.value ** b.value; + return NUM(res); + }), + + 'Core:div': FN_NATIVE(([a, b]) => { + assertNumber(a); + assertNumber(b); + const res = a.value / b.value; + return NUM(res); + }), + + 'Core:mod': FN_NATIVE(([a, b]) => { + assertNumber(a); + assertNumber(b); + return NUM(a.value % b.value); + }), + + 'Core:gt': FN_NATIVE(([a, b]) => { + assertNumber(a); + assertNumber(b); + return a.value > b.value ? TRUE : FALSE; + }), + + 'Core:lt': FN_NATIVE(([a, b]) => { + assertNumber(a); + assertNumber(b); + return a.value < b.value ? TRUE : FALSE; + }), + + 'Core:gteq': FN_NATIVE(([a, b]) => { + assertNumber(a); + assertNumber(b); + return a.value >= b.value ? TRUE : FALSE; + }), + + 'Core:lteq': FN_NATIVE(([a, b]) => { + assertNumber(a); + assertNumber(b); + return a.value <= b.value ? TRUE : FALSE; + }), + + 'Core:type': FN_NATIVE(([v]) => { + expectAny(v); + return STR(v.type); + }), + + 'Core:to_str': FN_NATIVE(([v]) => { + expectAny(v); + + return STR(reprValue(v)); + }), + + 'Core:range': FN_NATIVE(([a, b]) => { + assertNumber(a); + assertNumber(b); + if (a.value < b.value) { + return ARR(Array.from({ length: (b.value - a.value) + 1 }, (_, i) => NUM(i + a.value))); + } else if (a.value > b.value) { + return ARR(Array.from({ length: (a.value - b.value) + 1 }, (_, i) => NUM(a.value - i))); + } else { + return ARR([a]); + } + }), + + 'Core:sleep': FN_NATIVE(async ([delay]) => { + assertNumber(delay); + await new Promise((r) => setTimeout(r, delay.value)); + return NULL; + }), + + 'Core:abort': FN_NATIVE(async ([message]) => { + assertString(message); + throw new AiScriptUserError(message.value); + }), +}; diff --git a/src/interpreter/lib/std.ts b/src/interpreter/lib/std.ts index 79a3c1f7..68396adb 100644 --- a/src/interpreter/lib/std.ts +++ b/src/interpreter/lib/std.ts @@ -1,9 +1,8 @@ /* eslint-disable no-empty-pattern */ import { v4 as uuid } from 'uuid'; -import { NUM, STR, FN_NATIVE, FALSE, TRUE, ARR, NULL, BOOL, OBJ, ERROR } from '../value.js'; -import { assertNumber, assertString, assertBoolean, valToJs, jsToVal, assertFunction, assertObject, eq, expectAny, assertArray, reprValue } from '../util.js'; -import { AiScriptRuntimeError, AiScriptUserError } from '../../error.js'; -import { AISCRIPT_VERSION } from '../../constants.js'; +import { NUM, STR, FN_NATIVE, ARR, NULL, BOOL, OBJ, ERROR } from '../value.js'; +import { assertNumber, assertString, assertBoolean, valToJs, jsToVal, assertFunction, assertObject, expectAny, assertArray } from '../util.js'; +import { AiScriptRuntimeError } from '../../error.js'; import { textDecoder } from '../../const.js'; import { stdMath } from './math.js'; import type { Value } from '../value.js'; @@ -13,137 +12,6 @@ export const std: Record = { 'help': STR('SEE: https://aiscript-dev.github.io/guides/get-started.html'), - //#region Core - 'Core:v': STR(AISCRIPT_VERSION), - - 'Core:ai': STR('kawaii'), - - 'Core:not': FN_NATIVE(([a]) => { - assertBoolean(a); - return a.value ? FALSE : TRUE; - }), - - 'Core:eq': FN_NATIVE(([a, b]) => { - expectAny(a); - expectAny(b); - return eq(a, b) ? TRUE : FALSE; - }), - - 'Core:neq': FN_NATIVE(([a, b]) => { - expectAny(a); - expectAny(b); - return eq(a, b) ? FALSE : TRUE; - }), - - 'Core:and': FN_NATIVE(([a, b]) => { - assertBoolean(a); - if (!a.value) return FALSE; - assertBoolean(b); - return b.value ? TRUE : FALSE; - }), - - 'Core:or': FN_NATIVE(([a, b]) => { - assertBoolean(a); - if (a.value) return TRUE; - assertBoolean(b); - return b.value ? TRUE : FALSE; - }), - - 'Core:add': FN_NATIVE(([a, b]) => { - assertNumber(a); - assertNumber(b); - return NUM(a.value + b.value); - }), - - 'Core:sub': FN_NATIVE(([a, b]) => { - assertNumber(a); - assertNumber(b); - return NUM(a.value - b.value); - }), - - 'Core:mul': FN_NATIVE(([a, b]) => { - assertNumber(a); - assertNumber(b); - return NUM(a.value * b.value); - }), - - 'Core:pow': FN_NATIVE(([a, b]) => { - assertNumber(a); - assertNumber(b); - const res = a.value ** b.value; - return NUM(res); - }), - - 'Core:div': FN_NATIVE(([a, b]) => { - assertNumber(a); - assertNumber(b); - const res = a.value / b.value; - return NUM(res); - }), - - 'Core:mod': FN_NATIVE(([a, b]) => { - assertNumber(a); - assertNumber(b); - return NUM(a.value % b.value); - }), - - 'Core:gt': FN_NATIVE(([a, b]) => { - assertNumber(a); - assertNumber(b); - return a.value > b.value ? TRUE : FALSE; - }), - - 'Core:lt': FN_NATIVE(([a, b]) => { - assertNumber(a); - assertNumber(b); - return a.value < b.value ? TRUE : FALSE; - }), - - 'Core:gteq': FN_NATIVE(([a, b]) => { - assertNumber(a); - assertNumber(b); - return a.value >= b.value ? TRUE : FALSE; - }), - - 'Core:lteq': FN_NATIVE(([a, b]) => { - assertNumber(a); - assertNumber(b); - return a.value <= b.value ? TRUE : FALSE; - }), - - 'Core:type': FN_NATIVE(([v]) => { - expectAny(v); - return STR(v.type); - }), - - 'Core:to_str': FN_NATIVE(([v]) => { - expectAny(v); - - return STR(reprValue(v)); - }), - - 'Core:range': FN_NATIVE(([a, b]) => { - assertNumber(a); - assertNumber(b); - if (a.value < b.value) { - return ARR(Array.from({ length: (b.value - a.value) + 1 }, (_, i) => NUM(i + a.value))); - } else if (a.value > b.value) { - return ARR(Array.from({ length: (a.value - b.value) + 1 }, (_, i) => NUM(a.value - i))); - } else { - return ARR([a]); - } - }), - 'Core:sleep': FN_NATIVE(async ([delay]) => { - assertNumber(delay); - await new Promise((r) => setTimeout(r, delay.value)); - return NULL; - }), - 'Core:abort': FN_NATIVE(async ([message]) => { - assertString(message); - throw new AiScriptUserError(message.value); - }), - //#endregion - //#region Util 'Util:uuid': FN_NATIVE(() => { return STR(uuid()); @@ -318,12 +186,12 @@ export const std: Record = { 'Uri:encode_full': FN_NATIVE(([v]) => { assertString(v); return STR(encodeURI(v.value)); - }), + }), 'Uri:encode_component': FN_NATIVE(([v]) => { assertString(v); return STR(encodeURIComponent(v.value)); - }), + }), 'Uri:decode_full': FN_NATIVE(([v]) => { assertString(v); diff --git a/src/parser/scanner.ts b/src/parser/scanner.ts index 21a777e1..add2cc1a 100644 --- a/src/parser/scanner.ts +++ b/src/parser/scanner.ts @@ -1,5 +1,5 @@ import { AiScriptSyntaxError, AiScriptUnexpectedEOFError } from '../error.js'; -import { decodeUnicodeEscapeSequence } from '../utils/characters.js'; +import { decodeUnicodeEscapeSequence, tryDecodeSingleEscapeCharacter } from '../utils/characters.js'; import { CharStream } from './streams/char-stream.js'; import { TOKEN, TokenKind } from './token.js'; import { unexpectedTokenError } from './utils.js'; @@ -463,6 +463,25 @@ export class Scanner implements ITokenStream { return; } + private decodeEscapeSequence(): string { + if (this.stream.eof) { + throw new AiScriptUnexpectedEOFError(this.stream.getPos()); + } + + if (this.stream.char === 'u') { + const unicodeEscapeSequence = this.readUnicodeEscapeSequence(); + return String.fromCharCode(Number.parseInt(unicodeEscapeSequence.slice(1), 16)); + } + + const decodedSingleEscapeCharacter = tryDecodeSingleEscapeCharacter(this.stream.char); + if (decodedSingleEscapeCharacter != null) { + this.stream.next(); + return decodedSingleEscapeCharacter; + } + + throw new AiScriptSyntaxError(`invalid escape character: "${this.stream.char}"`, this.stream.getPos()); + } + private readUnicodeEscapeSequence(): `u${string}` { if (this.stream.eof || (this.stream.char as string) !== 'u') { throw new AiScriptSyntaxError('character "u" expected', this.stream.getPos()); @@ -569,11 +588,7 @@ export class Scanner implements ITokenStream { break; } case 'escape': { - if (this.stream.eof) { - throw new AiScriptUnexpectedEOFError(pos); - } - value += this.stream.char; - this.stream.next(); + value += this.decodeEscapeSequence(); state = 'string'; break; } @@ -632,13 +647,7 @@ export class Scanner implements ITokenStream { break; } case 'escape': { - // エスケープ対象の文字が無いままEOFに達した - if (this.stream.eof) { - throw new AiScriptUnexpectedEOFError(pos); - } - // 普通の文字として取り込み - buf += this.stream.char; - this.stream.next(); + buf += this.decodeEscapeSequence(); // 通常の文字列に戻る state = 'string'; break; diff --git a/src/utils/characters.ts b/src/utils/characters.ts index 2c5a170a..b2df24f1 100644 --- a/src/utils/characters.ts +++ b/src/utils/characters.ts @@ -72,3 +72,21 @@ export function decodeUnicodeEscapeSequence(string: string): string { return result; } + +export function tryDecodeSingleEscapeCharacter(s: string): string | null { + switch (s) { + // case 'b': return '\b'; + case 't': return '\t'; + case 'n': return '\n'; + // case 'v': return '\v'; + // case 'f': return '\f'; + case 'r': return '\r'; + case '"': return '"'; + case '\'': return '\''; + case '\\': return '\\'; + case '`': return '`'; + case '{': return '{'; + case '}': return '}'; + default: return null; + } +} diff --git a/test/interpreter.ts b/test/interpreter.ts index 0b8bc0ea..1b1a98e8 100644 --- a/test/interpreter.ts +++ b/test/interpreter.ts @@ -1,6 +1,6 @@ import * as assert from 'assert'; import { describe, expect, test, vi, beforeEach, afterEach } from 'vitest'; -import { Parser, Interpreter, values, errors, utils, Ast } from '../src'; +import { Parser, Interpreter, std, values, errors, utils, Ast } from '../src'; import { FALSE, NUM, OBJ, STR, TRUE, Value } from '../src/interpreter/value'; let { FN_NATIVE } = values; @@ -261,6 +261,7 @@ describe('pause', () => { let count = 0; const interpreter = new Interpreter({ + ...std, count: values.FN_NATIVE(() => { count++; }), }, {}); diff --git a/test/literals.ts b/test/literals.ts index 7cacfb71..c10c1ed7 100644 --- a/test/literals.ts +++ b/test/literals.ts @@ -367,3 +367,83 @@ describe('Template syntax', () => { }); }); +describe('Escape sequence', () => { + describe('valid', () => { + const cases: [string, string][] = [ + ['\\t', '\t'], // horizontal tab + ['\\n', '\n'], // line feed + ['\\r', '\r'], // carriage return + ['\\"', '"'], + ['\\\'', '\''], + ['\\\\', '\\'], + ['\\`', '`'], + ['\\{', '{'], + ['\\}', '}'], + ['\\u0041', 'A'], + ['\\u85cd', '藍'], + ['\\u85CD', '藍'], + ['\\ud842\\udfb7', '𠮷'], + ['\\uD842\\uDFB7', '𠮷'], + ]; + + describe('double quote', () => { + test.each(cases)('value of escape sequence "%s" must be "%s"', async (char, expected) => { + const res = await exe(` + <: "${char}" + `); + eq(res, STR(expected)); + }); + }); + + describe('single quote', () => { + test.each(cases)('value of escape sequence "%s" must be "%s"', async (char, expected) => { + const res = await exe(` + <: '${char}' + `); + eq(res, STR(expected)); + }); + }); + + describe('template', () => { + test.each(cases)('value of escape sequence "%s" must be "%s"', async (string, expected) => { + const res = await exe(` + <: \`${string}\` + `); + eq(res, STR(expected)); + }); + }); + }); + + describe('invalid', () => { + const cases: [string][] = [ + ['\\x'], + ['\\b'], + ['\\v'], + ['\\f'], + ]; + + describe('double quote', () => { + test.each(cases)('value of escape sequence "%s" must not be allowed', async (char) => { + await expect(async () => await exe(` + <: "${char}" + `)).rejects.toThrow(AiScriptSyntaxError); + }); + }); + + describe('single quote', () => { + test.each(cases)('value of escape sequence "%s" must not be allowed', async (char) => { + await expect(async () => await exe(` + <: '${char}' + `)).rejects.toThrow(AiScriptSyntaxError); + }); + }); + + describe('template', () => { + test.each(cases)('value of escape sequence "%s" must not be allowed', async (string) => { + await expect(async () => await exe(` + <: \`${string}\` + `)).rejects.toThrow(AiScriptSyntaxError); + }); + }); + }); +}); diff --git a/test/testutils.ts b/test/testutils.ts index 0b09ac11..feedd52c 100644 --- a/test/testutils.ts +++ b/test/testutils.ts @@ -1,11 +1,11 @@ import { expect as globalExpect } from 'vitest'; -import { Parser, Interpreter } from '../src'; +import { Parser, Interpreter, std } from '../src'; import { Value } from '../src/interpreter/value'; export async function exe(script: string): Promise { const parser = new Parser(); let result = undefined; - const interpreter = new Interpreter({}, { + const interpreter = new Interpreter(std, { out(value) { if (!result) result = value; else if (!Array.isArray(result)) result = [result, value]; @@ -23,7 +23,7 @@ export async function exe(script: string): Promise { export function exeSync(script: string): Value | undefined { const parser = new Parser(); - const interpreter = new Interpreter({}, { + const interpreter = new Interpreter(std, { out(value) { }, log(type, {val}) { diff --git a/unreleased/separate-std.md b/unreleased/separate-std.md new file mode 100644 index 00000000..566a7a14 --- /dev/null +++ b/unreleased/separate-std.md @@ -0,0 +1,3 @@ +- **Breaking For Hosts** print、readline、Core名前空間以外の標準関数がインタプリターと独立して定義されるようになりました。 + - 標準関数群は`std`としてエクスポートされます。 + - これまでと同様の動作を提供するには、stdをインポートしてInterpreterの第一引数に渡してください。 diff --git a/unreleased/str_escape_sequnece.md b/unreleased/str_escape_sequnece.md new file mode 100644 index 00000000..e868751b --- /dev/null +++ b/unreleased/str_escape_sequnece.md @@ -0,0 +1,8 @@ +- **Breaking change** 文字列リテラルやテンプレートリテラルにおけるエスケープシーケンスの仕様を変更しました。 + - 以下のエスケープシーケンスが追加されました。 + - `\t` - 水平タブ (U+0009) + - `\n` - 改行 (U+000A) + - `\r` - 復帰 (U+000D) + - `\u`とそれに続く4桁の16進数の英数字 - 与えられた値を持つUTF-16コード単位として解釈されます。 + - `\"`, `\'`, `\\`, `` \` ``, `\{`, `\}` - それぞれ、`\`の直後の文字そのものとなります。 + - `\`とそれに続く文字列が上記のいずれにも一致しない場合、文法エラーが発生するようになりました。