import { migrateMathJs } from '@cloudpss/expression/migrate'; import type { ExpressionSource } from '../dist/expression.js'; import { Scope } from '../dist/scope.js'; import { Evaluator, Expression, VmError, VmExtern } from '../dist/index.js'; import dedent from 'dedent'; const ext_args = new VmExtern({}); ext_args.assumeVmValue = ((_, __): boolean => true) as VmExtern['assumeVmValue']; const ext = new VmExtern({ func: (x: number, y: number, z: number) => x + y + z, includes: (x: unknown) => x, args: ext_args, }); const s = new Scope({ a: [1, 2, 3], m: [ [1, 2], [3, 4], ], n: null, f: 1, s: 'abc', sf: '12', b: false, o: { x: 1, y: 2, }, f_nan: Number.NaN, type: 'keyword', ext, get err(): number { throw new Error('test error'); }, }); const e = new Evaluator(); describe('migrate', () => { it('should migrate constant expressions', () => { const m = (e: string) => migrateMathJs(e as ExpressionSource, false, undefined); expect(m('')).toBe(''); expect(m(' ')).toBe(''); expect(m('undefined')).toBe(''); expect(m('null')).toBe('nil'); expect(m('nil')).toBe('nil'); expect(m('true')).toBe('true'); expect(m('false')).toBe('false'); expect(m('123')).toBe('123'); expect(m('-123.45e+6')).toBe('-123.45e+6'); expect(m('+123.45e-6')).toBe('+123.45e-6'); expect(m('.45e-6')).toBe('4.5e-7'); }); it('should migrate simple condition expressions', () => { const m = (e: string) => migrateMathJs(e as ExpressionSource, true, undefined); expect(m('')).toBe(''); expect(m(' ')).toBe(''); expect(m('undefined')).toBe(''); expect(m('null')).toBe('nil'); expect(m('nil')).toBe('nil'); expect(m('true')).toBe('true'); expect(m('false')).toBe('false'); expect(m('False')).toBe('false'); expect(m('TRUE')).toBe('true'); expect(m('0')).toBe('false'); expect(m(" '' ")).toBe('false'); expect(m('1')).toBe('true'); expect(m('"1"')).toBe('true'); }); it.each([ ['sin(0)', 'sin(0)', 0], ['sin(f)', 'sin(f)', Math.sin(1)], ['f', 'f', 1], ['o == null', 'o == nil', false], ['o == undefined', 'o == nil', false], ['o == undefined', 'o == nil', false], ['b == false', 'b =~ 0', true], ['true != b', '1 !~ b', true], ['f == true', 'f =~ 1', true], ['false == f', '0 =~ f', false], ['0 <= f + 1 <= 2', 'f + 1 is 0..2', true], ['$a', '$a', VmError], ['$a + 1', 'matrix.add($a, 1)', VmError], ['size(a)[1]', 'len(a)', 3], ['size(s)[1]', 'len(chars(s))', 3], [ 'size(null)[1]', dedent` return @@length(nil); // # 帮助函数 fn @@length(x) { if type(x) == 'string' { len(chars(x)) } else if type(x) == 'array' { len(x) } else { x.length ?? 0 } } // # 原始 math.js 表达式 // size(null)[1]`, 0, ], ['(a).length', 'len((a))', 3], ['(s).length', 'len(chars((s)))', 3], ['count(s)', 'len(chars(s))', 3], ['b ? true : false', 'b ?? false', false], ['b ? false : true', '!(b ?? false)', true], ['is(f, "string") ? count(f) != 0 : false', "type(f) == 'string' && f != ''", false], ['is(f, "string") ? equalText(f, "xyz") : false', "f == 'xyz'", false], ['b ? 1 : 2', 'if b { 1 } else { 2 }', 2], ['f >0', 'f > 0', true], ['f >=0', 'f >= 0', true], ['f <0', 'f < 0', false], ['f == 1', 'f =~ 1', true], ['f != 1', 'f !~ 1', false], ['1 1; a.filter(filter)', dedent` fn filter(x) { matrix.entrywise(x, 1, fn (a, b) { a > b }) } a::global.filter(filter) // # 转换日志 // - W: 使用了非精确转换的语法或函数,结果可能不准确 // # 原始 math.js 表达式 // filter(x) = x > 1; a.filter(filter)`, [2, 3], ], [`print('hello world, $s!', {s:s})`, '`hello world, $(s)!`', `hello world, abc!`], [`print('hello world, $1!', [s])`, '`hello world, $(s)!`', `hello world, abc!`], [`concat('Value: ', f, string(1 >= 0))`, '`Value: $(f)$(1 >= 0)`', 'Value: 1true'], [`concat(s, f, string(1 >= 0))`, '`$(s)$(f)$(1 >= 0)`', 'abc1true'], [ String.raw`'Value: '.concat('\\x', '$', 1, f, string(1 >= 0))`, '`Value: \\\\x\\$1$(f)$(1 >= 0)`', String.raw`Value: \x$11true`, ], [ `concat([1], a)`, dedent` flatten([[1], a]) // # 转换日志 // - W: 矩阵连接时结果可能不一致 // # 原始 math.js 表达式 // concat([1], a)`, [1, 1, 2, 3], ], [`equalText(type, "keyword")`, `global.type == 'keyword'`, true], [ `type = { "a": [1,2,3] }; type.a[1]`, dedent` let __type__ = (a: [1, 2, 3]); return @@maybe_chars(__type__.a)[0]; // # 帮助函数 fn @@maybe_chars(x) { if type(x) == 'string' { chars(x) } else { x } } // # 转换日志 // - W: 变量名 'type' 是 MiraScript 关键字,已转换为 '__type__' // # 原始 math.js 表达式 // type = { "a": [1,2,3] }; type.a[1]`, 1, ], [ '[1,2,3].map(f(x,y,z) = x^2)', dedent` return [1, 2, 3]::map(f); // # 帮助函数 fn f(x, y, z) { x^2 } // # 转换日志 // - W: 使用了非精确转换的语法或函数,结果可能不准确 // - W: 无法确定 '^' 的操作数为标量类型,计算结果可能不一致 // # 原始 math.js 表达式 // [1,2,3].map(f(x,y,z) = x^2)`, [1, 4, 9], ], ['', '', null], ['#', '//', null], ['#xx', '//xx', null], ['(random() - 1) < 0', '(random() - 1) < 0', true], [ 're(12) + im(i)', dedent` /* re */(12) + /* im */(i) // # 转换日志 // - E: 不支持复数 // # 原始 math.js 表达式 // re(12) + im(i)`, VmError, ], ['is(a, "number")', "type(a) == 'number'", false], ['is(a, "string")', "type(a) == 'string'", false], ['is(a, "boolean")', "type(a) == 'boolean'", false], ['is(a, "null")', "type(a) == 'nil'", false], ['is(a, "undefined")', "type(a) == 'nil'", false], [`sum([a > 1])`, `sum([matrix.entrywise(a, 1, fn (a, b) { a > b })]::flatten())`, 2], [`prod(1,2,3)`, `product(1, 2, 3)`, 6], [`sum(true)`, `sum(true)`, 1], [`sum(m)`, `sum(m::flatten())`, 10], [`prod([1,2,3])`, `product([1, 2, 3])`, 6], [`prod(f)`, `product(f)`, 1], [`abs(f) + round(f) + f`, `abs(f) + round(f) + f`, 3], [ `abs([1,2,3;-4,-5,-6])`, `matrix.entrywise([[1, 2, 3], [-4, -5, -6]], nil, abs)`, [ [1, 2, 3], [4, 5, 6], ], ], [`max(1, 2, f) + f`, `max(1, 2, f) + f`, 3], [`min(1, 2, f) + f`, `min(1, 2, f) + f`, 2], [ `max([1,2;3,4], 1)`, dedent` max([[1, 2], [3, 4]], 1) // # 转换日志 // - W: 函数行为可能不一致: max // # 原始 math.js 表达式 // max([1,2;3,4], 1)`, VmError, ], [`0:(f + 1)`, `[0..(f + 1)]`, [0, 1, 2]], [`3:1`, `[3..1]`, []], [`1:+1:2`, `[1..2]`, [1, 2]], [`3:-1:1`, `[1..3]`, [1, 2, 3]], [`1:4 + 1`, `[1..4 + 1]`, [1, 2, 3, 4, 5]], [`1.2:2*3`, `[1.2..2 * 3]`, [1.2, 2.2, 3.2, 4.2, 5.2]], [`a[1:2]`, 'a[0..1]', [1, 2]], [`a[2:-1:1]`, 'a[0..1]::reverse()', [2, 1]], [`not b and f > 0`, `!b && f > 0`, true], [ `n ? f : 0`, dedent` return if @@to_boolean(n) ?? false { f } else { 0 }; // # 帮助函数 fn @@to_boolean { if it is nil { nil } else { it != '' && it != '0' && it != 0 && it is not nan && it != false } } // # 原始 math.js 表达式 // n ? f : 0`, 0, ], [ `f ? f : 0`, dedent` return if @@to_boolean(f) ?? false { f } else { 0 }; // # 帮助函数 fn @@to_boolean { if it is nil { nil } else { it != '' && it != '0' && it != 0 && it is not nan && it != false } } // # 原始 math.js 表达式 // f ? f : 0`, 1, ], [ `[1,2,3,4][1:count([1,2,3,4])-1]`, dedent` [1, 2, 3, 4][0..(len([1, 2, 3, 4]) - 1 - 1)] // # 转换日志 // - W: 函数行为可能不一致: count // - W: 使用了非精确转换的语法或函数,结果可能不准确 // # 原始 math.js 表达式 // [1,2,3,4][1:count([1,2,3,4])-1]`, [1, 2, 3], ], [`f.toFixed(2)`, `f::format('.2')`, '1.00'], [`pi.toFixed()`, `PI::format('.0')`, '3'], [`pi.toString()`, `PI::to_string()`, String(Math.PI)], [ `format=3;f.toFixed(format)`, dedent` let format = 3; f::global.format('.$(format)')`, '1.000', ], [`a.includes(f)`, `(f in a)`, true], [`s.includes('a')`, `s::contains('a')`, true], [`a.concat([4,5])`, `flatten([a, [4, 5]])`, [1, 2, 3, 4, 5]], [`s.concat('def')`, '`$(s)def`', 'abcdef'], [ `(f > 0 ? a : s).concat(f)`, dedent` return (if f > 0 { a } else { s })::@@concat(f); // # 帮助函数 fn @@concat(it, ..args) { let arr = flatten([it,..args]); match it::type() { case 'string' { arr::join() } case _ { arr } } } // # 原始 math.js 表达式 // (f > 0 ? a : s).concat(f)`, [1, 2, 3, 1], ], [ `(f > 1 ? a : s).concat(f)`, dedent` return (if f > 1 { a } else { s })::@@concat(f); // # 帮助函数 fn @@concat(it, ..args) { let arr = flatten([it,..args]); match it::type() { case 'string' { arr::join() } case _ { arr } } } // # 原始 math.js 表达式 // (f > 1 ? a : s).concat(f)`, 'abc1', ], [`s.repeat(2)`, `s::repeat(2)::join()`, 'abcabc'], [`s.split('b')`, `s::split('b')`, ['a', 'c']], [`a.join('\`')`, `a::join(\`\\\`\`)`, '1`2`3'], [`a.join(f)`, `a::join(\`$(f)\`)`, '11213'], [`[a, s, f, 'x'].join()`, '`$(a),$(s),$(f),x`', '1, 2, 3,abc,1,x'], [`0xffffi16`, `-1`, -1], [ `is(ext.value.rt_state, 'Object') ? (ext.value.rt_state.monitoring ? '暂停输出' : '恢复输出') : '暂停输出'`, dedent` return if ext.value.rt_state != nil { if @@to_boolean(ext.value.rt_state.monitoring) ?? false { '暂停输出' } else { '恢复输出' } } else { '暂停输出' }; // # 帮助函数 fn @@to_boolean { if it is nil { nil } else { it != '' && it != '0' && it != 0 && it is not nan && it != false } } // # 原始 math.js 表达式 // is(ext.value.rt_state, 'Object') ? (ext.value.rt_state.monitoring ? '暂停输出' : '恢复输出') : '暂停输出'`, '暂停输出', ], [ `(b ? (ext.args['charge_time'] = 5) : (ext.args['charge_time'] = 999))`, dedent` if b { let ret = 5; ext.args.charge_time = ret; ret } else { let ret = 999; ext.args.charge_time = ret; ret } // # 转换日志 // - W: 使用了非精确转换的语法或函数,结果可能不准确 // # 原始 math.js 表达式 // (b ? (ext.args['charge_time'] = 5) : (ext.args['charge_time'] = 999))`, 999, ], [ `b ? a = 1 : a = 2`, dedent` panic("无法从 math.js 转换为 MiraScript"); if b { (/* a = 1 */) } else { (/* a = 2 */) } // # 转换日志 // - C: 不支持在表达式中创建局部变量 // # 原始 math.js 表达式 // b ? a = 1 : a = 2 `, VmError, ], [ `ext.x = 12`, dedent` { let ret = 12; ext.x = ret; ret } // # 转换日志 // - W: 使用了非精确转换的语法或函数,结果可能不准确 // # 原始 math.js 表达式 // ext.x = 12 `, 12, ], [ `ext.y = 1; ext.y + 1`, dedent` ext.y = 1; matrix.add(ext.y, 1) // # 转换日志 // - W: 使用了非精确转换的语法或函数,结果可能不准确 // # 原始 math.js 表达式 // ext.y = 1; ext.y + 1`, 2, ], [ `ext.func(1,2,not f)`, dedent` return ext.func(1, 2, !@@to_boolean(f)); // # 帮助函数 fn @@to_boolean { if it is nil { nil } else { it != '' && it != '0' && it != 0 && it is not nan && it != false } } // # 原始 math.js 表达式 // ext.func(1,2,not f)`, 3, ], [ `ext.func(1,2,not f);`, dedent` ext.func(1, 2, !@@to_boolean(f)); // # 帮助函数 fn @@to_boolean { if it is nil { nil } else { it != '' && it != '0' && it != 0 && it is not nan && it != false } } // # 原始 math.js 表达式 // ext.func(1,2,not f);`, null, ], [`ext.includes(f)`, `ext.includes(f)`, 1], [ `a.map(f(x, i, a) = x + i)`, dedent` return a::map(f); // # 帮助函数 fn f(x, i, a) { matrix.add(x, i) } // # 转换日志 // - W: 使用了非精确转换的语法或函数,结果可能不准确 // # 原始 math.js 表达式 // a.map(f(x, i, a) = x + i)`, [1, 3, 5], ], [ `s.replace('a', 'x')`, dedent` s::replace('a', 'x') // # 转换日志 // - W: MiraScript 的 'replace' 函数会替换全部结果 // # 原始 math.js 表达式 // s.replace('a', 'x')`, 'xbc', ], [`s.replaceAll('a', 'x')`, `s::replace('a', 'x')`, 'xbc'], [`a.find(1)`, `a::find(1).1`, 1], [`a.findIndex(1)`, `(a::find(1).0 ?? -1)`, 0], [ `reverse = [1,2,3];reverse.reverse()`, dedent` let reverse = [1, 2, 3]; reverse::global.reverse()`, [3, 2, 1], ], [ `a.flatMap(f(x, i) = [x, x^2, [i]])`, dedent` return a::map(f)::flatten(); // # 帮助函数 fn f(x, i) { [x, x^2, [i]] } // # 转换日志 // - W: 使用了非精确转换的语法或函数,结果可能不准确 // - W: 无法确定 '^' 的操作数为标量类型,计算结果可能不一致 // # 原始 math.js 表达式 // a.flatMap(f(x, i) = [x, x^2, [i]])`, [1, 1, [0], 2, 4, [1], 3, 9, [2]], ], [ `typeOf(f)`, dedent` type(f) // # 转换日志 // - W: 'type' 与 'typeOf' 结果可能不同 // # 原始 math.js 表达式 // typeOf(f)`, 'number', ], [`norm(a)`, `hypot(a)`, Math.hypot(1, 2, 3)], [`norm([1,2])`, `hypot(1, 2)`, Math.hypot(1, 2)], [ `zeros(2,3)`, `matrix.zeros(2, 3)`, [ [0, 0, 0], [0, 0, 0], ], ], [ `diag([1,2,3])`, `matrix.diagonal([1, 2, 3])`, [ [1, 0, 0], [0, 2, 0], [0, 0, 3], ], ], [`inv(2)`, `matrix.invert(2)`, 0.5], [`gamma(5)`, `gamma(5)`, 24], [`factorial([1,2,3])`, `matrix.entrywise([1, 2, 3], nil, factorial)`, [1, 2, 6]], [`s.toLowerCase()`, `s::to_lowercase()`, 'abc'], [`"xyz".toUpperCase()`, `'xyz'::to_uppercase()`, 'XYZ'], [`fix([1.5, 2.6, -1.7])`, `matrix.entrywise([1.5, 2.6, -1.7], nil, trunc)`, [1, 2, -1]], [`round([1.54, 2.63, -1.71], 1)`, `matrix.entrywise([1.54, 2.63, -1.71], 1, round)`, [1.5, 2.6, -1.7]], [`ceil(1.32, 1)`, `ceil(1.32, 1)`, 1.4], [`max([1,2,3])`, `max([1, 2, 3])`, 3], [ `n ? true : false`, dedent` return @@to_boolean(n) ?? false; // # 帮助函数 fn @@to_boolean { if it is nil { nil } else { it != '' && it != '0' && it != 0 && it is not nan && it != false } } // # 原始 math.js 表达式 // n ? true : false`, false, ], [ `n ? false : true`, dedent` return !(@@to_boolean(n) ?? false); // # 帮助函数 fn @@to_boolean { if it is nil { nil } else { it != '' && it != '0' && it != 0 && it is not nan && it != false } } // # 原始 math.js 表达式 // n ? false : true`, true, ], [`isNaN(f_nan) ? 140 : f_nan`, `if f_nan is nan { 140 } else { f_nan }`, 140], [`isInteger(f * 3)`, `((f * 3) % 1) == 0`, true], [`isFinite(sf)`, `to_number(sf) is > -inf and < inf`, true], [`isNaN([f, f_nan])`, `matrix.entrywise([f, f_nan], nil, fn { to_number(it) is nan })`, [false, true]], [`add(1, 3)`, `matrix.add(1, 3)`, 4], [`subtract([1,2], [3,4])`, `matrix.subtract([1, 2], [3, 4])`, [-2, -2]], [`multiply(2, [3,4])`, `matrix.multiply(2, [3, 4])`, [6, 8]], [`divide([3,4], 2)`, `matrix.multiply([3, 4], matrix.invert(2))`, [1.5, 2]], [`dotMultiply([1,2], [3,4])`, `matrix.entrywise_multiply([1, 2], [3, 4])`, [3, 8]], [`dotDivide([3,4], [1,2])`, `matrix.entrywise_divide([3, 4], [1, 2])`, [3, 2]], [`equal(1, 1)`, `matrix.entrywise(1, 1, fn (a, b) { a =~ b })`, true], [ `compare(2, [1, 2, 3])`, `matrix.entrywise(2, [1, 2, 3], fn (a, b) { if a < b { -1 } else if a > b { 1 } else { 0 } })`, [1, 0, -1], ], [ `and(1, [0, 1, -1])`, dedent` return matrix.entrywise(1, [0, 1, -1], fn (a, b) { @@to_boolean(a) && @@to_boolean(b) }); // # 帮助函数 fn @@to_boolean { if it is nil { nil } else { it != '' && it != '0' && it != 0 && it is not nan && it != false } } // # 原始 math.js 表达式 // and(1, [0, 1, -1])`, [false, true, true], ], [ `err`, dedent` err // # 转换日志 // - W: 全局变量 err 求值失败: test error // # 原始 math.js 表达式 // err`, /test error/, ], [ `ext.args.arr = []; [b ? ext.args.arr.push(1) : ext.args.arr.push(2), ext.args.arr]`, dedent` ext.args.arr = []; [if b { let ret = 1; ext.args.arr = [..ext.args.arr, ret]; ret } else { let ret = 2; ext.args.arr = [..ext.args.arr, ret]; ret }, ext.args.arr] // # 转换日志 // - W: 使用了非精确转换的语法或函数,结果可能不准确 // - W: 'push' 的行为可能不一致 // # 原始 math.js 表达式 // ext.args.arr = []; [b ? ext.args.arr.push(1) : ext.args.arr.push(2), ext.args.arr]`, [2, [2]], ], [ `ext.args.arr = 1:4; [ext.args.arr.pop(), ext.args.arr]`, dedent` ext.args.arr = [1..4]; [{ let ret = ext.args.arr[-1]; ext.args.arr = ext.args.arr[0..<-1]; ret }, ext.args.arr] // # 转换日志 // - W: 使用了非精确转换的语法或函数,结果可能不准确 // - W: 'pop' 的行为可能不一致 // # 原始 math.js 表达式 // ext.args.arr = 1:4; [ext.args.arr.pop(), ext.args.arr]`, [4, [1, 2, 3]], ], [ `fromJson(1)`, dedent` return @@from_json(1); // # 帮助函数 fn @@from_json(s) { if type(s) == 'string' { from_json(s, s) } else { s } } // # 原始 math.js 表达式 // fromJson(1)`, 1, ], [ `fromJson("not json")`, dedent` return @@from_json('not json'); // # 帮助函数 fn @@from_json(s) { if type(s) == 'string' { from_json(s, s) } else { s } } // # 原始 math.js 表达式 // fromJson("not json")`, 'not json', ], [`toJson({x:1})`, `to_json((x: 1))`, '{"x":1}'], ])('should migrate expression: %s', (f, t, r) => { const migrated = migrateMathJs(f as ExpressionSource, false, s); expect(migrated).toBe(t); if ((typeof r === 'function' && r.prototype instanceof Error) || r instanceof RegExp) { expect(() => { e.evaluate(Expression(migrated), s); }).toThrow(r); } else { const result = e.evaluate(Expression(migrated), s); expect(result).toEqual(r); } }); });