From ecd76255403a35023a5ed910305588ed2d7381fc Mon Sep 17 00:00:00 2001 From: FineArchs Date: Tue, 21 May 2024 14:21:54 +0900 Subject: [PATCH] divide test --- jest.config.cjs | 10 +- test/index.ts | 2873 ++++++++------------------------------------- test/keywords.ts | 87 ++ test/literals.ts | 189 +++ test/syntax.ts | 1471 +++++++++++++++++++++++ test/testutils.ts | 36 + 6 files changed, 2277 insertions(+), 2389 deletions(-) create mode 100644 test/keywords.ts create mode 100644 test/literals.ts create mode 100644 test/syntax.ts create mode 100644 test/testutils.ts diff --git a/jest.config.cjs b/jest.config.cjs index 4c87106b..ae2a8b83 100644 --- a/jest.config.cjs +++ b/jest.config.cjs @@ -154,15 +154,15 @@ module.exports = { // The glob patterns Jest uses to detect test files testMatch: [ - "**/__tests__/**/*.[jt]s?(x)", - "**/?(*.)+(spec|test).[tj]s?(x)", + // "**/__tests__/**/*.[jt]s?(x)", + // "**/?(*.)+(spec|test).[tj]s?(x)", "/test/**/*" ], // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped - // testPathIgnorePatterns: [ - // "\\\\node_modules\\\\" - // ], + testPathIgnorePatterns: [ + "/test/testutils.ts", + ], // The regexp pattern or array of patterns that Jest uses to detect test files // testRegex: [], diff --git a/test/index.ts b/test/index.ts index 621533fe..22baee6d 100644 --- a/test/index.ts +++ b/test/index.ts @@ -7,39 +7,9 @@ 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 { AiScriptSyntaxError } from '../src/error'; -let { AiScriptRuntimeError, AiScriptIndexOutOfRangeError } = errors; - -const exe = (program: string): Promise => new Promise((ok, err) => { - const aiscript = new Interpreter({}, { - out(value) { - ok(value); - }, - maxStep: 9999, - }); - - try { - const parser = new Parser(); - const ast = parser.parse(program); - aiscript.exec(ast).catch(err); - } catch (e) { - err(e); - } -}); - -const getMeta = (program: string) => { - const parser = new Parser(); - const ast = parser.parse(program); - - const metadata = Interpreter.collectMetadata(ast); +import { AiScriptSyntaxError, AiScriptRuntimeError, AiScriptIndexOutOfRangeError } from '../src/error'; +import { exe, eq } from './testutils'; - return metadata; -}; - -const eq = (a, b) => { - assert.deepEqual(a.type, b.type); - assert.deepEqual(a.value, b.value); -}; test.concurrent('Hello, world!', async () => { const res = await exe('<: "Hello, world!"'); @@ -52,317 +22,6 @@ test.concurrent('empty script', async () => { assert.deepEqual(ast, []); }); -describe('ops', () => { - test.concurrent('==', async () => { - eq(await exe('<: (1 == 1)'), BOOL(true)); - eq(await exe('<: (1 == 2)'), BOOL(false)); - eq(await exe('<: (Core:type == Core:type)'), BOOL(true)); - eq(await exe('<: (Core:type == Core:gt)'), BOOL(false)); - eq(await exe('<: (@(){} == @(){})'), BOOL(false)); - eq(await exe('<: (Core:eq == @(){})'), BOOL(false)); - eq(await exe(` - let f = @(){} - let g = f - - <: (f == g) - `), BOOL(true)); - }); - - test.concurrent('!=', async () => { - eq(await exe('<: (1 != 2)'), BOOL(true)); - eq(await exe('<: (1 != 1)'), BOOL(false)); - }); - - test.concurrent('&&', async () => { - eq(await exe('<: (true && true)'), BOOL(true)); - eq(await exe('<: (true && false)'), BOOL(false)); - eq(await exe('<: (false && true)'), BOOL(false)); - eq(await exe('<: (false && false)'), BOOL(false)); - eq(await exe('<: (false && null)'), BOOL(false)); - try { - await exe('<: (true && null)'); - } catch (e) { - assert.ok(e instanceof AiScriptRuntimeError); - return; - } - - eq( - await exe(` - var tmp = null - - @func() { - tmp = true - return true - } - - false && func() - - <: tmp - `), - NULL - ) - - eq( - await exe(` - var tmp = null - - @func() { - tmp = true - return true - } - - true && func() - - <: tmp - `), - BOOL(true) - ) - - assert.fail(); - }); - - test.concurrent('||', async () => { - eq(await exe('<: (true || true)'), BOOL(true)); - eq(await exe('<: (true || false)'), BOOL(true)); - eq(await exe('<: (false || true)'), BOOL(true)); - eq(await exe('<: (false || false)'), BOOL(false)); - eq(await exe('<: (true || null)'), BOOL(true)); - try { - await exe('<: (false || null)'); - } catch (e) { - assert.ok(e instanceof AiScriptRuntimeError); - return; - } - - eq( - await exe(` - var tmp = null - - @func() { - tmp = true - return true - } - - true || func() - - <: tmp - `), - NULL - ) - - eq( - await exe(` - var tmp = null - - @func() { - tmp = true - return true - } - - false || func() - - <: tmp - `), - BOOL(true) - ) - - assert.fail(); - }); - - test.concurrent('+', async () => { - eq(await exe('<: (1 + 1)'), NUM(2)); - }); - - test.concurrent('-', async () => { - eq(await exe('<: (1 - 1)'), NUM(0)); - }); - - test.concurrent('*', async () => { - eq(await exe('<: (1 * 1)'), NUM(1)); - }); - - test.concurrent('^', async () => { - eq(await exe('<: (1 ^ 0)'), NUM(1)); - }); - - test.concurrent('/', async () => { - eq(await exe('<: (1 / 1)'), NUM(1)); - }); - - test.concurrent('%', async () => { - eq(await exe('<: (1 % 1)'), NUM(0)); - }); - - test.concurrent('>', async () => { - eq(await exe('<: (2 > 1)'), BOOL(true)); - eq(await exe('<: (1 > 1)'), BOOL(false)); - eq(await exe('<: (0 > 1)'), BOOL(false)); - }); - - test.concurrent('<', async () => { - eq(await exe('<: (2 < 1)'), BOOL(false)); - eq(await exe('<: (1 < 1)'), BOOL(false)); - eq(await exe('<: (0 < 1)'), BOOL(true)); - }); - - test.concurrent('>=', async () => { - eq(await exe('<: (2 >= 1)'), BOOL(true)); - eq(await exe('<: (1 >= 1)'), BOOL(true)); - eq(await exe('<: (0 >= 1)'), BOOL(false)); - }); - - test.concurrent('<=', async () => { - eq(await exe('<: (2 <= 1)'), BOOL(false)); - eq(await exe('<: (1 <= 1)'), BOOL(true)); - eq(await exe('<: (0 <= 1)'), BOOL(true)); - }); - - test.concurrent('precedence', async () => { - eq(await exe('<: 1 + 2 * 3 + 4'), NUM(11)); - eq(await exe('<: 1 + 4 / 4 + 1'), NUM(3)); - eq(await exe('<: 1 + 1 == 2 && 2 * 2 == 4'), BOOL(true)); - eq(await exe('<: (1 + 1) * 2'), NUM(4)); - }); - - test.concurrent('negative numbers', async () => { - eq(await exe('<: 1+-1'), NUM(0)); - eq(await exe('<: 1--1'), NUM(2));//反直観的、禁止される可能性がある? - eq(await exe('<: -1*-1'), NUM(1)); - eq(await exe('<: -1==-1'), BOOL(true)); - eq(await exe('<: 1>-1'), BOOL(true)); - eq(await exe('<: -1<1'), BOOL(true)); - }); - -}); - -describe('Infix expression', () => { - test.concurrent('simple infix expression', async () => { - eq(await exe('<: 0 < 1'), BOOL(true)); - eq(await exe('<: 1 + 1'), NUM(2)); - }); - - test.concurrent('combination', async () => { - eq(await exe('<: 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10'), NUM(55)); - eq(await exe('<: Core:add(1, 3) * Core:mul(2, 5)'), NUM(40)); - }); - - test.concurrent('use parentheses to distinguish expr', async () => { - eq(await exe('<: (1 + 10) * (2 + 5)'), NUM(77)); - }); - - test.concurrent('syntax symbols vs infix operators', async () => { - const res = await exe(` - <: match true { - case 1 == 1 => "true" - case 1 < 1 => "false" - } - `); - eq(res, STR('true')); - }); - - test.concurrent('number + if expression', async () => { - eq(await exe('<: 1 + if true 1 else 2'), NUM(2)); - }); - - test.concurrent('number + match expression', async () => { - const res = await exe(` - <: 1 + match 2 == 2 { - case true => 3 - case false => 4 - } - `); - eq(res, NUM(4)); - }); - - test.concurrent('eval + eval', async () => { - eq(await exe('<: eval { 1 } + eval { 1 }'), NUM(2)); - }); - - test.concurrent('disallow line break', async () => { - try { - await exe(` - <: 1 + - 1 + 1 - `); - } catch (e) { - assert.ok(true); - return; - } - assert.fail(); - }); - - test.concurrent('escaped line break', async () => { - eq(await exe(` - <: 1 + \\ - 1 + 1 - `), NUM(3)); - }); - - test.concurrent('infix-to-fncall on namespace', async () => { - eq( - await exe(` - :: Hoge { - @add(x, y) { - x + y - } - } - <: Hoge:add(1, 2) - `), - NUM(3) - ); - }); -}); - -describe('Comment', () => { - test.concurrent('single line comment', async () => { - const res = await exe(` - // let a = ... - let a = 42 - <: a - `); - eq(res, NUM(42)); - }); - - test.concurrent('multi line comment', async () => { - const res = await exe(` - /* variable declaration here... - let a = ... - */ - let a = 42 - <: a - `); - eq(res, NUM(42)); - }); - - test.concurrent('multi line comment 2', async () => { - const res = await exe(` - /* variable declaration here... - let a = ... - */ - let a = 42 - /* - another comment here - */ - <: a - `); - eq(res, NUM(42)); - }); - - test.concurrent('// as string', async () => { - const res = await exe('<: "//"'); - eq(res, STR('//')); - }); - - test.concurrent('line tail', async () => { - const res = await exe(` - let x = 'a' // comment - let y = 'b' - <: x - `); - eq(res, STR('a')); - }); -}); - test.concurrent('式にコロンがあってもオブジェクトと判定されない', async () => { const res = await exe(` <: eval { @@ -394,14 +53,6 @@ test.concurrent('dec', async () => { eq(res, NUM(-6)); }); -test.concurrent('var', async () => { - const res = await exe(` - let a = 42 - <: a - `); - eq(res, NUM(42)); -}); - test.concurrent('参照が繋がらない', async () => { const res = await exe(` var f = @() { "a" } @@ -413,2041 +64,709 @@ test.concurrent('参照が繋がらない', async () => { eq(res, STR('a')); }); -describe('Cannot put multiple statements in a line', () => { - test.concurrent('var def', async () => { - try { - await exe(` - let a = 42 let b = 11 - `); - } catch (e) { - assert.ok(true); - return; - } - assert.fail(); - }); - - test.concurrent('var def (op)', async () => { - try { - await exe(` - let a = 13 + 75 let b = 24 + 146 - `); - } catch (e) { - assert.ok(true); - return; - } - assert.fail(); - }); - - test.concurrent('var def in block', async () => { - try { - await exe(` - eval { - let a = 42 let b = 11 - } - `); - } catch (e) { - assert.ok(true); - return; - } - assert.fail(); - }); +test.concurrent('empty function', async () => { + const res = await exe(` + @hoge() { } + <: hoge() + `); + eq(res, NULL); }); -describe('terminator', () => { - describe('top-level', () => { - test.concurrent('newline', async () => { - const res = await exe(` - :: A { - let x = 1 - } - :: B { - let x = 2 - } - <: A:x - `); - eq(res, NUM(1)); - }); - - test.concurrent('semi colon', async () => { - const res = await exe(` - ::A{let x = 1};::B{let x = 2} - <: A:x - `); - eq(res, NUM(1)); - }); - - test.concurrent('semi colon of the tail', async () => { - const res = await exe(` - ::A{let x = 1}; - <: A:x - `); - eq(res, NUM(1)); - }); - }); +test.concurrent('empty lambda', async () => { + const res = await exe(` + let hoge = @() { } + <: hoge() + `); + eq(res, NULL); +}); - describe('block', () => { - test.concurrent('newline', async () => { - const res = await exe(` - eval { - let x = 1 - let y = 2 - <: x + y - } - `); - eq(res, NUM(3)); - }); +test.concurrent('lambda that returns an object', async () => { + const res = await exe(` + let hoge = @() {{}} + <: hoge() + `); + eq(res, OBJ(new Map())); +}); - test.concurrent('semi colon', async () => { - const res = await exe(` - eval{let x=1;let y=2;<:x+y} - `); - eq(res, NUM(3)); - }); +test.concurrent('Closure', async () => { + const res = await exe(` + @store(v) { + let state = v + @() { + state + } + } + let s = store("ai") + <: s() + `); + eq(res, STR('ai')); +}); - test.concurrent('semi colon of the tail', async () => { - const res = await exe(` - eval{let x=1;<:x;} - `); - eq(res, NUM(1)); - }); - }); - - describe('namespace', () => { - test.concurrent('newline', async () => { - const res = await exe(` - :: A { - let x = 1 - let y = 2 - } - <: A:x + A:y - `); - eq(res, NUM(3)); - }); - - test.concurrent('semi colon', async () => { - const res = await exe(` - ::A{let x=1;let y=2} - <: A:x + A:y - `); - eq(res, NUM(3)); - }); - - test.concurrent('semi colon of the tail', async () => { - const res = await exe(` - ::A{let x=1;} - <: A:x - `); - eq(res, NUM(1)); - }); - }); -}); - -describe('separator', () => { - describe('match', () => { - test.concurrent('multi line', async () => { - const res = await exe(` - let x = 1 - <: match x { - case 1 => "a" - case 2 => "b" - } - `); - eq(res, STR('a')); - }); - - test.concurrent('multi line with semi colon', async () => { - const res = await exe(` - let x = 1 - <: match x { - case 1 => "a", - case 2 => "b" - } - `); - eq(res, STR('a')); - }); - - test.concurrent('single line', async () => { - const res = await exe(` - let x = 1 - <:match x{case 1=>"a",case 2=>"b"} - `); - eq(res, STR('a')); - }); - - test.concurrent('single line with tail semi colon', async () => { - const res = await exe(` - let x = 1 - <: match x{case 1=>"a",case 2=>"b",} - `); - eq(res, STR('a')); - }); - - test.concurrent('multi line (default)', async () => { - const res = await exe(` - let x = 3 - <: match x { - case 1 => "a" - case 2 => "b" - default => "c" - } - `); - eq(res, STR('c')); - }); - - test.concurrent('multi line with semi colon (default)', async () => { - const res = await exe(` - let x = 3 - <: match x { - case 1 => "a", - case 2 => "b", - default => "c" - } - `); - eq(res, STR('c')); - }); - - test.concurrent('single line (default)', async () => { - const res = await exe(` - let x = 3 - <:match x{case 1=>"a",case 2=>"b",default=>"c"} - `); - eq(res, STR('c')); - }); - - test.concurrent('single line with tail semi colon (default)', async () => { - const res = await exe(` - let x = 3 - <:match x{case 1=>"a",case 2=>"b",default=>"c",} - `); - eq(res, STR('c')); - }); - }); - - describe('call', () => { - test.concurrent('multi line', async () => { - const res = await exe(` - @f(a, b, c) { - a * b + c - } - <: f( - 2 - 3 - 1 - ) - `); - eq(res, NUM(7)); - }); - - test.concurrent('multi line with comma', async () => { - const res = await exe(` - @f(a, b, c) { - a * b + c - } - <: f( - 2, - 3, - 1 - ) - `); - eq(res, NUM(7)); - }); - - test.concurrent('single line', async () => { - const res = await exe(` - @f(a, b, c) { - a * b + c - } - <:f(2,3,1) - `); - eq(res, NUM(7)); - }); - - test.concurrent('single line with tail comma', async () => { - const res = await exe(` - @f(a, b, c) { - a * b + c - } - <:f(2,3,1,) - `); - eq(res, NUM(7)); - }); - }); - - describe('obj', () => { - test.concurrent('multi line', async () => { - const res = await exe(` - let x = { - a: 1 - b: 2 - } - <: x.b - `); - eq(res, NUM(2)); - }); - - test.concurrent('multi line, multi newlines', async () => { - const res = await exe(` - let x = { - - a: 1 - - b: 2 - - } - <: x.b - `); - eq(res, NUM(2)); - }); - - test.concurrent('multi line with comma', async () => { - const res = await exe(` - let x = { - a: 1, - b: 2 - } - <: x.b - `); - eq(res, NUM(2)); - }); - - test.concurrent('single line', async () => { - const res = await exe(` - let x={a:1,b:2} - <: x.b - `); - eq(res, NUM(2)); - }); - - test.concurrent('single line with tail comma', async () => { - const res = await exe(` - let x={a:1,b:2,} - <: x.b - `); - eq(res, NUM(2)); - }); - }); - - describe('arr', () => { - test.concurrent('multi line', async () => { - const res = await exe(` - let x = [ - 1 - 2 - ] - <: x[1] - `); - eq(res, NUM(2)); - }); - - test.concurrent('multi line, multi newlines', async () => { - const res = await exe(` - let x = [ - - 1 - - 2 - - ] - <: x[1] - `); - eq(res, NUM(2)); - }); - - test.concurrent('multi line with comma', async () => { - const res = await exe(` - let x = [ - 1, - 2 - ] - <: x[1] - `); - eq(res, NUM(2)); - }); - - test.concurrent('multi line with comma, multi newlines', async () => { - const res = await exe(` - let x = [ - - 1, - - 2 - - ] - <: x[1] - `); - eq(res, NUM(2)); - }); - - test.concurrent('multi line with comma and tail comma', async () => { - const res = await exe(` - let x = [ - 1, - 2, - ] - <: x[1] - `); - eq(res, NUM(2)); - }); - - test.concurrent('multi line with comma and tail comma, multi newlines', async () => { - const res = await exe(` - let x = [ - - 1, - - 2, - - ] - <: x[1] - `); - eq(res, NUM(2)); - }); - - test.concurrent('single line', async () => { - const res = await exe(` - let x=[1,2] - <: x[1] - `); - eq(res, NUM(2)); - }); - - test.concurrent('single line with tail comma', async () => { - const res = await exe(` - let x=[1,2,] - <: x[1] - `); - eq(res, NUM(2)); - }); - }); - - describe('function params', () => { - test.concurrent('single line', async () => { - const res = await exe(` - @f(a, b) { - a + b - } - <: f(1, 2) - `); - eq(res, NUM(3)); - }); - - test.concurrent('single line with tail comma', async () => { - const res = await exe(` - @f(a, b, ) { - a + b - } - <: f(1, 2) - `); - eq(res, NUM(3)); - }); - - test.concurrent('multi line', async () => { - const res = await exe(` - @f( - a - b - ) { - a + b - } - <: f(1, 2) - `); - eq(res, NUM(3)); - }); - - test.concurrent('multi line with comma', async () => { - const res = await exe(` - @f( - a, - b - ) { - a + b - } - <: f(1, 2) - `); - eq(res, NUM(3)); - }); - - test.concurrent('multi line with tail comma', async () => { - const res = await exe(` - @f( - a, - b, - ) { - a + b - } - <: f(1, 2) - `); - eq(res, NUM(3)); - }); - }); -}); - -test.concurrent('empty function', async () => { - const res = await exe(` - @hoge() { } - <: hoge() - `); - eq(res, NULL); -}); - -test.concurrent('empty lambda', async () => { - const res = await exe(` - let hoge = @() { } - <: hoge() - `); - eq(res, NULL); -}); - -test.concurrent('lambda that returns an object', async () => { - const res = await exe(` - let hoge = @() {{}} - <: hoge() - `); - eq(res, OBJ(new Map())); -}); - -test.concurrent('Closure', async () => { - const res = await exe(` - @store(v) { - let state = v - @() { - state - } - } - let s = store("ai") - <: s() - `); - eq(res, STR('ai')); -}); - -test.concurrent('Closure (counter)', async () => { - const res = await exe(` - @create_counter() { - var count = 0 - { - get_count: @() { count }, - count: @() { count = (count + 1) }, - } - } +test.concurrent('Closure (counter)', async () => { + const res = await exe(` + @create_counter() { + var count = 0 + { + get_count: @() { count }, + count: @() { count = (count + 1) }, + } + } let counter = create_counter() let get_count = counter.get_count - let count = counter.count - - count() - count() - count() - - <: get_count() - `); - eq(res, NUM(3)); -}); - -test.concurrent('Recursion', async () => { - const res = await exe(` - @fact(n) { - if (n == 0) { 1 } else { (fact((n - 1)) * n) } - } - - <: fact(5) - `); - eq(res, NUM(120)); -}); - -describe('Var name starts with reserved word', () => { - test.concurrent('let', async () => { - const res = await exe(` - @f() { - let letcat = "ai" - letcat - } - <: f() - `); - eq(res, STR('ai')); - }); - - test.concurrent('var', async () => { - const res = await exe(` - @f() { - let varcat = "ai" - varcat - } - <: f() - `); - eq(res, STR('ai')); - }); - - test.concurrent('return', async () => { - const res = await exe(` - @f() { - let returncat = "ai" - returncat - } - <: f() - `); - eq(res, STR('ai')); - }); - - test.concurrent('each', async () => { - const res = await exe(` - @f() { - let eachcat = "ai" - eachcat - } - <: f() - `); - eq(res, STR('ai')); - }); - - test.concurrent('for', async () => { - const res = await exe(` - @f() { - let forcat = "ai" - forcat - } - <: f() - `); - eq(res, STR('ai')); - }); - - test.concurrent('loop', async () => { - const res = await exe(` - @f() { - let loopcat = "ai" - loopcat - } - <: f() - `); - eq(res, STR('ai')); - }); - - test.concurrent('break', async () => { - const res = await exe(` - @f() { - let breakcat = "ai" - breakcat - } - <: f() - `); - eq(res, STR('ai')); - }); - - test.concurrent('continue', async () => { - const res = await exe(` - @f() { - let continuecat = "ai" - continuecat - } - <: f() - `); - eq(res, STR('ai')); - }); - - test.concurrent('if', async () => { - const res = await exe(` - @f() { - let ifcat = "ai" - ifcat - } - <: f() - `); - eq(res, STR('ai')); - }); - - test.concurrent('match', async () => { - const res = await exe(` - @f() { - let matchcat = "ai" - matchcat - } - <: f() - `); - eq(res, STR('ai')); - }); - - test.concurrent('true', async () => { - const res = await exe(` - @f() { - let truecat = "ai" - truecat - } - <: f() - `); - eq(res, STR('ai')); - }); - - test.concurrent('false', async () => { - const res = await exe(` - @f() { - let falsecat = "ai" - falsecat - } - <: f() - `); - eq(res, STR('ai')); - }); - - test.concurrent('null', async () => { - const res = await exe(` - @f() { - let nullcat = "ai" - nullcat - } - <: f() - `); - eq(res, STR('ai')); - }); -}); - -describe('name validation of reserved word', () => { - test.concurrent('def', async () => { - try { - await exe(` - let let = 1 - `); - } catch (e) { - assert.ok(true); - return; - } - assert.fail(); - }); - - test.concurrent('attr', async () => { - try { - await exe(` - #[let 1] - @f() { 1 } - `); - } catch (e) { - assert.ok(true); - return; - } - assert.fail(); - }); - - test.concurrent('ns', async () => { - try { - await exe(` - :: let { - @f() { 1 } - } - `); - } catch (e) { - assert.ok(true); - return; - } - assert.fail(); - }); - - test.concurrent('var', async () => { - try { - await exe(` - let - `); - } catch (e) { - assert.ok(true); - return; - } - assert.fail(); - }); - - test.concurrent('prop', async () => { - try { - await exe(` - let x = { let: 1 } - x.let - `); - } catch (e) { - assert.ok(true); - return; - } - assert.fail(); - }); - - test.concurrent('meta', async () => { - try { - await exe(` - ### let 1 - `); - } catch (e) { - assert.ok(true); - return; - } - assert.fail(); - }); - - test.concurrent('fn', async () => { - try { - await exe(` - @let() { 1 } - `); - } catch (e) { - assert.ok(true); - return; - } - assert.fail(); - }); -}); - -describe('Object', () => { - test.concurrent('property access', async () => { - const res = await exe(` - let obj = { - a: { - b: { - c: 42, - }, - }, - } - - <: obj.a.b.c - `); - eq(res, NUM(42)); - }); - - test.concurrent('property access (fn call)', async () => { - const res = await exe(` - @f() { 42 } - - let obj = { - a: { - b: { - c: f, - }, - }, - } - - <: obj.a.b.c() - `); - eq(res, NUM(42)); - }); - - test.concurrent('property assign', async () => { - const res = await exe(` - let obj = { - a: 1 - b: { - c: 2 - d: { - e: 3 - } - } - } - - obj.a = 24 - obj.b.d.e = 42 - - <: obj - `); - eq(res, OBJ(new Map([ - ['a', NUM(24)], - ['b', OBJ(new Map([ - ['c', NUM(2)], - ['d', OBJ(new Map([ - ['e', NUM(42)], - ]))], - ]))], - ]))); - }); - - /* 未実装 - test.concurrent('string key', async () => { - const res = await exe(` - let obj = { - "藍": 42, - } - - <: obj."藍" - `); - eq(res, NUM(42)); - }); - - test.concurrent('string key including colon and period', async () => { - const res = await exe(` - let obj = { - ":.:": 42, - } - - <: obj.":.:" - `); - eq(res, NUM(42)); - }); - - test.concurrent('expression key', async () => { - const res = await exe(` - let key = "藍" - - let obj = { - : 42, - } - - <: obj - `); - eq(res, NUM(42)); - }); - */ -}); - -describe('Array', () => { - test.concurrent('Array item access', async () => { - const res = await exe(` - let arr = ["ai", "chan", "kawaii"] - - <: arr[1] - `); - eq(res, STR('chan')); - }); - - test.concurrent('Array item assign', async () => { - const res = await exe(` - let arr = ["ai", "chan", "kawaii"] - - arr[1] = "taso" - - <: arr - `); - eq(res, ARR([STR('ai'), STR('taso'), STR('kawaii')])); - }); - - test.concurrent('Assign array item to out of range', async () => { - try { - await exe(` - let arr = [1, 2, 3] - - arr[3] = 4 - - <: null - `) - } catch (e) { - eq(e instanceof AiScriptIndexOutOfRangeError, false); - } - - try { - await exe(` - let arr = [1, 2, 3] - - arr[9] = 10 - - <: null - `) - } catch (e) { - eq(e instanceof AiScriptIndexOutOfRangeError, true); - } - }); - - test.concurrent('index out of range error', async () => { - try { - await exe(` - <: [42][1] - `); - } catch (e) { - assert.equal(e instanceof AiScriptIndexOutOfRangeError, true); - return; - } - assert.fail(); - }); - - test.concurrent('index out of range on assignment', async () => { - try { - await exe(` - var a = [] - a[2] = 'hoge' - `); - } catch (e) { - assert.equal(e instanceof AiScriptIndexOutOfRangeError, true); - return; - } - assert.fail(); - }); - - test.concurrent('non-integer-indexed assignment', async () => { - try { - await exe(` - var a = [] - a[6.21] = 'hoge' - `); - } catch (e) { - assert.equal(e instanceof AiScriptIndexOutOfRangeError, true); - return; - } - assert.fail(); - }); -}); - -describe('chain', () => { - test.concurrent('chain access (prop + index + call)', async () => { - const res = await exe(` - let obj = { - a: { - b: [@(name) { name }, @(str) { "chan" }, @() { "kawaii" }], - }, - } - - <: obj.a.b[0]("ai") - `); - eq(res, STR('ai')); - }); - - test.concurrent('chained assign left side (prop + index)', async () => { - const res = await exe(` - let obj = { - a: { - b: ["ai", "chan", "kawaii"], - }, - } - - obj.a.b[1] = "taso" - - <: obj - `); - eq(res, OBJ(new Map([ - ['a', OBJ(new Map([ - ['b', ARR([STR('ai'), STR('taso'), STR('kawaii')])] - ]))] - ]))); - }); - - test.concurrent('chained assign right side (prop + index + call)', async () => { - const res = await exe(` - let obj = { - a: { - b: ["ai", "chan", "kawaii"], - }, - } - - var x = null - x = obj.a.b[1] - - <: x - `); - eq(res, STR('chan')); - }); - - test.concurrent('chained inc/dec left side (index + prop)', async () => { - const res = await exe(` - let arr = [ - { - a: 1, - b: 2, - } - ] - - arr[0].a += 1 - arr[0].b -= 1 - - <: arr - `); - eq(res, ARR([ - OBJ(new Map([ - ['a', NUM(2)], - ['b', NUM(1)] - ])) - ])); - }); - - test.concurrent('chained inc/dec left side (prop + index)', async () => { - const res = await exe(` - let obj = { - a: { - b: [1, 2, 3], - }, - } - - obj.a.b[1] += 1 - obj.a.b[2] -= 1 - - <: obj - `); - eq(res, OBJ(new Map([ - ['a', OBJ(new Map([ - ['b', ARR([NUM(1), NUM(3), NUM(2)])] - ]))] - ]))); - }); - - test.concurrent('prop in def', async () => { - const res = await exe(` - let x = @() { - let obj = { - a: 1 - } - obj.a - } - - <: x() - `); - eq(res, NUM(1)); - }); - - test.concurrent('prop in return', async () => { - const res = await exe(` - let x = @() { - let obj = { - a: 1 - } - return obj.a - 2 - } - - <: x() - `); - eq(res, NUM(1)); - }); - - test.concurrent('prop in each', async () => { - const res = await exe(` - let msgs = [] - let x = { a: ["ai", "chan", "kawaii"] } - each let item, x.a { - let y = { a: item } - msgs.push([y.a, "!"].join()) - } - <: msgs - `); - eq(res, ARR([STR('ai!'), STR('chan!'), STR('kawaii!')])); - }); - - test.concurrent('prop in for', async () => { - const res = await exe(` - let x = { times: 10, count: 0 } - for (let i, x.times) { - x.count = (x.count + i) - } - <: x.count - `); - eq(res, NUM(45)); - }); - - test.concurrent('object with index', async () => { - const res = await exe(` - let ai = {a: {}}['a'] - ai['chan'] = 'kawaii' - <: ai[{a: 'chan'}['a']] - `); - eq(res, STR('kawaii')); - }); - - test.concurrent('property chain with parenthesis', async () => { - let ast = Parser.parse(` - (a.b).c - `); - const line = ast[0]; - if ( - line.type !== 'prop' || - line.target.type !== 'prop' || - line.target.target.type !== 'identifier' - ) - assert.fail(); - assert.equal(line.target.target.name, 'a'); - assert.equal(line.target.name, 'b'); - assert.equal(line.name, 'c'); - }); - - test.concurrent('index chain with parenthesis', async () => { - let ast = Parser.parse(` - (a[42]).b - `); - const line = ast[0]; - if ( - line.type !== 'prop' || - line.target.type !== 'index' || - line.target.target.type !== 'identifier' || - line.target.index.type !== 'num' - ) - assert.fail(); - assert.equal(line.target.target.name, 'a'); - assert.equal(line.target.index.value, 42); - assert.equal(line.name, 'b'); - }); - - test.concurrent('call chain with parenthesis', async () => { - let ast = Parser.parse(` - (foo(42, 57)).bar - `); - const line = ast[0]; - if ( - line.type !== 'prop' || - line.target.type !== 'call' || - line.target.target.type !== 'identifier' || - line.target.args.length !== 2 || - line.target.args[0].type !== 'num' || - line.target.args[1].type !== 'num' - ) - assert.fail(); - assert.equal(line.target.target.name, 'foo'); - assert.equal(line.target.args[0].value, 42); - assert.equal(line.target.args[1].value, 57); - assert.equal(line.name, 'bar'); - }); - - test.concurrent('longer chain with parenthesis', async () => { - let ast = Parser.parse(` - (a.b.c).d.e - `); - const line = ast[0]; - if ( - line.type !== 'prop' || - line.target.type !== 'prop' || - line.target.target.type !== 'prop' || - line.target.target.target.type !== 'prop' || - line.target.target.target.target.type !== 'identifier' - ) - assert.fail(); - assert.equal(line.target.target.target.target.name, 'a'); - assert.equal(line.target.target.target.name, 'b'); - assert.equal(line.target.target.name, 'c'); - assert.equal(line.target.name, 'd'); - assert.equal(line.name, 'e'); - }); -}); - -describe('Template syntax', () => { - test.concurrent('Basic', async () => { - const res = await exe(` - let str = "kawaii" - <: \`Ai is {str}!\` - `); - eq(res, STR('Ai is kawaii!')); - }); - - test.concurrent('convert to str', async () => { - const res = await exe(` - <: \`1 + 1 = {(1 + 1)}\` - `); - eq(res, STR('1 + 1 = 2')); - }); - - test.concurrent('invalid', async () => { - try { - await exe(` - <: \`{hoge}\` - `); - } catch (e) { - assert.ok(true); - return; - } - assert.fail(); - }); - - test.concurrent('Escape', async () => { - const res = await exe(` - let message = "Hello" - <: \`\\\`a\\{b\\}c\\\`\` - `); - eq(res, STR('`a{b}c`')); - }); -}); - -test.concurrent('Throws error when divided by zero', async () => { - try { - await exe(` - <: (0 / 0) - `); - } catch (e) { - assert.ok(true); - return; - } - assert.fail(); -}); - -describe('Function call', () => { - test.concurrent('without args', async () => { - const res = await exe(` - @f() { - 42 - } - <: f() - `); - eq(res, NUM(42)); - }); - - test.concurrent('with args', async () => { - const res = await exe(` - @f(x) { - x - } - <: f(42) - `); - eq(res, NUM(42)); - }); - - test.concurrent('with args (separated by comma)', async () => { - const res = await exe(` - @f(x, y) { - (x + y) - } - <: f(1, 1) - `); - eq(res, NUM(2)); - }); - - test.concurrent('std: throw AiScript error when required arg missing', async () => { - try { - await exe(` - <: Core:eq(1) - `); - } catch (e) { - assert.ok(e instanceof AiScriptRuntimeError); - return; - } - assert.fail(); - }); - - test.concurrent('omitted args', async () => { - const res = await exe(` - @f(x, y) { - [x, y] - } - <: f(1) - `); - eq(res, ARR([NUM(1), NULL])); - }); -}); - -describe('Return', () => { - test.concurrent('Early return', async () => { - const res = await exe(` - @f() { - if true { - return "ai" - } - - "pope" - } - <: f() - `); - eq(res, STR('ai')); - }); - - test.concurrent('Early return (nested)', async () => { - const res = await exe(` - @f() { - if true { - if true { - return "ai" - } - } + let count = counter.count - "pope" - } - <: f() - `); - eq(res, STR('ai')); - }); + count() + count() + count() - test.concurrent('Early return (nested) 2', async () => { - const res = await exe(` - @f() { - if true { - return "ai" - } + <: get_count() + `); + eq(res, NUM(3)); +}); - "pope" - } +test.concurrent('Recursion', async () => { + const res = await exe(` + @fact(n) { + if (n == 0) { 1 } else { (fact((n - 1)) * n) } + } - @g() { - if (f() == "ai") { - return "kawaii" - } + <: fact(5) + `); + eq(res, NUM(120)); +}); - "pope" +describe('Object', () => { + test.concurrent('property access', async () => { + const res = await exe(` + let obj = { + a: { + b: { + c: 42, + }, + }, } - <: g() + <: obj.a.b.c `); - eq(res, STR('kawaii')); + eq(res, NUM(42)); }); - test.concurrent('Early return without block', async () => { + test.concurrent('property access (fn call)', async () => { const res = await exe(` - @f() { - if true return "ai" + @f() { 42 } - "pope" + let obj = { + a: { + b: { + c: f, + }, + }, } - <: f() + + <: obj.a.b.c() `); - eq(res, STR('ai')); + eq(res, NUM(42)); }); - test.concurrent('return inside for', async () => { + test.concurrent('property assign', async () => { const res = await exe(` - @f() { - var count = 0 - for (let i, 100) { - count += 1 - if (i == 42) { - return count + let obj = { + a: 1 + b: { + c: 2 + d: { + e: 3 } } } - <: f() - `); - eq(res, NUM(43)); - }); - test.concurrent('return inside for 2', async () => { - const res = await exe(` - @f() { - for (let i, 10) { - return 1 - } - 2 - } - <: f() + obj.a = 24 + obj.b.d.e = 42 + + <: obj `); - eq(res, NUM(1)); + eq(res, OBJ(new Map([ + ['a', NUM(24)], + ['b', OBJ(new Map([ + ['c', NUM(2)], + ['d', OBJ(new Map([ + ['e', NUM(42)], + ]))], + ]))], + ]))); }); - test.concurrent('return inside loop', async () => { + /* 未実装 + test.concurrent('string key', async () => { const res = await exe(` - @f() { - var count = 0 - loop { - count += 1 - if (count == 42) { - return count - } - } + let obj = { + "藍": 42, } - <: f() + + <: obj."藍" `); eq(res, NUM(42)); }); - test.concurrent('return inside loop 2', async () => { + test.concurrent('string key including colon and period', async () => { const res = await exe(` - @f() { - loop { - return 1 - } - 2 + let obj = { + ":.:": 42, } - <: f() - `); - eq(res, NUM(1)); - }); - test.concurrent('return inside each', async () => - { - const res = await exe(` - @f() { - var count = 0 - each (let item, ["ai", "chan", "kawaii"]) { - count += 1 - if (item == "chan") { - return count - } - } - } - <: f() + <: obj.":.:" `); - eq(res, NUM(2)); + eq(res, NUM(42)); }); - test.concurrent('return inside each 2', async () => - { + test.concurrent('expression key', async () => { const res = await exe(` - @f() { - each (let item, ["ai", "chan", "kawaii"]) { - return 1 - } - 2 + let key = "藍" + + let obj = { + : 42, } - <: f() + + <: obj `); - eq(res, NUM(1)); + eq(res, NUM(42)); }); + */ }); -describe('Eval', () => { - test.concurrent('returns value', async () => { +describe('Array', () => { + test.concurrent('Array item access', async () => { const res = await exe(` - let foo = eval { - let a = 1 - let b = 2 - (a + b) - } + let arr = ["ai", "chan", "kawaii"] - <: foo + <: arr[1] `); - eq(res, NUM(3)); + eq(res, STR('chan')); }); -}); -describe('exists', () => { - test.concurrent('Basic', async () => { + test.concurrent('Array item assign', async () => { const res = await exe(` - let foo = null - <: [(exists foo), (exists bar)] - `); - eq(res, ARR([BOOL(true), BOOL(false)])); - }); -}); + let arr = ["ai", "chan", "kawaii"] -describe('if', () => { - test.concurrent('if', async () => { - const res1 = await exe(` - var msg = "ai" - if true { - msg = "kawaii" - } - <: msg - `); - eq(res1, STR('kawaii')); + arr[1] = "taso" - const res2 = await exe(` - var msg = "ai" - if false { - msg = "kawaii" - } - <: msg + <: arr `); - eq(res2, STR('ai')); + eq(res, ARR([STR('ai'), STR('taso'), STR('kawaii')])); }); - test.concurrent('else', async () => { - const res1 = await exe(` - var msg = null - if true { - msg = "ai" - } else { - msg = "kawaii" - } - <: msg - `); - eq(res1, STR('ai')); + test.concurrent('Assign array item to out of range', async () => { + try { + await exe(` + let arr = [1, 2, 3] - const res2 = await exe(` - var msg = null - if false { - msg = "ai" - } else { - msg = "kawaii" - } - <: msg - `); - eq(res2, STR('kawaii')); - }); + arr[3] = 4 - test.concurrent('elif', async () => { - const res1 = await exe(` - var msg = "bebeyo" - if false { - msg = "ai" - } elif true { - msg = "kawaii" + <: null + `) + } catch (e) { + eq(e instanceof AiScriptIndexOutOfRangeError, false); } - <: msg - `); - eq(res1, STR('kawaii')); - const res2 = await exe(` - var msg = "bebeyo" - if false { - msg = "ai" - } elif false { - msg = "kawaii" + try { + await exe(` + let arr = [1, 2, 3] + + arr[9] = 10 + + <: null + `) + } catch (e) { + eq(e instanceof AiScriptIndexOutOfRangeError, true); } - <: msg - `); - eq(res2, STR('bebeyo')); }); - test.concurrent('if ~ elif ~ else', async () => { - const res1 = await exe(` - var msg = null - if false { - msg = "ai" - } elif true { - msg = "chan" - } else { - msg = "kawaii" + test.concurrent('index out of range error', async () => { + try { + await exe(` + <: [42][1] + `); + } catch (e) { + assert.equal(e instanceof AiScriptIndexOutOfRangeError, true); + return; } - <: msg - `); - eq(res1, STR('chan')); + assert.fail(); + }); - const res2 = await exe(` - var msg = null - if false { - msg = "ai" - } elif false { - msg = "chan" - } else { - msg = "kawaii" + test.concurrent('index out of range on assignment', async () => { + try { + await exe(` + var a = [] + a[2] = 'hoge' + `); + } catch (e) { + assert.equal(e instanceof AiScriptIndexOutOfRangeError, true); + return; } - <: msg - `); - eq(res2, STR('kawaii')); + assert.fail(); }); - test.concurrent('expr', async () => { - const res1 = await exe(` - <: if true "ai" else "kawaii" - `); - eq(res1, STR('ai')); - - const res2 = await exe(` - <: if false "ai" else "kawaii" - `); - eq(res2, STR('kawaii')); + test.concurrent('non-integer-indexed assignment', async () => { + try { + await exe(` + var a = [] + a[6.21] = 'hoge' + `); + } catch (e) { + assert.equal(e instanceof AiScriptIndexOutOfRangeError, true); + return; + } + assert.fail(); }); }); -describe('match', () => { - test.concurrent('Basic', async () => { +describe('chain', () => { + test.concurrent('chain access (prop + index + call)', async () => { const res = await exe(` - <: match 2 { - case 1 => "a" - case 2 => "b" - case 3 => "c" + let obj = { + a: { + b: [@(name) { name }, @(str) { "chan" }, @() { "kawaii" }], + }, } - `); - eq(res, STR('b')); - }); - test.concurrent('When default not provided, returns null', async () => { - const res = await exe(` - <: match 42 { - case 1 => "a" - case 2 => "b" - case 3 => "c" - } + <: obj.a.b[0]("ai") `); - eq(res, NULL); + eq(res, STR('ai')); }); - test.concurrent('With default', async () => { + test.concurrent('chained assign left side (prop + index)', async () => { const res = await exe(` - <: match 42 { - case 1 => "a" - case 2 => "b" - case 3 => "c" - default => "d" + let obj = { + a: { + b: ["ai", "chan", "kawaii"], + }, } + + obj.a.b[1] = "taso" + + <: obj `); - eq(res, STR('d')); + eq(res, OBJ(new Map([ + ['a', OBJ(new Map([ + ['b', ARR([STR('ai'), STR('taso'), STR('kawaii')])] + ]))] + ]))); }); - test.concurrent('With block', async () => { + test.concurrent('chained assign right side (prop + index + call)', async () => { const res = await exe(` - <: match 2 { - case 1 => 1 - case 2 => { - let a = 1 - let b = 2 - (a + b) - } - case 3 => 3 + let obj = { + a: { + b: ["ai", "chan", "kawaii"], + }, } + + var x = null + x = obj.a.b[1] + + <: x `); - eq(res, NUM(3)); + eq(res, STR('chan')); }); - test.concurrent('With return', async () => { + test.concurrent('chained inc/dec left side (index + prop)', async () => { const res = await exe(` - @f(x) { - match x { - case 1 => { - return "ai" - } + let arr = [ + { + a: 1, + b: 2, } - "foo" - } - <: f(1) - `); - eq(res, STR('ai')); - }); -}); + ] -describe('loop', () => { - test.concurrent('Basic', async () => { - const res = await exe(` - var count = 0 - loop { - if (count == 10) break - count = (count + 1) - } - <: count - `); - eq(res, NUM(10)); - }); + arr[0].a += 1 + arr[0].b -= 1 - test.concurrent('with continue', async () => { - const res = await exe(` - var a = ["ai", "chan", "kawaii", "yo", "!"] - var b = [] - loop { - var x = a.shift() - if (x == "chan") continue - if (x == "yo") break - b.push(x) - } - <: b + <: arr `); - eq(res, ARR([STR('ai'), STR('kawaii')])); + eq(res, ARR([ + OBJ(new Map([ + ['a', NUM(2)], + ['b', NUM(1)] + ])) + ])); }); -}); -describe('for', () => { - test.concurrent('Basic', async () => { + test.concurrent('chained inc/dec left side (prop + index)', async () => { const res = await exe(` - var count = 0 - for (let i, 10) { - count += i + 1 + let obj = { + a: { + b: [1, 2, 3], + }, } - <: count - `); - eq(res, NUM(55)); - }); - test.concurrent('initial value', async () => { - const res = await exe(` - var count = 0 - for (let i = 2, 10) { - count += i - } - <: count + obj.a.b[1] += 1 + obj.a.b[2] -= 1 + + <: obj `); - eq(res, NUM(65)); + eq(res, OBJ(new Map([ + ['a', OBJ(new Map([ + ['b', ARR([NUM(1), NUM(3), NUM(2)])] + ]))] + ]))); }); - test.concurrent('wuthout iterator', async () => { + test.concurrent('prop in def', async () => { const res = await exe(` - var count = 0 - for (10) { - count = (count + 1) + let x = @() { + let obj = { + a: 1 + } + obj.a } - <: count + + <: x() `); - eq(res, NUM(10)); + eq(res, NUM(1)); }); - test.concurrent('without brackets', async () => { + test.concurrent('prop in return', async () => { const res = await exe(` - var count = 0 - for let i, 10 { - count = (count + i) + let x = @() { + let obj = { + a: 1 + } + return obj.a + 2 } - <: count + + <: x() `); - eq(res, NUM(45)); + eq(res, NUM(1)); }); - test.concurrent('Break', async () => { + test.concurrent('prop in each', async () => { const res = await exe(` - var count = 0 - for (let i, 20) { - if (i == 11) break - count += i + let msgs = [] + let x = { a: ["ai", "chan", "kawaii"] } + each let item, x.a { + let y = { a: item } + msgs.push([y.a, "!"].join()) } - <: count + <: msgs `); - eq(res, NUM(55)); + eq(res, ARR([STR('ai!'), STR('chan!'), STR('kawaii!')])); }); - test.concurrent('continue', async () => { + test.concurrent('prop in for', async () => { const res = await exe(` - var count = 0 - for (let i, 10) { - if (i == 5) continue - count = (count + 1) + let x = { times: 10, count: 0 } + for (let i, x.times) { + x.count = (x.count + i) } - <: count + <: x.count `); - eq(res, NUM(9)); + eq(res, NUM(45)); }); - test.concurrent('single statement', async () => { + test.concurrent('object with index', async () => { const res = await exe(` - var count = 0 - for 10 count += 1 - <: count + let ai = {a: {}}['a'] + ai['chan'] = 'kawaii' + <: ai[{a: 'chan'}['a']] `); - eq(res, NUM(10)); + eq(res, STR('kawaii')); }); - test.concurrent('var name without space', async () => { - try { - await exe(` - for (leti, 10) { - <: i - } + test.concurrent('property chain with parenthesis', async () => { + let ast = Parser.parse(` + (a.b).c `); - } catch (e) { - assert.ok(true); - return; - } - assert.fail(); - }); -}); - -describe('for of', () => { - test.concurrent('standard', async () => { - const res = await exe(` - let msgs = [] - each let item, ["ai", "chan", "kawaii"] { - msgs.push([item, "!"].join()) - } - <: msgs - `); - eq(res, ARR([STR('ai!'), STR('chan!'), STR('kawaii!')])); + const line = ast[0]; + if ( + line.type !== 'prop' || + line.target.type !== 'prop' || + line.target.target.type !== 'identifier' + ) + assert.fail(); + assert.equal(line.target.target.name, 'a'); + assert.equal(line.target.name, 'b'); + assert.equal(line.name, 'c'); }); - test.concurrent('Break', async () => { - const res = await exe(` - let msgs = [] - each let item, ["ai", "chan", "kawaii", "yo"] { - if (item == "kawaii") break - msgs.push([item, "!"].join()) - } - <: msgs - `); - eq(res, ARR([STR('ai!'), STR('chan!')])); + test.concurrent('index chain with parenthesis', async () => { + let ast = Parser.parse(` + (a[42]).b + `); + const line = ast[0]; + if ( + line.type !== 'prop' || + line.target.type !== 'index' || + line.target.target.type !== 'identifier' || + line.target.index.type !== 'num' + ) + assert.fail(); + assert.equal(line.target.target.name, 'a'); + assert.equal(line.target.index.value, 42); + assert.equal(line.name, 'b'); }); - test.concurrent('single statement', async () => { - const res = await exe(` - let msgs = [] - each let item, ["ai", "chan", "kawaii"] msgs.push([item, "!"].join()) - <: msgs - `); - eq(res, ARR([STR('ai!'), STR('chan!'), STR('kawaii!')])); + test.concurrent('call chain with parenthesis', async () => { + let ast = Parser.parse(` + (foo(42, 57)).bar + `); + const line = ast[0]; + if ( + line.type !== 'prop' || + line.target.type !== 'call' || + line.target.target.type !== 'identifier' || + line.target.args.length !== 2 || + line.target.args[0].type !== 'num' || + line.target.args[1].type !== 'num' + ) + assert.fail(); + assert.equal(line.target.target.name, 'foo'); + assert.equal(line.target.args[0].value, 42); + assert.equal(line.target.args[1].value, 57); + assert.equal(line.name, 'bar'); }); - test.concurrent('var name without space', async () => { - try { - await exe(` - each letitem, ["ai", "chan", "kawaii"] { - <: item - } + test.concurrent('longer chain with parenthesis', async () => { + let ast = Parser.parse(` + (a.b.c).d.e `); - } catch (e) { - assert.ok(true); - return; - } - assert.fail(); + const line = ast[0]; + if ( + line.type !== 'prop' || + line.target.type !== 'prop' || + line.target.target.type !== 'prop' || + line.target.target.target.type !== 'prop' || + line.target.target.target.target.type !== 'identifier' + ) + assert.fail(); + assert.equal(line.target.target.target.target.name, 'a'); + assert.equal(line.target.target.target.name, 'b'); + assert.equal(line.target.target.name, 'c'); + assert.equal(line.target.name, 'd'); + assert.equal(line.name, 'e'); }); }); -describe('not', () => { - test.concurrent('Basic', async () => { - const res = await exe(` - <: !true +test.concurrent('Throws error when divided by zero', async () => { + try { + await exe(` + <: (0 / 0) `); - eq(res, BOOL(false)); - }); + } catch (e) { + assert.ok(true); + return; + } + assert.fail(); }); -describe('namespace', () => { - test.concurrent('standard', async () => { +describe('Function call', () => { + test.concurrent('without args', async () => { const res = await exe(` - <: Foo:bar() + @f() { + 42 + } + <: f() + `); + eq(res, NUM(42)); + }); - :: Foo { - @bar() { "ai" } + test.concurrent('with args', async () => { + const res = await exe(` + @f(x) { + x } + <: f(42) `); - eq(res, STR('ai')); + eq(res, NUM(42)); }); - test.concurrent('self ref', async () => { + test.concurrent('with args (separated by comma)', async () => { const res = await exe(` - <: Foo:bar() - - :: Foo { - let ai = "kawaii" - @bar() { ai } + @f(x, y) { + (x + y) } + <: f(1, 1) `); - eq(res, STR('kawaii')); + eq(res, NUM(2)); }); - test.concurrent('cannot declare mutable variable', async () => { + test.concurrent('std: throw AiScript error when required arg missing', async () => { try { await exe(` - :: Foo { - var ai = "kawaii" - } + <: Core:eq(1) `); } catch (e) { - assert.ok(true); + assert.ok(e instanceof AiScriptRuntimeError); return; } assert.fail(); }); - test.concurrent('nested', async () => { + test.concurrent('omitted args', async () => { const res = await exe(` - <: Foo:Bar:baz() - - :: Foo { - :: Bar { - @baz() { "ai" } - } + @f(x, y) { + [x, y] } + <: f(1) `); - eq(res, STR('ai')); + eq(res, ARR([NUM(1), NULL])); }); +}); - test.concurrent('nested ref', async () => { +describe('Return', () => { + test.concurrent('Early return', async () => { const res = await exe(` - <: Foo:baz - - :: Foo { - let baz = Bar:ai - :: Bar { - let ai = "kawaii" + @f() { + if true { + return "ai" } + + "pope" } + <: f() `); - eq(res, STR('kawaii')); + eq(res, STR('ai')); }); -}); -describe('literal', () => { - test.concurrent('string (single quote)', async () => { + test.concurrent('Early return (nested)', async () => { const res = await exe(` - <: 'foo' - `); - eq(res, STR('foo')); - }); + @f() { + if true { + if true { + return "ai" + } + } - test.concurrent('string (double quote)', async () => { - const res = await exe(` - <: "foo" + "pope" + } + <: f() `); - eq(res, STR('foo')); - }); - - test.concurrent('Escaped double quote', async () => { - const res = await exe('<: "ai saw a note \\"bebeyo\\"."'); - eq(res, STR('ai saw a note "bebeyo".')); - }); - - test.concurrent('Escaped single quote', async () => { - const res = await exe('<: \'ai saw a note \\\'bebeyo\\\'.\''); - eq(res, STR('ai saw a note \'bebeyo\'.')); + eq(res, STR('ai')); }); - test.concurrent('bool (true)', async () => { + test.concurrent('Early return (nested) 2', async () => { const res = await exe(` - <: true - `); - eq(res, BOOL(true)); - }); + @f() { + if true { + return "ai" + } - test.concurrent('bool (false)', async () => { - const res = await exe(` - <: false - `); - eq(res, BOOL(false)); - }); + "pope" + } - test.concurrent('number (Int)', async () => { - const res = await exe(` - <: 10 - `); - eq(res, NUM(10)); - }); + @g() { + if (f() == "ai") { + return "kawaii" + } - test.concurrent('number (Float)', async () => { - const res = await exe(` - <: 0.5 - `); - eq(res, NUM(0.5)); - }); + "pope" + } - test.concurrent('arr (separated by comma)', async () => { - const res = await exe(` - <: [1, 2, 3] + <: g() `); - eq(res, ARR([NUM(1), NUM(2), NUM(3)])); + eq(res, STR('kawaii')); }); - test.concurrent('arr (separated by comma) (with trailing comma)', async () => { + test.concurrent('Early return without block', async () => { const res = await exe(` - <: [1, 2, 3,] - `); - eq(res, ARR([NUM(1), NUM(2), NUM(3)])); - }); + @f() { + if true return "ai" - test.concurrent('arr (separated by line break)', async () => { - const res = await exe(` - <: [ - 1 - 2 - 3 - ] + "pope" + } + <: f() `); - eq(res, ARR([NUM(1), NUM(2), NUM(3)])); + eq(res, STR('ai')); }); - test.concurrent('arr (separated by line break and comma)', async () => { + test.concurrent('return inside for', async () => { const res = await exe(` - <: [ - 1, - 2, - 3 - ] + @f() { + var count = 0 + for (let i, 100) { + count += 1 + if (i == 42) { + return count + } + } + } + <: f() `); - eq(res, ARR([NUM(1), NUM(2), NUM(3)])); + eq(res, NUM(43)); }); - test.concurrent('arr (separated by line break and comma) (with trailing comma)', async () => { + test.concurrent('return inside for 2', async () => { const res = await exe(` - <: [ - 1, - 2, - 3, - ] + @f() { + for (let i, 10) { + return 1 + } + 2 + } + <: f() `); - eq(res, ARR([NUM(1), NUM(2), NUM(3)])); + eq(res, NUM(1)); }); - test.concurrent('obj (separated by comma)', async () => { + test.concurrent('return inside loop', async () => { const res = await exe(` - <: { a: 1, b: 2, c: 3 } + @f() { + var count = 0 + loop { + count += 1 + if (count == 42) { + return count + } + } + } + <: f() `); - eq(res, OBJ(new Map([['a', NUM(1)], ['b', NUM(2)], ['c', NUM(3)]]))); + eq(res, NUM(42)); }); - test.concurrent('obj (separated by comma) (with trailing comma)', async () => { + test.concurrent('return inside loop 2', async () => { const res = await exe(` - <: { a: 1, b: 2, c: 3, } + @f() { + loop { + return 1 + } + 2 + } + <: f() `); - eq(res, OBJ(new Map([['a', NUM(1)], ['b', NUM(2)], ['c', NUM(3)]]))); + eq(res, NUM(1)); }); - test.concurrent('obj (separated by line break)', async () => { + test.concurrent('return inside each', async () => + { const res = await exe(` - <: { - a: 1 - b: 2 - c: 3 + @f() { + var count = 0 + each (let item, ["ai", "chan", "kawaii"]) { + count += 1 + if (item == "chan") { + return count + } + } } + <: f() `); - eq(res, OBJ(new Map([['a', NUM(1)], ['b', NUM(2)], ['c', NUM(3)]]))); + eq(res, NUM(2)); }); - test.concurrent('obj and arr (separated by line break)', async () => { + test.concurrent('return inside each 2', async () => + { const res = await exe(` - <: { - a: 1 - b: [ - 1 - 2 - 3 - ] - c: 3 + @f() { + each (let item, ["ai", "chan", "kawaii"]) { + return 1 + } + 2 } + <: f() `); - eq(res, OBJ(new Map([ - ['a', NUM(1)], - ['b', ARR([NUM(1), NUM(2), NUM(3)])], - ['c', NUM(3)] - ]))); + eq(res, NUM(1)); }); }); @@ -2477,191 +796,6 @@ describe('type declaration', () => { }); }); -describe('meta', () => { - test.concurrent('default meta', async () => { - const res = getMeta(` - ### { a: 1, b: 2, c: 3, } - `); - eq(res, new Map([ - [null, { - a: 1, - b: 2, - c: 3, - }] - ])); - eq(res!.get(null), { - a: 1, - b: 2, - c: 3, - }); - }); - - describe('String', () => { - test.concurrent('valid', async () => { - const res = getMeta(` - ### x "hoge" - `); - eq(res, new Map([ - ['x', 'hoge'] - ])); - }); - }); - - describe('Number', () => { - test.concurrent('valid', async () => { - const res = getMeta(` - ### x 42 - `); - eq(res, new Map([ - ['x', 42] - ])); - }); - }); - - describe('Boolean', () => { - test.concurrent('valid', async () => { - const res = getMeta(` - ### x true - `); - eq(res, new Map([ - ['x', true] - ])); - }); - }); - - describe('Null', () => { - test.concurrent('valid', async () => { - const res = getMeta(` - ### x null - `); - eq(res, new Map([ - ['x', null] - ])); - }); - }); - - describe('Array', () => { - test.concurrent('valid', async () => { - const res = getMeta(` - ### x [1, 2, 3] - `); - eq(res, new Map([ - ['x', [1, 2, 3]] - ])); - }); - - test.concurrent('invalid', async () => { - try { - getMeta(` - ### x [1, (2 + 2), 3] - `); - } catch (e) { - assert.ok(true); - return; - } - assert.fail(); - }); - }); - - describe('Object', () => { - test.concurrent('valid', async () => { - const res = getMeta(` - ### x { a: 1, b: 2, c: 3, } - `); - eq(res, new Map([ - ['x', { - a: 1, - b: 2, - c: 3, - }] - ])); - }); - - test.concurrent('invalid', async () => { - try { - getMeta(` - ### x { a: 1, b: (2 + 2), c: 3, } - `); - } catch (e) { - assert.ok(true); - return; - } - assert.fail(); - }); - }); - - describe('Template', () => { - test.concurrent('invalid', async () => { - try { - getMeta(` - ### x \`foo {bar} baz\` - `); - } catch (e) { - assert.ok(true); - return; - } - assert.fail(); - }); - }); - - describe('Expression', () => { - test.concurrent('invalid', async () => { - try { - getMeta(` - ### x (1 + 1) - `); - } catch (e) { - assert.ok(true); - return; - } - assert.fail(); - }); - }); -}); - -describe('lang version', () => { - test.concurrent('number', async () => { - const res = utils.getLangVersion(` - /// @2021 - @f(x) { - x - } - `); - assert.strictEqual(res, '2021'); - }); - - test.concurrent('chars', async () => { - const res = utils.getLangVersion(` - /// @ canary - const a = 1 - @f(x) { - x - } - f(a) - `); - assert.strictEqual(res, 'canary'); - }); - - test.concurrent('complex', async () => { - const res = utils.getLangVersion(` - /// @ 2.0-Alpha - @f(x) { - x - } - `); - assert.strictEqual(res, '2.0-Alpha'); - }); - - test.concurrent('no specified', async () => { - const res = utils.getLangVersion(` - @f(x) { - x - } - `); - assert.strictEqual(res, null); - }); -}); - describe('Attribute', () => { test.concurrent('single attribute with function (str)', async () => { let node: Ast.Node; @@ -2785,35 +919,6 @@ describe('Location', () => { }); }); -describe('Variable declaration', () => { - test.concurrent('Do not assign to let (issue #328)', async () => { - const err = await exe(` - let hoge = 33 - hoge = 4 - `).then(() => undefined).catch(err => err); - - assert.ok(err instanceof AiScriptRuntimeError); - }); -}); - -describe('Variable assignment', () => { - test.concurrent('simple', async () => { - eq(await exe(` - var hoge = 25 - hoge = 7 - <: hoge - `), NUM(7)); - }); - test.concurrent('destructuring assignment', async () => { - eq(await exe(` - var hoge = 'foo' - var fuga = { value: 'bar' } - [{ value: hoge }, fuga] = [fuga, hoge] - <: [hoge, fuga] - `), ARR([STR('bar'), STR('foo')])); - }); -}); - describe('primitive props', () => { describe('num', () => { test.concurrent('to_str', async () => { diff --git a/test/keywords.ts b/test/keywords.ts new file mode 100644 index 00000000..79848c5f --- /dev/null +++ b/test/keywords.ts @@ -0,0 +1,87 @@ +import { expect, test } from '@jest/globals'; +import { Parser, Interpreter } from '../src'; +import { AiScriptSyntaxError } from '../src/error'; +import { exe } from './testutils'; + +const reservedWords = [ + 'null', + 'true', + 'false', + 'each', + 'for', + 'loop', + 'break', + 'continue', + 'match', + 'case', + 'default', + 'if', + 'elif', + 'else', + 'return', + 'eval', + 'var', + 'let', + 'exists', +] as const; + +const sampleCodes = Object.entries({ + variable: word => + ` + let ${word} = "ai" + ${word} + `, + + function: word => + ` + @${word}() { 'ai' } + ${word}() + `, + + attribute: word => + ` + #[${word} 1] + @f() { 1 } + `, + + namespace: word => + ` + :: ${word} { + @f() { 1 } + } + ${word}:f() + `, + + prop: word => + ` + let x = { ${word}: 1 } + x.${word} + `, + + meta: word => + ` + ### ${word} 1 + `, +}); + +function pickRandom(arr: T[]): T { + return arr[Math.floor(Math.random() * arr.length)]; +} + +describe.each( + sampleCodes +)('reserved word validation on %s', (_, sampleCode) => { + + test.concurrent.each( + [pickRandom(reservedWords)] + )('%s must be rejected', (word) => { + return expect(exe(sampleCode(word))).rejects.toThrow(AiScriptSyntaxError); + }); + + test.concurrent.each( + [pickRandom(reservedWords)] + )('%scat must be allowed', (word) => { + return exe(sampleCode(word+'cat')); + }); + +}); diff --git a/test/literals.ts b/test/literals.ts new file mode 100644 index 00000000..77a8ab33 --- /dev/null +++ b/test/literals.ts @@ -0,0 +1,189 @@ +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 { } from '../src/error'; +import { exe, eq } from './testutils'; + +describe('literal', () => { + test.concurrent('string (single quote)', async () => { + const res = await exe(` + <: 'foo' + `); + eq(res, STR('foo')); + }); + + test.concurrent('string (double quote)', async () => { + const res = await exe(` + <: "foo" + `); + eq(res, STR('foo')); + }); + + test.concurrent('Escaped double quote', async () => { + const res = await exe('<: "ai saw a note \\"bebeyo\\"."'); + eq(res, STR('ai saw a note "bebeyo".')); + }); + + test.concurrent('Escaped single quote', async () => { + const res = await exe('<: \'ai saw a note \\\'bebeyo\\\'.\''); + eq(res, STR('ai saw a note \'bebeyo\'.')); + }); + + test.concurrent('bool (true)', async () => { + const res = await exe(` + <: true + `); + eq(res, BOOL(true)); + }); + + test.concurrent('bool (false)', async () => { + const res = await exe(` + <: false + `); + eq(res, BOOL(false)); + }); + + test.concurrent('number (Int)', async () => { + const res = await exe(` + <: 10 + `); + eq(res, NUM(10)); + }); + + test.concurrent('number (Float)', async () => { + const res = await exe(` + <: 0.5 + `); + 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)])); + }); + + 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(` + <: { + a: 1 + b: [ + 1 + 2 + 3 + ] + c: 3 + } + `); + eq(res, OBJ(new Map([ + ['a', NUM(1)], + ['b', ARR([NUM(1), NUM(2), NUM(3)])], + ['c', NUM(3)] + ]))); + }); +}); + +describe('Template syntax', () => { + test.concurrent('Basic', async () => { + const res = await exe(` + let str = "kawaii" + <: \`Ai is {str}!\` + `); + eq(res, STR('Ai is kawaii!')); + }); + + test.concurrent('convert to str', async () => { + const res = await exe(` + <: \`1 + 1 = {(1 + 1)}\` + `); + eq(res, STR('1 + 1 = 2')); + }); + + test.concurrent('invalid', async () => { + try { + await exe(` + <: \`{hoge}\` + `); + } catch (e) { + assert.ok(true); + return; + } + assert.fail(); + }); + + test.concurrent('Escape', async () => { + const res = await exe(` + let message = "Hello" + <: \`\\\`a\\{b\\}c\\\`\` + `); + eq(res, STR('`a{b}c`')); + }); +}); + diff --git a/test/syntax.ts b/test/syntax.ts new file mode 100644 index 00000000..eefb8b15 --- /dev/null +++ b/test/syntax.ts @@ -0,0 +1,1471 @@ +import * as assert from 'assert'; +import { expect, test } from '@jest/globals'; +import { utils } from '../src'; +import { NUM, STR, NULL, ARR, OBJ, BOOL, TRUE, FALSE, ERROR ,FN_NATIVE } from '../src/interpreter/value'; +import { AiScriptRuntimeError } from '../src/error'; +import { exe, getMeta, eq } from './testutils'; + +/* + * General + */ +describe('terminator', () => { + describe('top-level', () => { + test.concurrent('newline', async () => { + const res = await exe(` + :: A { + let x = 1 + } + :: B { + let x = 2 + } + <: A:x + `); + eq(res, NUM(1)); + }); + + test.concurrent('semi colon', async () => { + const res = await exe(` + ::A{let x = 1};::B{let x = 2} + <: A:x + `); + eq(res, NUM(1)); + }); + + test.concurrent('semi colon of the tail', async () => { + const res = await exe(` + ::A{let x = 1}; + <: A:x + `); + eq(res, NUM(1)); + }); + }); + + describe('block', () => { + test.concurrent('newline', async () => { + const res = await exe(` + eval { + let x = 1 + let y = 2 + <: x + y + } + `); + eq(res, NUM(3)); + }); + + test.concurrent('semi colon', async () => { + const res = await exe(` + eval{let x=1;let y=2;<:x+y} + `); + eq(res, NUM(3)); + }); + + test.concurrent('semi colon of the tail', async () => { + const res = await exe(` + eval{let x=1;<:x;} + `); + eq(res, NUM(1)); + }); + }); + + describe('namespace', () => { + test.concurrent('newline', async () => { + const res = await exe(` + :: A { + let x = 1 + let y = 2 + } + <: A:x + A:y + `); + eq(res, NUM(3)); + }); + + test.concurrent('semi colon', async () => { + const res = await exe(` + ::A{let x=1;let y=2} + <: A:x + A:y + `); + eq(res, NUM(3)); + }); + + test.concurrent('semi colon of the tail', async () => { + const res = await exe(` + ::A{let x=1;} + <: A:x + `); + eq(res, NUM(1)); + }); + }); +}); + +describe('separator', () => { + describe('match', () => { + test.concurrent('multi line', async () => { + const res = await exe(` + let x = 1 + <: match x { + case 1 => "a" + case 2 => "b" + } + `); + eq(res, STR('a')); + }); + + test.concurrent('multi line with semi colon', async () => { + const res = await exe(` + let x = 1 + <: match x { + case 1 => "a", + case 2 => "b" + } + `); + eq(res, STR('a')); + }); + + test.concurrent('single line', async () => { + const res = await exe(` + let x = 1 + <:match x{case 1=>"a",case 2=>"b"} + `); + eq(res, STR('a')); + }); + + test.concurrent('single line with tail semi colon', async () => { + const res = await exe(` + let x = 1 + <: match x{case 1=>"a",case 2=>"b",} + `); + eq(res, STR('a')); + }); + + test.concurrent('multi line (default)', async () => { + const res = await exe(` + let x = 3 + <: match x { + case 1 => "a" + case 2 => "b" + default => "c" + } + `); + eq(res, STR('c')); + }); + + test.concurrent('multi line with semi colon (default)', async () => { + const res = await exe(` + let x = 3 + <: match x { + case 1 => "a", + case 2 => "b", + default => "c" + } + `); + eq(res, STR('c')); + }); + + test.concurrent('single line (default)', async () => { + const res = await exe(` + let x = 3 + <:match x{case 1=>"a",case 2=>"b",default=>"c"} + `); + eq(res, STR('c')); + }); + + test.concurrent('single line with tail semi colon (default)', async () => { + const res = await exe(` + let x = 3 + <:match x{case 1=>"a",case 2=>"b",default=>"c",} + `); + eq(res, STR('c')); + }); + }); + + describe('call', () => { + test.concurrent('multi line', async () => { + const res = await exe(` + @f(a, b, c) { + a * b + c + } + <: f( + 2 + 3 + 1 + ) + `); + eq(res, NUM(7)); + }); + + test.concurrent('multi line with comma', async () => { + const res = await exe(` + @f(a, b, c) { + a * b + c + } + <: f( + 2, + 3, + 1 + ) + `); + eq(res, NUM(7)); + }); + + test.concurrent('single line', async () => { + const res = await exe(` + @f(a, b, c) { + a * b + c + } + <:f(2,3,1) + `); + eq(res, NUM(7)); + }); + + test.concurrent('single line with tail comma', async () => { + const res = await exe(` + @f(a, b, c) { + a * b + c + } + <:f(2,3,1,) + `); + eq(res, NUM(7)); + }); + }); + + describe('obj', () => { + test.concurrent('multi line', async () => { + const res = await exe(` + let x = { + a: 1 + b: 2 + } + <: x.b + `); + eq(res, NUM(2)); + }); + + test.concurrent('multi line, multi newlines', async () => { + const res = await exe(` + let x = { + + a: 1 + + b: 2 + + } + <: x.b + `); + eq(res, NUM(2)); + }); + + test.concurrent('multi line with comma', async () => { + const res = await exe(` + let x = { + a: 1, + b: 2 + } + <: x.b + `); + eq(res, NUM(2)); + }); + + test.concurrent('single line', async () => { + const res = await exe(` + let x={a:1,b:2} + <: x.b + `); + eq(res, NUM(2)); + }); + + test.concurrent('single line with tail comma', async () => { + const res = await exe(` + let x={a:1,b:2,} + <: x.b + `); + eq(res, NUM(2)); + }); + }); + + describe('arr', () => { + test.concurrent('multi line', async () => { + const res = await exe(` + let x = [ + 1 + 2 + ] + <: x[1] + `); + eq(res, NUM(2)); + }); + + test.concurrent('multi line, multi newlines', async () => { + const res = await exe(` + let x = [ + + 1 + + 2 + + ] + <: x[1] + `); + eq(res, NUM(2)); + }); + + test.concurrent('multi line with comma', async () => { + const res = await exe(` + let x = [ + 1, + 2 + ] + <: x[1] + `); + eq(res, NUM(2)); + }); + + test.concurrent('multi line with comma, multi newlines', async () => { + const res = await exe(` + let x = [ + + 1, + + 2 + + ] + <: x[1] + `); + eq(res, NUM(2)); + }); + + test.concurrent('multi line with comma and tail comma', async () => { + const res = await exe(` + let x = [ + 1, + 2, + ] + <: x[1] + `); + eq(res, NUM(2)); + }); + + test.concurrent('multi line with comma and tail comma, multi newlines', async () => { + const res = await exe(` + let x = [ + + 1, + + 2, + + ] + <: x[1] + `); + eq(res, NUM(2)); + }); + + test.concurrent('single line', async () => { + const res = await exe(` + let x=[1,2] + <: x[1] + `); + eq(res, NUM(2)); + }); + + test.concurrent('single line with tail comma', async () => { + const res = await exe(` + let x=[1,2,] + <: x[1] + `); + eq(res, NUM(2)); + }); + }); + + describe('function params', () => { + test.concurrent('single line', async () => { + const res = await exe(` + @f(a, b) { + a + b + } + <: f(1, 2) + `); + eq(res, NUM(3)); + }); + + test.concurrent('single line with tail comma', async () => { + const res = await exe(` + @f(a, b, ) { + a + b + } + <: f(1, 2) + `); + eq(res, NUM(3)); + }); + + test.concurrent('multi line', async () => { + const res = await exe(` + @f( + a + b + ) { + a + b + } + <: f(1, 2) + `); + eq(res, NUM(3)); + }); + + test.concurrent('multi line with comma', async () => { + const res = await exe(` + @f( + a, + b + ) { + a + b + } + <: f(1, 2) + `); + eq(res, NUM(3)); + }); + + test.concurrent('multi line with tail comma', async () => { + const res = await exe(` + @f( + a, + b, + ) { + a + b + } + <: f(1, 2) + `); + eq(res, NUM(3)); + }); + }); +}); + + +describe('Comment', () => { + test.concurrent('single line comment', async () => { + const res = await exe(` + // let a = ... + let a = 42 + <: a + `); + eq(res, NUM(42)); + }); + + test.concurrent('multi line comment', async () => { + const res = await exe(` + /* variable declaration here... + let a = ... + */ + let a = 42 + <: a + `); + eq(res, NUM(42)); + }); + + test.concurrent('multi line comment 2', async () => { + const res = await exe(` + /* variable declaration here... + let a = ... + */ + let a = 42 + /* + another comment here + */ + <: a + `); + eq(res, NUM(42)); + }); + + test.concurrent('// as string', async () => { + const res = await exe('<: "//"'); + eq(res, STR('//')); + }); + + test.concurrent('line tail', async () => { + const res = await exe(` + let x = 'a' // comment + let y = 'b' + <: x + `); + eq(res, STR('a')); + }); +}); + +describe('lang version', () => { + test.concurrent('number', async () => { + const res = utils.getLangVersion(` + /// @2021 + @f(x) { + x + } + `); + assert.strictEqual(res, '2021'); + }); + + test.concurrent('chars', async () => { + const res = utils.getLangVersion(` + /// @ canary + const a = 1 + @f(x) { + x + } + f(a) + `); + assert.strictEqual(res, 'canary'); + }); + + test.concurrent('complex', async () => { + const res = utils.getLangVersion(` + /// @ 2.0-Alpha + @f(x) { + x + } + `); + assert.strictEqual(res, '2.0-Alpha'); + }); + + test.concurrent('no specified', async () => { + const res = utils.getLangVersion(` + @f(x) { + x + } + `); + assert.strictEqual(res, null); + }); +}); + +/* + * Statements + */ +describe('Cannot put multiple statements in a line', () => { + test.concurrent('var def', async () => { + try { + await exe(` + let a = 42 let b = 11 + `); + } catch (e) { + assert.ok(true); + return; + } + assert.fail(); + }); + + test.concurrent('var def (op)', async () => { + try { + await exe(` + let a = 13 + 75 let b = 24 + 146 + `); + } catch (e) { + assert.ok(true); + return; + } + assert.fail(); + }); + + test.concurrent('var def in block', async () => { + try { + await exe(` + eval { + let a = 42 let b = 11 + } + `); + } catch (e) { + assert.ok(true); + return; + } + assert.fail(); + }); +}); + +describe('Variable declaration', () => { + test.concurrent('let', async () => { + const res = await exe(` + let a = 42 + <: a + `); + eq(res, NUM(42)); + }); + test.concurrent('Do not assign to let (issue #328)', async () => { + const err = await exe(` + let hoge = 33 + hoge = 4 + `).then(() => undefined).catch(err => err); + + assert.ok(err instanceof AiScriptRuntimeError); + }); + test.concurrent('empty function', async () => { + const res = await exe(` + @hoge() { } + <: hoge() + `); + eq(res, NULL); + }); +}); + +describe('Variable assignment', () => { + test.concurrent('simple', async () => { + eq(await exe(` + var hoge = 25 + hoge = 7 + <: hoge + `), NUM(7)); + }); + test.concurrent('destructuring assignment', async () => { + eq(await exe(` + var hoge = 'foo' + var fuga = { value: 'bar' } + [{ value: hoge }, fuga] = [fuga, hoge] + <: [hoge, fuga] + `), ARR([STR('bar'), STR('foo')])); + }); +}); + +describe('for', () => { + test.concurrent('Basic', async () => { + const res = await exe(` + var count = 0 + for (let i, 10) { + count += i + 1 + } + <: count + `); + eq(res, NUM(55)); + }); + + test.concurrent('initial value', async () => { + const res = await exe(` + var count = 0 + for (let i = 2, 10) { + count += i + } + <: count + `); + eq(res, NUM(65)); + }); + + test.concurrent('wuthout iterator', async () => { + const res = await exe(` + var count = 0 + for (10) { + count = (count + 1) + } + <: count + `); + eq(res, NUM(10)); + }); + + test.concurrent('without brackets', async () => { + const res = await exe(` + var count = 0 + for let i, 10 { + count = (count + i) + } + <: count + `); + eq(res, NUM(45)); + }); + + test.concurrent('Break', async () => { + const res = await exe(` + var count = 0 + for (let i, 20) { + if (i == 11) break + count += i + } + <: count + `); + eq(res, NUM(55)); + }); + + test.concurrent('continue', async () => { + const res = await exe(` + var count = 0 + for (let i, 10) { + if (i == 5) continue + count = (count + 1) + } + <: count + `); + eq(res, NUM(9)); + }); + + test.concurrent('single statement', async () => { + const res = await exe(` + var count = 0 + for 10 count += 1 + <: count + `); + eq(res, NUM(10)); + }); + + test.concurrent('var name without space', async () => { + try { + await exe(` + for (leti, 10) { + <: i + } + `); + } catch (e) { + assert.ok(true); + return; + } + assert.fail(); + }); +}); + +describe('each', () => { + test.concurrent('standard', async () => { + const res = await exe(` + let msgs = [] + each let item, ["ai", "chan", "kawaii"] { + msgs.push([item, "!"].join()) + } + <: msgs + `); + eq(res, ARR([STR('ai!'), STR('chan!'), STR('kawaii!')])); + }); + + test.concurrent('Break', async () => { + const res = await exe(` + let msgs = [] + each let item, ["ai", "chan", "kawaii", "yo"] { + if (item == "kawaii") break + msgs.push([item, "!"].join()) + } + <: msgs + `); + eq(res, ARR([STR('ai!'), STR('chan!')])); + }); + + test.concurrent('single statement', async () => { + const res = await exe(` + let msgs = [] + each let item, ["ai", "chan", "kawaii"] msgs.push([item, "!"].join()) + <: msgs + `); + eq(res, ARR([STR('ai!'), STR('chan!'), STR('kawaii!')])); + }); + + test.concurrent('var name without space', async () => { + try { + await exe(` + each letitem, ["ai", "chan", "kawaii"] { + <: item + } + `); + } catch (e) { + assert.ok(true); + return; + } + assert.fail(); + }); +}); + +describe('loop', () => { + test.concurrent('Basic', async () => { + const res = await exe(` + var count = 0 + loop { + if (count == 10) break + count = (count + 1) + } + <: count + `); + eq(res, NUM(10)); + }); + + test.concurrent('with continue', async () => { + const res = await exe(` + var a = ["ai", "chan", "kawaii", "yo", "!"] + var b = [] + loop { + var x = a.shift() + if (x == "chan") continue + if (x == "yo") break + b.push(x) + } + <: b + `); + eq(res, ARR([STR('ai'), STR('kawaii')])); + }); +}); + +/* + * Global statements + */ +describe('meta', () => { + test.concurrent('default meta', async () => { + const res = getMeta(` + ### { a: 1, b: 2, c: 3, } + `); + eq(res, new Map([ + [null, { + a: 1, + b: 2, + c: 3, + }] + ])); + eq(res!.get(null), { + a: 1, + b: 2, + c: 3, + }); + }); + + describe('String', () => { + test.concurrent('valid', async () => { + const res = getMeta(` + ### x "hoge" + `); + eq(res, new Map([ + ['x', 'hoge'] + ])); + }); + }); + + describe('Number', () => { + test.concurrent('valid', async () => { + const res = getMeta(` + ### x 42 + `); + eq(res, new Map([ + ['x', 42] + ])); + }); + }); + + describe('Boolean', () => { + test.concurrent('valid', async () => { + const res = getMeta(` + ### x true + `); + eq(res, new Map([ + ['x', true] + ])); + }); + }); + + describe('Null', () => { + test.concurrent('valid', async () => { + const res = getMeta(` + ### x null + `); + eq(res, new Map([ + ['x', null] + ])); + }); + }); + + describe('Array', () => { + test.concurrent('valid', async () => { + const res = getMeta(` + ### x [1, 2, 3] + `); + eq(res, new Map([ + ['x', [1, 2, 3]] + ])); + }); + + test.concurrent('invalid', async () => { + try { + getMeta(` + ### x [1, (2 + 2), 3] + `); + } catch (e) { + assert.ok(true); + return; + } + assert.fail(); + }); + }); + + describe('Object', () => { + test.concurrent('valid', async () => { + const res = getMeta(` + ### x { a: 1, b: 2, c: 3, } + `); + eq(res, new Map([ + ['x', { + a: 1, + b: 2, + c: 3, + }] + ])); + }); + + test.concurrent('invalid', async () => { + try { + getMeta(` + ### x { a: 1, b: (2 + 2), c: 3, } + `); + } catch (e) { + assert.ok(true); + return; + } + assert.fail(); + }); + }); + + describe('Template', () => { + test.concurrent('invalid', async () => { + try { + getMeta(` + ### x \`foo {bar} baz\` + `); + } catch (e) { + assert.ok(true); + return; + } + assert.fail(); + }); + }); + + describe('Expression', () => { + test.concurrent('invalid', async () => { + try { + getMeta(` + ### x (1 + 1) + `); + } catch (e) { + assert.ok(true); + return; + } + assert.fail(); + }); + }); +}); + +describe('namespace', () => { + test.concurrent('standard', async () => { + const res = await exe(` + <: Foo:bar() + + :: Foo { + @bar() { "ai" } + } + `); + eq(res, STR('ai')); + }); + + test.concurrent('self ref', async () => { + const res = await exe(` + <: Foo:bar() + + :: Foo { + let ai = "kawaii" + @bar() { ai } + } + `); + eq(res, STR('kawaii')); + }); + + test.concurrent('cannot declare mutable variable', async () => { + try { + await exe(` + :: Foo { + var ai = "kawaii" + } + `); + } catch (e) { + assert.ok(true); + return; + } + assert.fail(); + }); + + test.concurrent('nested', async () => { + const res = await exe(` + <: Foo:Bar:baz() + + :: Foo { + :: Bar { + @baz() { "ai" } + } + } + `); + eq(res, STR('ai')); + }); + + test.concurrent('nested ref', async () => { + const res = await exe(` + <: Foo:baz + + :: Foo { + let baz = Bar:ai + :: Bar { + let ai = "kawaii" + } + } + `); + eq(res, STR('kawaii')); + }); +}); + +describe('operators', () => { + test.concurrent('==', async () => { + eq(await exe('<: (1 == 1)'), BOOL(true)); + eq(await exe('<: (1 == 2)'), BOOL(false)); + eq(await exe('<: (Core:type == Core:type)'), BOOL(true)); + eq(await exe('<: (Core:type == Core:gt)'), BOOL(false)); + eq(await exe('<: (@(){} == @(){})'), BOOL(false)); + eq(await exe('<: (Core:eq == @(){})'), BOOL(false)); + eq(await exe(` + let f = @(){} + let g = f + + <: (f == g) + `), BOOL(true)); + }); + + test.concurrent('!=', async () => { + eq(await exe('<: (1 != 2)'), BOOL(true)); + eq(await exe('<: (1 != 1)'), BOOL(false)); + }); + + test.concurrent('&&', async () => { + eq(await exe('<: (true && true)'), BOOL(true)); + eq(await exe('<: (true && false)'), BOOL(false)); + eq(await exe('<: (false && true)'), BOOL(false)); + eq(await exe('<: (false && false)'), BOOL(false)); + eq(await exe('<: (false && null)'), BOOL(false)); + try { + await exe('<: (true && null)'); + } catch (e) { + assert.ok(e instanceof AiScriptRuntimeError); + return; + } + + eq( + await exe(` + var tmp = null + + @func() { + tmp = true + return true + } + + false && func() + + <: tmp + `), + NULL + ) + + eq( + await exe(` + var tmp = null + + @func() { + tmp = true + return true + } + + true && func() + + <: tmp + `), + BOOL(true) + ) + + assert.fail(); + }); + + test.concurrent('||', async () => { + eq(await exe('<: (true || true)'), BOOL(true)); + eq(await exe('<: (true || false)'), BOOL(true)); + eq(await exe('<: (false || true)'), BOOL(true)); + eq(await exe('<: (false || false)'), BOOL(false)); + eq(await exe('<: (true || null)'), BOOL(true)); + try { + await exe('<: (false || null)'); + } catch (e) { + assert.ok(e instanceof AiScriptRuntimeError); + return; + } + + eq( + await exe(` + var tmp = null + + @func() { + tmp = true + return true + } + + true || func() + + <: tmp + `), + NULL + ) + + eq( + await exe(` + var tmp = null + + @func() { + tmp = true + return true + } + + false || func() + + <: tmp + `), + BOOL(true) + ) + + assert.fail(); + }); + + test.concurrent('+', async () => { + eq(await exe('<: (1 + 1)'), NUM(2)); + }); + + test.concurrent('-', async () => { + eq(await exe('<: (1 - 1)'), NUM(0)); + }); + + test.concurrent('*', async () => { + eq(await exe('<: (1 * 1)'), NUM(1)); + }); + + test.concurrent('^', async () => { + eq(await exe('<: (1 ^ 0)'), NUM(1)); + }); + + test.concurrent('/', async () => { + eq(await exe('<: (1 / 1)'), NUM(1)); + }); + + test.concurrent('%', async () => { + eq(await exe('<: (1 % 1)'), NUM(0)); + }); + + test.concurrent('>', async () => { + eq(await exe('<: (2 > 1)'), BOOL(true)); + eq(await exe('<: (1 > 1)'), BOOL(false)); + eq(await exe('<: (0 > 1)'), BOOL(false)); + }); + + test.concurrent('<', async () => { + eq(await exe('<: (2 < 1)'), BOOL(false)); + eq(await exe('<: (1 < 1)'), BOOL(false)); + eq(await exe('<: (0 < 1)'), BOOL(true)); + }); + + test.concurrent('>=', async () => { + eq(await exe('<: (2 >= 1)'), BOOL(true)); + eq(await exe('<: (1 >= 1)'), BOOL(true)); + eq(await exe('<: (0 >= 1)'), BOOL(false)); + }); + + test.concurrent('<=', async () => { + eq(await exe('<: (2 <= 1)'), BOOL(false)); + eq(await exe('<: (1 <= 1)'), BOOL(true)); + eq(await exe('<: (0 <= 1)'), BOOL(true)); + }); + + test.concurrent('precedence', async () => { + eq(await exe('<: 1 + 2 * 3 + 4'), NUM(11)); + eq(await exe('<: 1 + 4 / 4 + 1'), NUM(3)); + eq(await exe('<: 1 + 1 == 2 && 2 * 2 == 4'), BOOL(true)); + eq(await exe('<: (1 + 1) * 2'), NUM(4)); + }); + + test.concurrent('negative numbers', async () => { + eq(await exe('<: 1+-1'), NUM(0)); + eq(await exe('<: 1--1'), NUM(2));//反直観的、禁止される可能性がある? + eq(await exe('<: -1*-1'), NUM(1)); + eq(await exe('<: -1==-1'), BOOL(true)); + eq(await exe('<: 1>-1'), BOOL(true)); + eq(await exe('<: -1<1'), BOOL(true)); + }); + +}); + +describe('not', () => { + test.concurrent('Basic', async () => { + const res = await exe(` + <: !true + `); + eq(res, BOOL(false)); + }); +}); + +describe('Infix expression', () => { + test.concurrent('simple infix expression', async () => { + eq(await exe('<: 0 < 1'), BOOL(true)); + eq(await exe('<: 1 + 1'), NUM(2)); + }); + + test.concurrent('combination', async () => { + eq(await exe('<: 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10'), NUM(55)); + eq(await exe('<: Core:add(1, 3) * Core:mul(2, 5)'), NUM(40)); + }); + + test.concurrent('use parentheses to distinguish expr', async () => { + eq(await exe('<: (1 + 10) * (2 + 5)'), NUM(77)); + }); + + test.concurrent('syntax symbols vs infix operators', async () => { + const res = await exe(` + <: match true { + case 1 == 1 => "true" + case 1 < 1 => "false" + } + `); + eq(res, STR('true')); + }); + + test.concurrent('number + if expression', async () => { + eq(await exe('<: 1 + if true 1 else 2'), NUM(2)); + }); + + test.concurrent('number + match expression', async () => { + const res = await exe(` + <: 1 + match 2 == 2 { + case true => 3 + case false => 4 + } + `); + eq(res, NUM(4)); + }); + + test.concurrent('eval + eval', async () => { + eq(await exe('<: eval { 1 } + eval { 1 }'), NUM(2)); + }); + + test.concurrent('disallow line break', async () => { + try { + await exe(` + <: 1 + + 1 + 1 + `); + } catch (e) { + assert.ok(true); + return; + } + assert.fail(); + }); + + test.concurrent('escaped line break', async () => { + eq(await exe(` + <: 1 + \\ + 1 + 1 + `), NUM(3)); + }); + + test.concurrent('infix-to-fncall on namespace', async () => { + eq( + await exe(` + :: Hoge { + @add(x, y) { + x + y + } + } + <: Hoge:add(1, 2) + `), + NUM(3) + ); + }); +}); + +describe('if', () => { + test.concurrent('if', async () => { + const res1 = await exe(` + var msg = "ai" + if true { + msg = "kawaii" + } + <: msg + `); + eq(res1, STR('kawaii')); + + const res2 = await exe(` + var msg = "ai" + if false { + msg = "kawaii" + } + <: msg + `); + eq(res2, STR('ai')); + }); + + test.concurrent('else', async () => { + const res1 = await exe(` + var msg = null + if true { + msg = "ai" + } else { + msg = "kawaii" + } + <: msg + `); + eq(res1, STR('ai')); + + const res2 = await exe(` + var msg = null + if false { + msg = "ai" + } else { + msg = "kawaii" + } + <: msg + `); + eq(res2, STR('kawaii')); + }); + + test.concurrent('elif', async () => { + const res1 = await exe(` + var msg = "bebeyo" + if false { + msg = "ai" + } elif true { + msg = "kawaii" + } + <: msg + `); + eq(res1, STR('kawaii')); + + const res2 = await exe(` + var msg = "bebeyo" + if false { + msg = "ai" + } elif false { + msg = "kawaii" + } + <: msg + `); + eq(res2, STR('bebeyo')); + }); + + test.concurrent('if ~ elif ~ else', async () => { + const res1 = await exe(` + var msg = null + if false { + msg = "ai" + } elif true { + msg = "chan" + } else { + msg = "kawaii" + } + <: msg + `); + eq(res1, STR('chan')); + + const res2 = await exe(` + var msg = null + if false { + msg = "ai" + } elif false { + msg = "chan" + } else { + msg = "kawaii" + } + <: msg + `); + eq(res2, STR('kawaii')); + }); + + test.concurrent('expr', async () => { + const res1 = await exe(` + <: if true "ai" else "kawaii" + `); + eq(res1, STR('ai')); + + const res2 = await exe(` + <: if false "ai" else "kawaii" + `); + eq(res2, STR('kawaii')); + }); +}); + +describe('eval', () => { + test.concurrent('returns value', async () => { + const res = await exe(` + let foo = eval { + let a = 1 + let b = 2 + (a + b) + } + + <: foo + `); + eq(res, NUM(3)); + }); +}); + +describe('match', () => { + test.concurrent('Basic', async () => { + const res = await exe(` + <: match 2 { + case 1 => "a" + case 2 => "b" + case 3 => "c" + } + `); + eq(res, STR('b')); + }); + + test.concurrent('When default not provided, returns null', async () => { + const res = await exe(` + <: match 42 { + case 1 => "a" + case 2 => "b" + case 3 => "c" + } + `); + eq(res, NULL); + }); + + test.concurrent('With default', async () => { + const res = await exe(` + <: match 42 { + case 1 => "a" + case 2 => "b" + case 3 => "c" + default => "d" + } + `); + eq(res, STR('d')); + }); + + test.concurrent('With block', async () => { + const res = await exe(` + <: match 2 { + case 1 => 1 + case 2 => { + let a = 1 + let b = 2 + (a + b) + } + case 3 => 3 + } + `); + eq(res, NUM(3)); + }); + + test.concurrent('With return', async () => { + const res = await exe(` + @f(x) { + match x { + case 1 => { + return "ai" + } + } + "foo" + } + <: f(1) + `); + eq(res, STR('ai')); + }); +}); + +describe('exists', () => { + test.concurrent('Basic', async () => { + const res = await exe(` + let foo = null + <: [(exists foo), (exists bar)] + `); + eq(res, ARR([BOOL(true), BOOL(false)])); + }); +}); + diff --git a/test/testutils.ts b/test/testutils.ts new file mode 100644 index 00000000..f1e3a380 --- /dev/null +++ b/test/testutils.ts @@ -0,0 +1,36 @@ +import * as assert from 'assert'; +import { Parser, Interpreter } from '../src'; + +export async function exe(script: string): Promise { + const parser = new Parser(); + let result = undefined; + const interpreter = new Interpreter({}, { + out(value) { + if (!result) result = value; + else if (!Array.isArray(result)) result = [result, value]; + else result.push(value); + }, + log(type, {val}) { + if (type === 'end') result ??= val; + }, + maxStep: 9999, + }); + const ast = parser.parse(script); + await interpreter.exec(ast); + return result; +}; + +export const getMeta = (script: string) => { + const parser = new Parser(); + const ast = parser.parse(script); + + const metadata = Interpreter.collectMetadata(ast); + + return metadata; +}; + +export const eq = (a, b) => { + assert.deepEqual(a.type, b.type); + assert.deepEqual(a.value, b.value); +}; +