MIRROR: javascript for ๐Ÿœ's, a tiny runtime with big ambitions
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

#16: tail-call optimization, computed keys, and more!

authored by

Mack and committed by
GitHub
b9324ced 8ea2e11c

+1554 -129
examples/demo/fibonacci.js examples/demo/fibonacci_memo.js
+11
examples/demo/fibonacci_tco.js
··· 1 + function fib(n, a = 0, b = 1) { 2 + if (n === 0) return a; 3 + return fib(n - 1, b, a + b); 4 + } 5 + 6 + const start = performance.now(); 7 + const result = fib(30); 8 + const end = performance.now(); 9 + 10 + console.log(`fibonacci(30) = ${result}`); 11 + console.log(`Time: ${(end - start).toFixed(2)} ms`);
examples/demo/tco_fib.js examples/demo/fibonacci_recursive.js
+114
examples/spec/class_computed_key.js
··· 1 + import { test, summary } from './helpers.js'; 2 + 3 + console.log('Class Computed Property Key Tests\n'); 4 + 5 + const key = 'x'; 6 + class BasicComputed { 7 + [key] = 10; 8 + } 9 + test('basic computed field', new BasicComputed().x, 10); 10 + 11 + class ComputedExpr { 12 + ['a' + 'b'] = 42; 13 + } 14 + test('computed field with concat', new ComputedExpr().ab, 42); 15 + 16 + let counter = 0; 17 + function makeKey() { 18 + counter++; 19 + return 'field' + counter; 20 + } 21 + 22 + class SideEffectKey { 23 + [makeKey()] = 'val'; 24 + } 25 + const seInstance = new SideEffectKey(); 26 + test('side-effect key evaluated (field name)', seInstance.field1, 'val'); 27 + test('side-effect key eval count after class def', counter, 1); 28 + 29 + const before = counter; 30 + const se2 = new SideEffectKey(); 31 + test('side-effect key not re-evaluated on new instance', counter, before); 32 + test('second instance has same key', se2.field1, 'val'); 33 + 34 + let order = []; 35 + function track(name) { 36 + order.push(name); 37 + return name; 38 + } 39 + 40 + class MultiComputed { 41 + [track('a')] = 1; 42 + [track('b')] = 2; 43 + [track('c')] = 3; 44 + } 45 + const mc = new MultiComputed(); 46 + test('multi computed field a', mc.a, 1); 47 + test('multi computed field b', mc.b, 2); 48 + test('multi computed field c', mc.c, 3); 49 + 50 + class ComputedMethod { 51 + ['say' + 'Hi']() { 52 + return 'hello'; 53 + } 54 + } 55 + test('computed method', new ComputedMethod().sayHi(), 'hello'); 56 + 57 + const prefix = 'get'; 58 + const suffix = 'Name'; 59 + class ComputedVarExpr { 60 + [prefix + suffix]() { 61 + return 'Alice'; 62 + } 63 + } 64 + test('computed method from vars', new ComputedVarExpr().getName(), 'Alice'); 65 + 66 + class StaticComputed { 67 + static ['s' + 'val'] = 99; 68 + } 69 + test('static computed field', StaticComputed.sval, 99); 70 + 71 + let staticCounter = 0; 72 + function staticKey() { 73 + staticCounter++; 74 + return 'sk' + staticCounter; 75 + } 76 + 77 + class StaticSideEffect { 78 + static [staticKey()] = 'static_val'; 79 + } 80 + test('static side-effect key', StaticSideEffect.sk1, 'static_val'); 81 + test('static side-effect count', staticCounter, 1); 82 + 83 + class NumericKey { 84 + [2 + 3] = 'five'; 85 + } 86 + test('numeric computed key', new NumericKey()[5], 'five'); 87 + 88 + const dynKey = Symbol('dyn'); 89 + class SymbolKey { 90 + [dynKey] = 'symbol_val'; 91 + } 92 + test('symbol computed key', new SymbolKey()[dynKey], 'symbol_val'); 93 + 94 + const flag = true; 95 + class TernaryKey { 96 + [flag ? 'yes' : 'no'] = 100; 97 + } 98 + test('ternary computed key true', new TernaryKey().yes, 100); 99 + 100 + const flag2 = false; 101 + class TernaryKey2 { 102 + [flag2 ? 'yes' : 'no'] = 200; 103 + } 104 + test('ternary computed key false', new TernaryKey2().no, 200); 105 + 106 + function base() { 107 + return 'item'; 108 + } 109 + class CallArithKey { 110 + [base() + '_' + (1 + 2)] = 'combined'; 111 + } 112 + test('call+arith computed key', new CallArithKey().item_3, 'combined'); 113 + 114 + summary();
+141
examples/spec/private_classes.js
··· 1 + import { test, summary } from './helpers.js'; 2 + 3 + console.log('Private Class Tests\n'); 4 + 5 + // Private instance fields 6 + class Counter { 7 + #count = 0; 8 + increment() { this.#count++; } 9 + get value() { return this.#count; } 10 + } 11 + const counter = new Counter(); 12 + test('private field initial', counter.value, 0); 13 + counter.increment(); 14 + counter.increment(); 15 + test('private field after increment', counter.value, 2); 16 + 17 + // Private field with constructor 18 + class Box { 19 + #content; 20 + constructor(val) { this.#content = val; } 21 + unwrap() { return this.#content; } 22 + } 23 + test('private field via constructor', new Box(42).unwrap(), 42); 24 + test('private field string', new Box('hello').unwrap(), 'hello'); 25 + 26 + // Private field does not collide with public 27 + class Dual { 28 + #x = 'private'; 29 + x = 'public'; 30 + getPrivate() { return this.#x; } 31 + getPublic() { return this.x; } 32 + } 33 + const dual = new Dual(); 34 + test('private vs public field', dual.getPrivate(), 'private'); 35 + test('public field unaffected', dual.getPublic(), 'public'); 36 + 37 + // Private instance methods 38 + class Formatter { 39 + #prefix; 40 + constructor(prefix) { this.#prefix = prefix; } 41 + #format(str) { return this.#prefix + ': ' + str; } 42 + display(str) { return this.#format(str); } 43 + } 44 + test('private method', new Formatter('LOG').display('test'), 'LOG: test'); 45 + 46 + // Private method does not collide with public 47 + class Methods { 48 + #x() { return 'private'; } 49 + x() { return this.#x(); } 50 + } 51 + test('private method call', new Methods().x(), 'private'); 52 + 53 + // Private getters and setters 54 + class Temperature { 55 + #celsius = 0; 56 + get #c() { return this.#celsius; } 57 + set #c(val) { this.#celsius = val; } 58 + get fahrenheit() { return this.#c * 9 / 5 + 32; } 59 + set fahrenheit(val) { this.#c = (val - 32) * 5 / 9; } 60 + } 61 + const temp = new Temperature(); 62 + test('private getter initial', temp.fahrenheit, 32); 63 + temp.fahrenheit = 212; 64 + test('private setter', temp.fahrenheit, 212); 65 + 66 + // Private static methods 67 + class MathHelper { 68 + static #square(n) { return n * n; } 69 + static calc(n) { return MathHelper.#square(n); } 70 + } 71 + test('private static method', MathHelper.calc(5), 25); 72 + 73 + // Private static accessors 74 + class Config { 75 + static #data = null; 76 + static get #config() { return Config.#data || 'default'; } 77 + static set #config(val) { Config.#data = val; } 78 + static get() { return Config.#config; } 79 + static set(val) { Config.#config = val; } 80 + } 81 + test('private static getter default', Config.get(), 'default'); 82 + Config.set('custom'); 83 + test('private static setter', Config.get(), 'custom'); 84 + 85 + // Private static fields 86 + class IdGen { 87 + static #next = 1; 88 + static generate() { return IdGen.#next++; } 89 + } 90 + test('private static field 1', IdGen.generate(), 1); 91 + test('private static field 2', IdGen.generate(), 2); 92 + test('private static field 3', IdGen.generate(), 3); 93 + 94 + // Computed instance fields 95 + class Computed { 96 + ['x'] = 10; 97 + ['y'] = 20; 98 + } 99 + const comp = new Computed(); 100 + test('computed instance field x', comp.x, 10); 101 + test('computed instance field y', comp.y, 20); 102 + 103 + // Computed static fields 104 + class ComputedStatic { 105 + static ['a'] = 100; 106 + static ['b'] = 200; 107 + } 108 + test('computed static field a', ComputedStatic.a, 100); 109 + test('computed static field b', ComputedStatic.b, 200); 110 + 111 + // Static initialization blocks 112 + let staticVal = 0; 113 + class WithStaticBlock { 114 + static { staticVal = 42; } 115 + } 116 + test('static init block', staticVal, 42); 117 + 118 + let staticSum = 0; 119 + class MultiBlock { 120 + static { staticSum += 10; } 121 + static { staticSum += 20; } 122 + } 123 + test('multiple static blocks', staticSum, 30); 124 + 125 + // Inheritance with private fields 126 + class Animal { 127 + #name; 128 + constructor(name) { this.#name = name; } 129 + getName() { return this.#name; } 130 + } 131 + class Dog extends Animal { 132 + #breed; 133 + constructor(name, breed) { 134 + super(name); 135 + this.#breed = breed; 136 + } 137 + describe() { return this.getName() + ' (' + this.#breed + ')'; } 138 + } 139 + test('inherited private fields', new Dog('Rex', 'Labrador').describe(), 'Rex (Labrador)'); 140 + 141 + summary();
+142
examples/spec/tco.js
··· 1 + import { test, summary } from './helpers.js'; 2 + 3 + console.log('Tail Call Optimization Tests\n'); 4 + 5 + function factorial(n, acc = 1) { 6 + if (n <= 1) return acc; 7 + return factorial(n - 1, n * acc); 8 + } 9 + test('tail factorial(10)', factorial(10), 3628800); 10 + test('tail factorial(20)', factorial(20), 2432902008176640000); 11 + 12 + function countDown(n) { 13 + if (n <= 0) return 'done'; 14 + return countDown(n - 1); 15 + } 16 + test('deep tail recursion', countDown(100000), 'done'); 17 + 18 + function sum(n, acc = 0) { 19 + if (n <= 0) return acc; 20 + return sum(n - 1, acc + n); 21 + } 22 + test('tail sum(100000)', sum(100000), 5000050000); 23 + 24 + function fib(n, a = 0, b = 1) { 25 + if (n === 0) return a; 26 + if (n === 1) return b; 27 + return fib(n - 1, b, a + b); 28 + } 29 + test('tail fib(10)', fib(10), 55); 30 + test('tail fib(30)', fib(30), 832040); 31 + 32 + function isEven(n) { 33 + if (n === 0) return true; 34 + return isOdd(n - 1); 35 + } 36 + function isOdd(n) { 37 + if (n === 0) return false; 38 + return isEven(n - 1); 39 + } 40 + test('mutual isEven(10)', isEven(10), true); 41 + test('mutual isOdd(11)', isOdd(11), true); 42 + test('mutual isEven(10000)', isEven(10000), true); 43 + 44 + function normalFib(n) { 45 + if (n <= 1) return n; 46 + return normalFib(n - 1) + normalFib(n - 2); 47 + } 48 + test('non-tail recursion', normalFib(10), 55); 49 + 50 + function normalFactorial(n) { 51 + if (n <= 1) return 1; 52 + return n * normalFactorial(n - 1); 53 + } 54 + test('non-tail factorial', normalFactorial(5), 120); 55 + 56 + function tailMultiArg(a, b, c) { 57 + if (a <= 0) return b + c; 58 + return tailMultiArg(a - 1, b + 1, c + 2); 59 + } 60 + test('tail multi-arg', tailMultiArg(1000, 0, 0), 3000); 61 + 62 + function ternaryTail(n) { 63 + return n <= 0 ? 'done' : ternaryTail(n - 1); 64 + } 65 + test('ternary tail', ternaryTail(50000), 'done'); 66 + 67 + function lastElement(arr, i = 0) { 68 + if (i >= arr.length - 1) return arr[i]; 69 + return lastElement(arr, i + 1); 70 + } 71 + test('tail lastElement', lastElement([1, 2, 3, 4, 5]), 5); 72 + 73 + function ternaryBothBare(n) { 74 + return n <= 0 ? ternaryBothBare_done(n) : ternaryBothBare(n - 1); 75 + } 76 + function ternaryBothBare_done(_) { 77 + return 'done'; 78 + } 79 + test('ternary both bare calls', ternaryBothBare(100000), 'done'); 80 + 81 + function ternaryThenBinop(n) { 82 + return n <= 0 ? identity(0) + 1 : ternaryThenBinop(n - 1); 83 + } 84 + function identity(x) { 85 + return x; 86 + } 87 + test('ternary then-branch binop', ternaryThenBinop(5), 1); 88 + 89 + function ternaryElseBinop(n) { 90 + return n <= 0 ? identity(42) : identity(n) * 2; 91 + } 92 + test('ternary else-branch binop', ternaryElseBinop(0), 42); 93 + test('ternary else-branch binop non-zero', ternaryElseBinop(3), 6); 94 + 95 + function ternaryBothBinop(n) { 96 + return n <= 0 ? identity(10) + 5 : identity(n) - 1; 97 + } 98 + test('ternary both branches binop', ternaryBothBinop(0), 15); 99 + test('ternary both branches binop non-zero', ternaryBothBinop(7), 6); 100 + 101 + function ternaryNestedArith(n) { 102 + return n <= 0 ? (identity(2) + 3) * identity(4) : ternaryNestedArith(n - 1); 103 + } 104 + test('ternary nested arith in then', ternaryNestedArith(5), 20); 105 + 106 + function ternaryParenBareCall(n) { 107 + return n <= 0 ? ternaryParenBareCall_end() : ternaryParenBareCall(n - 1); 108 + } 109 + function ternaryParenBareCall_end() { 110 + return 'end'; 111 + } 112 + test('ternary paren bare call', ternaryParenBareCall(100000), 'end'); 113 + 114 + function ternaryCallWithBinopArg(n) { 115 + return n <= 0 ? identity(2 + 3) : ternaryCallWithBinopArg(n - 1); 116 + } 117 + test('ternary call with binop arg', ternaryCallWithBinopArg(100000), 5); 118 + 119 + function ternaryNested(n, branch) { 120 + return branch === 0 ? ternaryNested_a(n) : branch === 1 ? ternaryNested_b(n) : ternaryNested_c(n); 121 + } 122 + function ternaryNested_a(_) { 123 + return 'a'; 124 + } 125 + function ternaryNested_b(_) { 126 + return 'b'; 127 + } 128 + function ternaryNested_c(_) { 129 + return 'c'; 130 + } 131 + test('nested ternary branch 0', ternaryNested(1, 0), 'a'); 132 + test('nested ternary branch 1', ternaryNested(1, 1), 'b'); 133 + test('nested ternary branch 2', ternaryNested(1, 2), 'c'); 134 + 135 + function ternaryNestedUnsafe(n, branch) { 136 + return branch === 0 ? identity(n) + 1 : branch === 1 ? identity(n) : identity(n) * 3; 137 + } 138 + test('nested ternary unsafe then', ternaryNestedUnsafe(5, 0), 6); 139 + test('nested ternary safe middle', ternaryNestedUnsafe(5, 1), 5); 140 + test('nested ternary unsafe else', ternaryNestedUnsafe(5, 2), 15); 141 + 142 + summary();
+114
examples/spec/tco_brackets.js
··· 1 + import { test, summary } from './helpers.js'; 2 + 3 + console.log('TCO Bracket Edge Cases\n'); 4 + 5 + function indexCallResult(n) { 6 + if (n <= 0) return [42]; 7 + return indexCallResult(n - 1); 8 + } 9 + test('f() then index (small)', indexCallResult(5)[0], 42); 10 + 11 + function indexByCall(n) { 12 + if (n <= 0) return [99]; 13 + return indexByCall(n - 1); 14 + } 15 + function zeroFn() { 16 + return 0; 17 + } 18 + test('f()[g()] (small)', indexByCall(3)[zeroFn()], 99); 19 + 20 + function returnBracketAccess(arr) { 21 + return arr[0]; 22 + } 23 + test('bare bracket access', returnBracketAccess([7]), 7); 24 + 25 + function indexArrByCall(arr) { 26 + return arr[zeroFn()]; 27 + } 28 + test('arr[f()] access', indexArrByCall([55]), 55); 29 + 30 + const methods = { 31 + greet() { 32 + return 'hello'; 33 + }, 34 + farewell() { 35 + return 'bye'; 36 + } 37 + }; 38 + function computedCall(key) { 39 + return methods[key](); 40 + } 41 + test('obj[key]() simple', computedCall('greet'), 'hello'); 42 + test('obj[key]() simple 2', computedCall('farewell'), 'bye'); 43 + 44 + const ops = { 45 + dec(n) { 46 + return recurseViaComputed(n - 1); 47 + } 48 + }; 49 + function recurseViaComputed(n) { 50 + if (n <= 0) return 'done'; 51 + return ops['dec'](n); 52 + } 53 + test('obj[key]() deep recursion', recurseViaComputed(100000), 'done'); 54 + 55 + function callWithBracketArg(arr) { 56 + return identity(arr[0]); 57 + } 58 + function identity(x) { 59 + return x; 60 + } 61 + test('f(a[0]) bracket in arg', callWithBracketArg([33]), 33); 62 + 63 + function callWithBracketBinopArg(arr) { 64 + return identity(arr[0] + arr[1]); 65 + } 66 + test('f(a[0]+a[1]) binop inside args', callWithBracketBinopArg([10, 20]), 30); 67 + 68 + function recurseWithBracketArg(arr, i) { 69 + if (i >= arr.length) return 0; 70 + return recurseWithBracketArg(arr, i + 1); 71 + } 72 + test('deep with bracket arg', recurseWithBracketArg(new Array(100000), 0), 0); 73 + 74 + const handlers = {}; 75 + for (let i = 0; i < 10; i++) { 76 + handlers['h' + i] = function (n) { 77 + if (n <= 0) return 'handled'; 78 + return handlers['h' + i](n - 1); 79 + }; 80 + } 81 + test('obj[key+expr]() result', handlers['h0'](50), 'handled'); 82 + 83 + const dispatch = { 84 + action_run(n) { 85 + if (n <= 0) return 'ran'; 86 + return dispatch['action' + '_' + 'run'](n - 1); 87 + } 88 + }; 89 + test('obj[a+b]() deep (may fail without bracket fix)', dispatch['action_run'](100000), 'ran'); 90 + 91 + const table = []; 92 + for (let i = 0; i < 20; i++) { 93 + table[i] = function () { 94 + return i; 95 + }; 96 + } 97 + function callFromTable(a) { 98 + return table[a * 2 + 1](); 99 + } 100 + test('table[a*2+1]() computed', callFromTable(3), 7); 101 + 102 + function ternaryBracket(arr, flag) { 103 + return flag ? identity(arr[0]) : identity(arr[1]); 104 + } 105 + test('ternary with bracket args true', ternaryBracket([10, 20], true), 10); 106 + test('ternary with bracket args false', ternaryBracket([10, 20], false), 20); 107 + 108 + function ternaryComputedVsPlain(flag, n) { 109 + if (n <= 0) return 'end'; 110 + return flag ? ops['dec'](n) : ternaryComputedVsPlain(flag, n - 1); 111 + } 112 + test('ternary computed vs plain', ternaryComputedVsPlain(false, 100000), 'end'); 113 + 114 + summary();
+86
examples/spec/tco_shift.js
··· 1 + import { test, summary } from './helpers.js'; 2 + 3 + console.log('TCO Shift/Comparison Operator Tests\n'); 4 + 5 + function identity(x) { 6 + return x; 7 + } 8 + 9 + function shiftLeft(n) { 10 + if (n <= 0) return identity(1) << 3; 11 + return shiftLeft(n - 1); 12 + } 13 + test('f() << N result', shiftLeft(5), 8); 14 + 15 + function shiftRight(n) { 16 + if (n <= 0) return identity(64) >> 2; 17 + return shiftRight(n - 1); 18 + } 19 + test('f() >> N result', shiftRight(5), 16); 20 + 21 + function shiftRightUnsigned(n) { 22 + if (n <= 0) return identity(-1) >>> 24; 23 + return shiftRightUnsigned(n - 1); 24 + } 25 + test('f() >>> N result', shiftRightUnsigned(5), 255); 26 + 27 + function lessThan(n) { 28 + if (n <= 0) return identity(3) < 5; 29 + return lessThan(n - 1); 30 + } 31 + test('f() < N result', lessThan(3), true); 32 + 33 + function greaterThan(n) { 34 + if (n <= 0) return identity(10) > 5; 35 + return greaterThan(n - 1); 36 + } 37 + test('f() > N result', greaterThan(3), true); 38 + 39 + function lessEq(n) { 40 + if (n <= 0) return identity(5) <= 5; 41 + return lessEq(n - 1); 42 + } 43 + test('f() <= N result', lessEq(3), true); 44 + 45 + function greaterEq(n) { 46 + if (n <= 0) return identity(5) >= 10; 47 + return greaterEq(n - 1); 48 + } 49 + test('f() >= N result', greaterEq(3), false); 50 + 51 + function deepShiftLeft(n) { 52 + if (n <= 0) return identity(1) << 3; 53 + return deepShiftLeft(n - 1); 54 + } 55 + test('deep f()<<N value correct', deepShiftLeft(500), 8); 56 + 57 + function deepShiftRight(n) { 58 + if (n <= 0) return identity(64) >> 2; 59 + return deepShiftRight(n - 1); 60 + } 61 + test('deep f()>>N value correct', deepShiftRight(500), 16); 62 + 63 + function deepUnsignedShift(n) { 64 + if (n <= 0) return identity(-1) >>> 24; 65 + return deepUnsignedShift(n - 1); 66 + } 67 + test('deep f()>>>N value correct', deepUnsignedShift(500), 255); 68 + 69 + function recurseShiftArg(n) { 70 + if (n <= 0) return 'done'; 71 + return recurseShiftArg((n - 1) >> 0); 72 + } 73 + test('shift inside arg (tail-eligible)', recurseShiftArg(100000), 'done'); 74 + 75 + function ternaryShift(n) { 76 + return n <= 0 ? identity(1) << 4 : ternaryShift(n - 1); 77 + } 78 + test('ternary with shift in then-branch', ternaryShift(5), 16); 79 + 80 + function ternaryBothShift(flag) { 81 + return flag ? identity(1) << 2 : identity(1) >> 1; 82 + } 83 + test('ternary both shift true', ternaryBothShift(true), 4); 84 + test('ternary both shift false', ternaryBothShift(false), 0); 85 + 86 + summary();
+17 -4
include/internal.h
··· 12 12 extern UT_array *saved_scope_stack; 13 13 14 14 struct for_let_ctx { 15 - const char *var_name; // interned variable name 16 - jsoff_t var_len; // length of var name 17 - jsoff_t prop_off; // offset of var property in loop scope 18 - jsval_t body_scope; // loop body scope for capturing block-scoped vars 15 + const char *var_name; 16 + jsoff_t var_len; 17 + jsoff_t prop_off; 18 + jsval_t body_scope; 19 19 }; 20 20 21 21 struct js { ··· 66 66 int parse_depth; // recursion depth of parser (for stack overflow protection) 67 67 bool skip_func_hoist; // skip function declaration hoisting (pre-computed) 68 68 bool fatal_error; // fatal error that should bypass promise rejection handling 69 + bool tail_ctx; // true when evaluating a return expression (for TCO) 70 + bool has_tail_calls; // cached: current function body has tail-call-eligible returns 71 + 72 + struct { 73 + jsval_t func; 74 + jsval_t closure_scope; 75 + const char *code_str; 76 + jsoff_t fnlen; 77 + jsval_t *args; 78 + int argc; 79 + bool pending; 80 + } tc; 69 81 70 82 struct for_let_ctx *for_let_stack; 71 83 int for_let_stack_len; ··· 120 132 121 133 #define TYPE_FLAG(t) (1u << (t)) 122 134 #define T_EMPTY (NANBOX_PREFIX | ((jsval_t)T_FFI << NANBOX_TYPE_SHIFT) | 0xDEADULL) 135 + #define T_TAILCALL (NANBOX_PREFIX | ((jsval_t)T_FFI << NANBOX_TYPE_SHIFT) | 0x7A1CULL) 123 136 124 137 #define T_SPECIAL_OBJECT_MASK (TYPE_FLAG(T_OBJ) | TYPE_FLAG(T_ARR)) 125 138 #define T_NEEDS_PROTO_FALLBACK (TYPE_FLAG(T_FUNC) | TYPE_FLAG(T_ARR) | TYPE_FLAG(T_PROMISE))
+1
include/modules/symbol.h
··· 7 7 8 8 void init_symbol_module(void); 9 9 bool is_symbol_key(const char *key, size_t key_len); 10 + int sym_to_prop_key(jsval_t sym, char *buf, size_t bufsz); 10 11 11 12 const char *get_iterator_sym_key(void); 12 13 const char *get_asyncIterator_sym_key(void);
+1 -1
meson/ant.version
··· 1 - 0.5.3 1 + 0.6.0
+3 -1
meson/version/meson.build
··· 1 + fs = import('fs') 1 2 git_hash = run_command('git', 'rev-parse', '--short', 'HEAD', check: false).stdout().strip() 2 3 git_hash = git_hash != '' ? git_hash : 'unknown' 3 4 4 5 timestamp_opt = get_option('build_timestamp') 5 6 timestamp = timestamp_opt != '' ? timestamp_opt : run_command('date', '+%s', check: true).stdout().strip() 6 7 7 - ant_version = '0.5.3.' + timestamp + '-g' + git_hash 8 + ant_base_version = fs.read(meson.project_source_root() / 'meson' / 'ant.version').strip() 9 + ant_version = ant_base_version + '.' + timestamp + '-g' + git_hash 8 10 cmd_cc = meson.get_compiler('c') 9 11 10 12 target_triple = run_command(cmd_cc.cmd_array(), '-dumpmachine', check: true).stdout().strip()
+650 -122
src/ant.c
··· 291 291 bool is_strict; 292 292 bool is_expr; 293 293 bool uses_arguments; 294 + bool has_tail_calls; 294 295 jsoff_t rest_param_start; 295 296 jsoff_t rest_param_len; 296 297 UT_array *params; ··· 393 394 394 395 static jsval_t get_proto(struct js *js, jsval_t obj); 395 396 static void set_proto(struct js *js, jsval_t obj, jsval_t proto); 397 + 398 + enum tail_scan { TAIL_NONE, TAIL_OK, TAIL_UNSAFE }; 399 + static enum tail_scan scan_tail_span(const char *code, jsoff_t clen, jsoff_t start, jsoff_t end); 396 400 397 401 static void clear_break_label(void) { 398 402 break_target_label = NULL; ··· 6136 6140 keystr = (char *) &js->mem[off]; 6137 6141 keylen = slen; 6138 6142 } else if (vtype(key_val) == T_SYMBOL) { 6139 - snprintf(keybuf, sizeof(keybuf), "__sym_%llu__", (unsigned long long)sym_get_id(key_val)); 6143 + sym_to_prop_key(key_val, keybuf, sizeof(keybuf)); 6140 6144 keystr = keybuf; 6141 6145 keylen = strlen(keybuf); 6142 6146 } else { ··· 6679 6683 } return false; 6680 6684 } 6681 6685 6686 + static inline int find_statement_end(const token_stream_t *restrict ts, int start) { 6687 + const cached_token_t *restrict toks = ts->tokens; 6688 + const int count = ts->count; 6689 + if (start >= count) return count; 6690 + 6691 + static const void *dispatch[] = { 6692 + [TOK_LPAREN] = &&open, 6693 + [TOK_LBRACKET] = &&open, 6694 + [TOK_LBRACE] = &&open, 6695 + [TOK_RPAREN] = &&close, 6696 + [TOK_RBRACKET] = &&close, 6697 + [TOK_RBRACE] = &&close, 6698 + [TOK_SEMICOLON] = &&semi, 6699 + [TOK_EOF] = &&semi, 6700 + }; 6701 + 6702 + int depth = 0; 6703 + int j = start; 6704 + 6705 + for (;;) { 6706 + uint8_t t = toks[j].tok; 6707 + if (t < sizeof(dispatch) / sizeof(*dispatch) && dispatch[t]) goto *dispatch[t]; 6708 + 6709 + next: 6710 + if (++j >= count) return count; 6711 + continue; 6712 + 6713 + open: 6714 + depth++; 6715 + goto next; 6716 + 6717 + close: 6718 + if (depth == 0) return j; 6719 + depth--; 6720 + goto next; 6721 + 6722 + semi: 6723 + if (depth == 0) return j; 6724 + goto next; 6725 + } 6726 + } 6727 + 6728 + 6729 + static inline void find_top_level_tokens( 6730 + const token_stream_t *restrict ts, int start, int end, 6731 + uint8_t tok1, int *restrict first_tok1, 6732 + uint8_t tok2, int *restrict first_tok2_after_tok1 6733 + ) { 6734 + const cached_token_t *restrict toks = ts->tokens; 6735 + int depth = 0; 6736 + int j = start; 6737 + 6738 + for (; j < end; j++) { 6739 + uint8_t t = toks[j].tok; 6740 + if (t == TOK_LPAREN || t == TOK_LBRACKET) depth++; 6741 + else if (t == TOK_RPAREN || t == TOK_RBRACKET) depth--; 6742 + else if (depth == 0 && t == tok1) { 6743 + *first_tok1 = j; 6744 + if (!tok2 || !first_tok2_after_tok1) return; 6745 + j++; goto phase2; 6746 + } 6747 + } 6748 + 6749 + *first_tok1 = -1; 6750 + if (first_tok2_after_tok1) *first_tok2_after_tok1 = -1; 6751 + return; 6752 + 6753 + phase2: 6754 + for (; j < end; j++) { 6755 + uint8_t t = toks[j].tok; 6756 + if (t == TOK_LPAREN || t == TOK_LBRACKET) depth++; 6757 + else if (t == TOK_RPAREN || t == TOK_RBRACKET) depth--; 6758 + else if (depth == 0 && t == tok2) { *first_tok2_after_tok1 = j; return; } 6759 + } 6760 + *first_tok2_after_tok1 = -1; 6761 + } 6762 + 6763 + static inline int find_last_top_level_token(const token_stream_t *restrict ts, int start, int end, uint8_t tok) { 6764 + const cached_token_t *restrict toks = ts->tokens; 6765 + int depth = 0; 6766 + for (int j = end - 1; j >= start; j--) { 6767 + uint8_t t = toks[j].tok; 6768 + if (t == TOK_RPAREN || t == TOK_RBRACKET) depth++; 6769 + else if (t == TOK_LPAREN || t == TOK_LBRACKET) depth--; 6770 + else if (depth == 0 && t == tok) return j; 6771 + } 6772 + return -1; 6773 + } 6774 + 6775 + static inline int find_matching_open(const token_stream_t *restrict ts, int rparen_idx) { 6776 + const cached_token_t *restrict toks = ts->tokens; 6777 + int depth = 0; 6778 + for (int j = rparen_idx; j >= 0; j--) { 6779 + uint8_t t = toks[j].tok; 6780 + if (t == TOK_RPAREN) depth++; 6781 + else if (t == TOK_LPAREN) if (--depth == 0) return j; 6782 + } 6783 + return -1; 6784 + } 6785 + 6786 + static inline bool expr_ends_with_bare_call(const token_stream_t *restrict ts, int start, int end) { 6787 + if (start >= end) return false; 6788 + 6789 + const cached_token_t *restrict toks = ts->tokens; 6790 + if (toks[end - 1].tok != TOK_RPAREN) return false; 6791 + 6792 + int depth = 0; 6793 + for (int j = start; j < end - 1; j++) { 6794 + uint8_t t = toks[j].tok; 6795 + if (t == TOK_LPAREN || t == TOK_LBRACKET) depth++; 6796 + else if (t == TOK_RPAREN || t == TOK_RBRACKET) depth--; 6797 + } 6798 + 6799 + if (depth != 1) return false; 6800 + int match = find_matching_open(ts, end - 1); 6801 + if (match < 0 || match <= start) return false; 6802 + 6803 + uint8_t before = toks[match - 1].tok; 6804 + return before == TOK_IDENTIFIER || before == TOK_RPAREN || before == TOK_RBRACKET; 6805 + } 6806 + 6807 + static inline bool return_expr_is_tail_call( 6808 + const token_stream_t *restrict ts, int start, 6809 + const char *body, jsoff_t body_len 6810 + ) { 6811 + int end = find_statement_end(ts, start); 6812 + int last = end - 1; 6813 + 6814 + if (last < start) return false; 6815 + const int count = ts->count; 6816 + 6817 + int ternary, colon; 6818 + find_top_level_tokens(ts, start, end, TOK_Q, &ternary, TOK_COLON, &colon); 6819 + 6820 + if (ternary >= 0) { 6821 + if (colon < 0) return false; 6822 + jsoff_t then_start = ts->tokens[ternary + 1].toff; 6823 + jsoff_t then_end = ts->tokens[colon].toff; 6824 + jsoff_t else_start = (colon + 1 < count) ? ts->tokens[colon + 1].toff : body_len; 6825 + jsoff_t else_end = (end < count) ? ts->tokens[end].toff : body_len; 6826 + 6827 + int then_r = scan_tail_span(body, body_len, then_start, then_end); 6828 + if (then_r == TAIL_UNSAFE) return false; 6829 + int else_r = scan_tail_span(body, body_len, else_start, else_end); 6830 + 6831 + return else_r != TAIL_UNSAFE && (then_r == TAIL_OK || else_r == TAIL_OK); 6832 + } 6833 + 6834 + int last_comma = find_last_top_level_token(ts, start, end, TOK_COMMA); 6835 + if (last_comma >= 0) return return_expr_is_tail_call(ts, last_comma + 1, body, body_len); 6836 + 6837 + return expr_ends_with_bare_call(ts, start, end); 6838 + } 6839 + 6840 + static inline bool tokens_have_tail_calls( 6841 + const token_stream_t *restrict ts, 6842 + const char *body, jsoff_t body_len, bool is_expr 6843 + ) { 6844 + if (!ts || ts->count == 0) return false; 6845 + if (is_expr) return scan_tail_span(body, body_len, 0, body_len) == TAIL_OK; 6846 + 6847 + const cached_token_t *restrict toks = ts->tokens; 6848 + const int count = ts->count; 6849 + 6850 + for (int i = 0; i < count; i++) { 6851 + if (toks[i].tok != TOK_RETURN) continue; 6852 + 6853 + int j = i + 1; 6854 + if (j >= count) continue; 6855 + uint8_t first = toks[j].tok; 6856 + 6857 + if (first == TOK_SEMICOLON || first == TOK_RBRACE || first == TOK_EOF) continue; 6858 + if (return_expr_is_tail_call(ts, j, body, body_len)) return true; 6859 + } 6860 + 6861 + return false; 6862 + } 6863 + 6682 6864 static parsed_func_t *get_or_parse_func(struct js *js, const char *fn, jsoff_t fnlen) { 6683 6865 uint64_t h = hash_key(fn, fnlen); 6684 6866 parsed_func_t *cached = NULL; ··· 6775 6957 pf->is_strict = is_strict_function_body(&fn[fnpos], pf->body_len); 6776 6958 pf->uses_arguments = code_uses_arguments(&fn[pf->body_start], pf->body_len); 6777 6959 pf->tokens = (pf->body_len > 0) ? tokenize_body(js, &fn[pf->body_start], pf->body_len) : NULL; 6960 + pf->has_tail_calls = tokens_have_tail_calls(pf->tokens, &fn[pf->body_start], pf->body_len, pf->is_expr); 6778 6961 6779 6962 HASH_ADD(hh, func_parse_cache, code_hash, sizeof(pf->code_hash), pf); 6780 6963 return pf; ··· 6840 7023 if (saved_scope_stack == NULL) utarray_new(saved_scope_stack, &jsval_icd); 6841 7024 utarray_push_back(saved_scope_stack, &js->scope); 6842 7025 utarray_push_back(saved_scope_stack, &js->this_val); 6843 - 7026 + 7027 + jsval_t res; 7028 + jsval_t *tc_args = NULL; 7029 + int tc_argc = 0; 7030 + 7031 + for (;;) { 6844 7032 jsval_t target_this = peek_this(); 6845 7033 jsoff_t parent_scope_offset; 6846 7034 6847 - if (vtype(closure_scope) == T_OBJ) { 6848 - parent_scope_offset = (jsoff_t) vdata(closure_scope); 6849 - } else { 6850 - parent_scope_offset = (jsoff_t) vdata(js->scope); 6851 - } 7035 + if (vtype(closure_scope) == T_OBJ) parent_scope_offset = (jsoff_t) vdata(closure_scope); 7036 + else parent_scope_offset = (jsoff_t) vdata(js->scope); 6852 7037 6853 7038 if (global_scope_stack == NULL) utarray_new(global_scope_stack, &jsoff_icd); 6854 7039 jsval_t function_scope = mkobj(js, parent_scope_offset); 6855 7040 jsoff_t function_scope_offset = (jsoff_t)vdata(function_scope); 6856 7041 utarray_push_back(global_scope_stack, &function_scope_offset); 6857 7042 6858 - const char *caller_code = js->code; 6859 - jsoff_t caller_clen = js->clen; 6860 - jsoff_t caller_pos = js->pos; 6861 - 6862 7043 jsval_t args_buf[8]; 6863 - jsval_t *args = args_buf; 7044 + jsval_t *args; 6864 7045 6865 - int argc = 0; int args_cap = 8; 6866 - bool args_on_heap = false; 7046 + int argc; 7047 + bool args_on_heap; 6867 7048 6868 - #define ARGS_PUSH(val) do { \ 6869 - if (argc >= args_cap) { \ 6870 - int _new_cap = args_cap * 2; \ 6871 - jsval_t *_new = malloc(_new_cap * sizeof(jsval_t)); \ 6872 - memcpy(_new, args, argc * sizeof(jsval_t)); \ 6873 - if (args_on_heap) free(args); \ 6874 - args = _new; \ 6875 - args_cap = _new_cap; \ 6876 - args_on_heap = true; \ 6877 - } \ 6878 - args[argc++] = (val); \ 6879 - } while (0) 7049 + if (tc_args) { 7050 + args = tc_args; 7051 + argc = tc_argc; 7052 + args_on_heap = (tc_argc > 0); 7053 + tc_args = NULL; 7054 + tc_argc = 0; 7055 + } else { 7056 + const char *caller_code = js->code; 7057 + jsoff_t caller_clen = js->clen; 7058 + jsoff_t caller_pos = js->pos; 6880 7059 6881 - for (int i = 0; i < bound_argc; i++) ARGS_PUSH(bound_args[i]); 6882 - caller_pos = skiptonext(caller_code, caller_clen, caller_pos, NULL); 7060 + args = args_buf; 7061 + argc = 0; 7062 + args_on_heap = false; 7063 + int args_cap = 8; 6883 7064 6884 - while (caller_pos < caller_clen && caller_code[caller_pos] != ')') { 6885 - bool is_spread = ( 6886 - caller_code[caller_pos] == '.' && caller_pos + 2 < caller_clen && 6887 - caller_code[caller_pos + 1] == '.' && caller_code[caller_pos + 2] == '.' 6888 - ); 6889 - if (is_spread) caller_pos += 3; 6890 - js->pos = caller_pos; 6891 - js->consumed = 1; 6892 - jsval_t arg = resolveprop(js, js_expr(js)); 6893 - caller_pos = js->pos; 6894 - if (is_spread && vtype(arg) == T_ARR) { 6895 - jsoff_t len = js_arr_len(js, arg); 6896 - for (jsoff_t i = 0; i < len; i++) ARGS_PUSH(js_arr_get(js, arg, i)); 6897 - } else ARGS_PUSH(arg); 6898 - caller_pos = skiptonext(caller_code, caller_clen, caller_pos, NULL); 6899 - if (caller_pos < caller_clen && caller_code[caller_pos] == ',') caller_pos++; 7065 + #define ARGS_PUSH(val) do { \ 7066 + if (argc >= args_cap) { \ 7067 + int _new_cap = args_cap * 2; \ 7068 + jsval_t *_new = try_oom(_new_cap * sizeof(jsval_t)); \ 7069 + memcpy(_new, args, argc * sizeof(jsval_t)); \ 7070 + if (args_on_heap) free(args); \ 7071 + args = _new; \ 7072 + args_cap = _new_cap; \ 7073 + args_on_heap = true; \ 7074 + } \ 7075 + args[argc++] = (val); \ 7076 + } while (0) 7077 + 7078 + for (int i = 0; i < bound_argc; i++) ARGS_PUSH(bound_args[i]); 6900 7079 caller_pos = skiptonext(caller_code, caller_clen, caller_pos, NULL); 7080 + 7081 + while (caller_pos < caller_clen && caller_code[caller_pos] != ')') { 7082 + bool is_spread = ( 7083 + caller_code[caller_pos] == '.' && caller_pos + 2 < caller_clen && 7084 + caller_code[caller_pos + 1] == '.' && caller_code[caller_pos + 2] == '.' 7085 + ); 7086 + if (is_spread) caller_pos += 3; 7087 + js->pos = caller_pos; 7088 + js->consumed = 1; 7089 + jsval_t arg = resolveprop(js, js_expr(js)); 7090 + caller_pos = js->pos; 7091 + if (is_spread && vtype(arg) == T_ARR) { 7092 + jsoff_t len = js_arr_len(js, arg); 7093 + for (jsoff_t i = 0; i < len; i++) ARGS_PUSH(js_arr_get(js, arg, i)); 7094 + } else ARGS_PUSH(arg); 7095 + caller_pos = skiptonext(caller_code, caller_clen, caller_pos, NULL); 7096 + if (caller_pos < caller_clen && caller_code[caller_pos] == ',') caller_pos++; 7097 + caller_pos = skiptonext(caller_code, caller_clen, caller_pos, NULL); 7098 + } 7099 + 7100 + #undef ARGS_PUSH 7101 + js->pos = caller_pos; 6901 7102 } 6902 - 6903 - #undef ARGS_PUSH 6904 - 6905 - js->pos = caller_pos; 7103 + 6906 7104 js->scope = function_scope; 6907 - 6908 7105 parsed_func_t *pf = get_or_parse_func(js, fn, fnlen); 7106 + 6909 7107 if (!pf) { 6910 7108 if (args_on_heap) free(args); 6911 7109 restore_saved_scope(js); ··· 6913 7111 return js_mkerr(js, "failed to parse function"); 6914 7112 } 6915 7113 7114 + bool func_strict = pf->is_strict; 7115 + if (!func_strict && vtype(func_val) == T_FUNC) { 7116 + jsval_t strict_slot = get_slot(js, mkval(T_OBJ, vdata(func_val)), SLOT_STRICT); 7117 + func_strict = (vtype(strict_slot) == T_BOOL && vdata(strict_slot) == 1); 7118 + } 7119 + 7120 + if (func_strict && (vtype(target_this) == T_UNDEF || vtype(target_this) == T_NULL || 7121 + (vtype(target_this) == T_OBJ && vdata(target_this) == 0))) { 7122 + js->this_val = js_mkundef(); 7123 + } else js->this_val = target_this; 7124 + 6916 7125 int argi = 0; 6917 7126 for (int i = 0; i < pf->param_count; i++) { 6918 7127 parsed_param_t *pp = (parsed_param_t *)utarray_eltptr(pf->params, (unsigned int)i); ··· 6931 7140 } 6932 7141 } else { 6933 7142 jsval_t v; 6934 - if (argi < argc) { 6935 - v = args[argi++]; 6936 - } else if (pp->default_len > 0) { 6937 - v = js_eval_str(js, &fn[pp->default_start], pp->default_len); 6938 - } else { 6939 - v = js_mkundef(); 6940 - } 7143 + if (argi < argc) v = args[argi++]; 7144 + else if (pp->default_len > 0) v = js_eval_str(js, &fn[pp->default_start], pp->default_len); 7145 + else v = js_mkundef(); 6941 7146 jsval_t k = js_mkstr(js, &fn[pp->name_off], pp->name_len); 6942 7147 if (!is_err(k)) mkprop_fast(js, function_scope, k, v, 0); 6943 7148 } ··· 6954 7159 } 6955 7160 6956 7161 bool needs_arguments = pf->uses_arguments; 6957 - bool func_strict = pf->is_strict; 6958 - 6959 - if (!func_strict && vtype(func_val) == T_FUNC) { 6960 - jsval_t func_obj = mkval(T_OBJ, vdata(func_val)); 6961 - jsval_t strict_slot = get_slot(js, func_obj, SLOT_STRICT); 6962 - func_strict = (vtype(strict_slot) == T_BOOL && vdata(strict_slot) == 1); 6963 - } 6964 - 6965 - if (needs_arguments) { 6966 - setup_arguments(js, function_scope, args, argc, func_strict); 6967 - } 7162 + if (needs_arguments) setup_arguments(js, function_scope, args, argc, func_strict); 6968 7163 6969 7164 jsval_t slot_name = get_slot(js, func_val, SLOT_NAME); 6970 7165 if (vtype(slot_name) == T_STR && vtype(func_val) == T_FUNC) { ··· 6980 7175 js->skip_func_hoist = (vtype(no_func_decls) == T_BOOL && vdata(no_func_decls) == 1); 6981 7176 } else js->skip_func_hoist = false; 6982 7177 6983 - if (func_strict && (vtype(target_this) == T_UNDEF || vtype(target_this) == T_NULL || 6984 - (vtype(target_this) == T_OBJ && vdata(target_this) == 0))) { 6985 - js->this_val = js_mkundef(); 6986 - } else js->this_val = target_this; 6987 - 6988 7178 js->flags = F_CALL | (func_strict ? F_STRICT : 0); 6989 - jsval_t res; 6990 - 6991 7179 void *saved_token_stream = js->token_stream; 7180 + 6992 7181 int saved_token_stream_pos = js->token_stream_pos; 6993 7182 const char *saved_token_stream_code = js->token_stream_code; 6994 7183 ··· 7001 7190 js->token_stream_code = NULL; 7002 7191 } 7003 7192 7193 + bool saved_has_tail = js->has_tail_calls; 7194 + js->has_tail_calls = pf->has_tail_calls; 7195 + 7004 7196 if (pf->is_expr) { 7197 + js->tail_ctx = pf->has_tail_calls; 7005 7198 res = js_eval_str(js, &fn[pf->body_start], pf->body_len); 7006 - res = resolveprop(js, res); 7199 + js->tail_ctx = false; 7200 + if (res != (jsval_t)T_TAILCALL) res = resolveprop(js, res); 7007 7201 } else { 7008 7202 res = js_eval(js, &fn[pf->body_start], pf->body_len); 7009 7203 if (!is_err(res) && !(js->flags & F_RETURN)) res = js_mkundef(); 7010 7204 } 7011 - 7205 + 7206 + js->has_tail_calls = saved_has_tail; 7012 7207 js->token_stream = saved_token_stream; 7013 7208 js->token_stream_pos = saved_token_stream_pos; 7014 7209 js->token_stream_code = saved_token_stream_code; 7015 7210 7016 7211 js->skip_func_hoist = false; 7017 - if (global_scope_stack && utarray_len(global_scope_stack) > 0) utarray_pop_back(global_scope_stack); 7018 7212 7213 + if (global_scope_stack && utarray_len(global_scope_stack) > 0) utarray_pop_back(global_scope_stack); 7019 7214 if (args_on_heap) free(args); 7215 + 7216 + if (res == (jsval_t)T_TAILCALL && js->tc.pending) { 7217 + js->tc.pending = false; 7218 + fn = js->tc.code_str; 7219 + fnlen = js->tc.fnlen; 7220 + closure_scope = js->tc.closure_scope; 7221 + func_val = js->tc.func; 7222 + bound_args = NULL; 7223 + bound_argc = 0; 7224 + free(tc_args); 7225 + tc_args = js->tc.args; 7226 + js->tc.args = NULL; 7227 + tc_argc = js->tc.argc; 7228 + continue; 7229 + } 7230 + 7231 + break; 7232 + } // end trampoline 7233 + 7234 + free(tc_args); 7235 + tc_args = NULL; 7236 + free(js->tc.args); 7237 + js->tc.args = NULL; 7238 + 7020 7239 restore_saved_scope(js); 7021 - 7022 7240 return res; 7023 7241 } 7024 7242 ··· 7280 7498 7281 7499 if (vtype(args) != T_CODEREF) return js_mkerr(js, "bad call"); 7282 7500 if (vtype(func) != T_FUNC && vtype(func) != T_CFUNC && vtype(func) != T_FFI) return js_mkerr(js, "calling non-function"); 7501 + 7502 + bool is_tail = js->tail_ctx; 7503 + js->tail_ctx = false; 7283 7504 7284 7505 if (vtype(func) == T_FFI) { 7285 7506 const char *code = js->code; ··· 7466 7687 const jsoff_t *metadata = (const jsoff_t *)(&js->mem[meta_ptr_off]); 7467 7688 7468 7689 for (int i = 0; i < field_count; i++) { 7469 - jsoff_t name_off = metadata[i * 4 + 0]; 7470 - jsoff_t name_len = metadata[i * 4 + 1]; 7471 - jsoff_t init_start = metadata[i * 4 + 2]; 7472 - jsoff_t init_end = metadata[i * 4 + 3]; 7690 + jsoff_t name_off = metadata[i * 5 + 0]; 7691 + jsoff_t name_len = metadata[i * 5 + 1]; 7692 + jsoff_t init_start = metadata[i * 5 + 2]; 7693 + jsoff_t init_end = metadata[i * 5 + 3]; 7694 + bool computed = metadata[i * 5 + 4] != 0; 7473 7695 7474 - jsval_t fname = js_mkstr(js, &source[name_off], name_len); 7696 + jsval_t fname; 7697 + if (computed) fname = (jsval_t) name_off; 7698 + else fname = js_mkstr(js, &source[name_off], name_len); 7699 + 7475 7700 if (is_err(fname)) { 7476 7701 js->current_func = saved_func; 7477 7702 pop_call_frame(); ··· 7510 7735 int argc = (int)utarray_len(call_args); 7511 7736 res = start_async_in_coroutine(js, code_str, fnlen, closure_scope, argv, argc); 7512 7737 utarray_free(call_args); 7738 + } else if (is_tail && !is_arrow && !is_bound && vtype(js->new_target) == T_UNDEF) { 7739 + UT_array *tc_args_arr; 7740 + utarray_new(tc_args_arr, &jsval_icd); 7741 + if (bound_args) { 7742 + for (int i = 0; i < bound_argc; i++) utarray_push_back(tc_args_arr, &bound_args[i]); 7743 + free(bound_args); bound_args = NULL; 7744 + } 7745 + jsval_t tc_err; 7746 + int tc_argc = parse_call_args(js, tc_args_arr, &tc_err); 7747 + if (tc_argc < 0) { 7748 + utarray_free(tc_args_arr); 7749 + pop_call_frame(); 7750 + js->super_val = saved_super; 7751 + js->current_func = saved_func; 7752 + js->code = code; js->clen = clen; js->pos = pos; 7753 + js->flags = (flags & ~F_THROW) | (js->flags & F_THROW); 7754 + js->tok = tok; js->consumed = 1; 7755 + return tc_err; 7756 + } 7757 + jsval_t *tc_argv = NULL; 7758 + if (tc_argc > 0) { 7759 + tc_argv = malloc(tc_argc * sizeof(jsval_t)); 7760 + memcpy(tc_argv, utarray_front(tc_args_arr), tc_argc * sizeof(jsval_t)); 7761 + } 7762 + utarray_free(tc_args_arr); 7763 + js->tc.pending = true; 7764 + js->tc.func = func; 7765 + js->tc.closure_scope = closure_scope; 7766 + js->tc.code_str = code_str; 7767 + js->tc.fnlen = fnlen; 7768 + js->tc.args = tc_argv; 7769 + js->tc.argc = tc_argc; 7770 + pop_call_frame(); 7771 + js->super_val = saved_super; 7772 + js->current_func = saved_func; 7773 + js->code = code; js->clen = clen; js->pos = pos; 7774 + js->flags = (flags & ~F_THROW) | (js->flags & F_THROW); 7775 + js->tok = tok; 7776 + js->consumed = 1; 7777 + return (jsval_t)T_TAILCALL; 7513 7778 } else res = call_js_internal(js, code_str, fnlen, closure_scope, bound_args, bound_argc, func); 7514 7779 pop_call_frame(); 7515 7780 if (bound_args) free(bound_args); ··· 8742 9007 key = resolved_key; 8743 9008 } else if (vtype(resolved_key) == T_SYMBOL) { 8744 9009 char buf[64]; 8745 - snprintf(buf, sizeof(buf), "__sym_%llu__", (unsigned long long)sym_get_id(resolved_key)); 9010 + sym_to_prop_key(resolved_key, buf, sizeof(buf)); 8746 9011 key = js_mkstr(js, buf, strlen(buf)); 8747 9012 } else { 8748 9013 char buf[64]; ··· 9448 9713 } 9449 9714 } 9450 9715 js_call_dot_loop: 9451 - while (next(js) == TOK_LPAREN || next(js) == TOK_DOT || next(js) == TOK_OPTIONAL_CHAIN || next(js) == TOK_LBRACKET || next(js) == TOK_TEMPLATE) { 9716 + bool opt_chain_skip = false; 9717 + uint8_t cd_tok; 9718 + while ( 9719 + cd_tok = next(js), 9720 + cd_tok == TOK_LPAREN 9721 + || cd_tok == TOK_DOT 9722 + || cd_tok == TOK_OPTIONAL_CHAIN 9723 + || cd_tok == TOK_LBRACKET 9724 + || cd_tok == TOK_TEMPLATE 9725 + ) { 9726 + if (opt_chain_skip) { 9727 + if (js->tok == TOK_OPTIONAL_CHAIN || js->tok == TOK_DOT) { 9728 + js->consumed = 1; 9729 + uint8_t nxt = next(js); 9730 + if (nxt == TOK_HASH) { js->consumed = 1; next(js); } 9731 + if (next(js) == TOK_IDENTIFIER || is_keyword_propname(next(js))) js->consumed = 1; 9732 + else if (next(js) == TOK_LBRACKET) { 9733 + js->consumed = 1; js_expr(js); 9734 + if (next(js) == TOK_RBRACKET) js->consumed = 1; 9735 + } 9736 + if (js->tok == TOK_OPTIONAL_CHAIN) opt_chain_skip = false; 9737 + continue; 9738 + } else if (js->tok == TOK_LBRACKET) { 9739 + js->consumed = 1; js_expr(js); 9740 + if (next(js) == TOK_RBRACKET) js->consumed = 1; 9741 + continue; 9742 + } else if (js->tok == TOK_LPAREN) { 9743 + js->consumed = 1; js_call_params(js); 9744 + continue; 9745 + } else opt_chain_skip = false; 9746 + } 9452 9747 if (js->tok == TOK_TEMPLATE) { 9453 9748 if (vtype(res) == T_PROP) res = resolveprop(js, res); 9454 9749 if (is_err(res)) return res; ··· 9469 9764 return js_mkerr_typed(js, JS_ERR_SYNTAX, "private field name expected"); 9470 9765 } 9471 9766 js->consumed = 1; 9472 - prop_name = mkcoderef((jsoff_t) js->toff, (jsoff_t) js->tlen); 9767 + prop_name = mkcoderef((jsoff_t) (js->toff - 1), (jsoff_t) (js->tlen + 1)); 9473 9768 } else if (nxt == TOK_IDENTIFIER || is_keyword_propname(nxt)) { 9474 9769 js->consumed = 1; 9475 9770 prop_name = mkcoderef((jsoff_t) js->toff, (jsoff_t) js->tlen); ··· 9481 9776 js->consumed = 1; 9482 9777 if (op == TOK_OPTIONAL_CHAIN && (vtype(obj) == T_NULL || vtype(obj) == T_UNDEF)) { 9483 9778 res = js_mkundef(); 9484 - } else { 9485 - res = do_op(js, TOK_BRACKET, res, idx); 9486 - } 9779 + opt_chain_skip = true; 9780 + } else res = do_op(js, TOK_BRACKET, res, idx); 9487 9781 continue; 9488 - } else { 9489 - prop_name = js_group(js); 9490 - } 9782 + } else prop_name = js_group(js); 9491 9783 if (op == TOK_OPTIONAL_CHAIN && (vtype(obj) == T_NULL || vtype(obj) == T_UNDEF)) { 9492 9784 res = js_mkundef(); 9493 - } else { 9494 - res = do_op(js, op, res, prop_name); 9495 - } 9785 + opt_chain_skip = true; 9786 + } else res = do_op(js, op, res, prop_name); 9496 9787 } else if (js->tok == TOK_LBRACKET) { 9497 9788 js->consumed = 1; 9498 9789 if (vtype(res) != T_PROP && vtype(res) != T_PROPREF) { 9499 9790 obj = res; 9500 - } else { 9501 - obj = resolveprop(js, res); 9502 - } 9791 + } else obj = resolveprop(js, res); 9503 9792 jsval_t idx = js_expr(js); 9504 9793 if (is_err(idx)) return idx; 9505 9794 if (next(js) != TOK_RBRACKET) return js_mkerr_typed(js, JS_ERR_SYNTAX, "] expected"); ··· 12106 12395 return js_mkundef(); 12107 12396 } 12108 12397 12398 + static inline jsoff_t skip_comment_or_string(const char *code, jsoff_t clen, jsoff_t p) { 12399 + char c = code[p]; 12400 + if (c == '/' && p + 1 < clen) { 12401 + if (code[p + 1] == '/') return skip_line_comment(code, clen, p); 12402 + if (code[p + 1] == '*') return skip_block_comment(code, clen, p); 12403 + } 12404 + if (c == '\'' || c == '"') return skip_string_literal(code, clen, p, c); 12405 + if (c == '`') return skip_template_literal(code, clen, p); 12406 + return 0; 12407 + } 12408 + 12409 + static inline bool is_stmt_end(char c) { 12410 + return c == ';' || c == '}' || c == '\n'; 12411 + } 12412 + 12413 + static const uint8_t binop_table[128] = { 12414 + ['+']=1, ['*']=1, ['%']=1, ['^']=1, [',']=1, ['=']=1, 12415 + ['/']=2, ['-']=2, ['&']=2, ['|']=2, ['<']=2, ['>']=2, ['!']=2, 12416 + }; 12417 + 12418 + static bool is_binop_char(const char *code, jsoff_t clen, jsoff_t p) { 12419 + unsigned char c = (unsigned char)code[p]; 12420 + if (c > 127) return false; 12421 + uint8_t kind = binop_table[c]; 12422 + if (kind == 1) return true; 12423 + if (kind == 0) return false; 12424 + if (p + 1 >= clen) return c == '/'; 12425 + char n = code[p + 1]; 12426 + switch (c) { 12427 + case '/': return n != '/' && n != '*'; 12428 + case '-': return n != '-'; 12429 + case '&': return n != '&'; 12430 + case '|': return n != '|'; 12431 + case '<': return true; 12432 + case '>': return true; 12433 + case '!': return n == '='; 12434 + } 12435 + return false; 12436 + } 12437 + 12438 + static jsoff_t find_ternary_colon(const char *code, jsoff_t clen, jsoff_t p) { 12439 + int depth = 0, nesting = 0; 12440 + for (; p < clen; p++) { 12441 + jsoff_t skip = skip_comment_or_string(code, clen, p); 12442 + if (skip) { p = skip - 1; continue; } 12443 + switch (code[p]) { 12444 + case '(': case '[': depth++; break; 12445 + case ')': case ']': depth--; break; 12446 + case '?': if (!depth) nesting++; break; 12447 + case ':': if (!depth && nesting-- == 0) return p; break; 12448 + } 12449 + } 12450 + return clen; 12451 + } 12452 + 12453 + static enum tail_scan scan_tail_span(const char *code, jsoff_t clen, jsoff_t start, jsoff_t end) { 12454 + jsoff_t p = skiptonext(code, end, start, NULL); 12455 + int depth = 0, bdepth = 0; jsoff_t last_paren_close = 0; 12456 + bool seen_binop = false; 12457 + 12458 + while (p < end) { 12459 + jsoff_t skip = skip_comment_or_string(code, end, p); 12460 + if (skip) { p = skip; continue; } 12461 + char c = code[p]; 12462 + if (c == '[') { bdepth++; p++; continue; } 12463 + if (c == ']') { 12464 + bdepth--; p++; 12465 + if (bdepth < 0) return TAIL_UNSAFE; 12466 + continue; 12467 + } 12468 + if (c == '(') { depth++; p++; continue; } 12469 + if (c == ')') { 12470 + depth--; p++; 12471 + if (depth < 0) return TAIL_UNSAFE; 12472 + if (depth == 0 && bdepth == 0) last_paren_close = p; 12473 + continue; 12474 + } 12475 + if (depth == 0 && bdepth == 0) { 12476 + if (c == '?') { 12477 + p++; 12478 + jsoff_t colon = find_ternary_colon(code, end, p); 12479 + if (colon >= end) return TAIL_UNSAFE; 12480 + enum tail_scan t = scan_tail_span(code, colon, p, colon); 12481 + enum tail_scan f = scan_tail_span(code, end, colon + 1, end); 12482 + if (t == TAIL_UNSAFE || f == TAIL_UNSAFE) return TAIL_UNSAFE; 12483 + if (t == TAIL_OK || f == TAIL_OK) return TAIL_OK; 12484 + return TAIL_NONE; 12485 + } 12486 + if (is_binop_char(code, end, p)) seen_binop = true; 12487 + } p++; 12488 + } 12489 + 12490 + if (last_paren_close == 0) return TAIL_NONE; 12491 + if (seen_binop) return TAIL_UNSAFE; 12492 + jsoff_t after = skiptonext(code, end, last_paren_close, NULL); 12493 + if ((after >= end) || is_stmt_end(code[after])) return TAIL_OK; 12494 + return TAIL_UNSAFE; 12495 + } 12496 + 12497 + static bool is_tail_call_expr(struct js *js) { 12498 + const char *code = js->code; 12499 + jsoff_t clen = js->clen; 12500 + jsoff_t start = skiptonext(code, clen, js->pos, NULL); 12501 + return scan_tail_span(code, clen, start, clen) == TAIL_OK; 12502 + } 12503 + 12109 12504 static jsval_t js_return(struct js *js) { 12110 12505 uint8_t exe = !(js->flags & F_NOEXEC); 12111 12506 uint8_t in_func = js->flags & F_CALL; ··· 12114 12509 12115 12510 uint8_t nxt = next(js); 12116 12511 if (nxt != TOK_SEMICOLON && nxt != TOK_RBRACE && nxt != TOK_EOF && !js->had_newline) { 12512 + bool prev_tail = js->tail_ctx; 12513 + if (exe && in_func && js->has_tail_calls && is_tail_call_expr(js)) js->tail_ctx = true; 12117 12514 res = resolveprop(js, js_expr_comma(js)); 12515 + js->tail_ctx = prev_tail; 12118 12516 } 12119 12517 12120 12518 if (exe && !in_func) return js_mkundef(); ··· 12211 12609 } 12212 12610 } 12213 12611 12214 - } else { 12215 - break; 12216 - } 12612 + } else break; 12217 12613 } 12218 12614 12219 12615 if (!expect(js, TOK_RBRACE, &res)) { ··· 12314 12710 js->consumed = 0; 12315 12711 12316 12712 uint8_t preserve = 0; 12317 - if (js->flags & F_RETURN) { 12318 - preserve = js->flags & (F_RETURN | F_NOEXEC); 12319 - } 12320 - if (js->flags & F_THROW) { 12321 - preserve = js->flags & (F_THROW | F_NOEXEC); 12322 - } 12713 + if (js->flags & F_RETURN) preserve = js->flags & (F_RETURN | F_NOEXEC); 12714 + if (js->flags & F_THROW) preserve = js->flags & (F_THROW | F_NOEXEC); 12323 12715 js->flags = (flags & ~F_SWITCH) | preserve; 12324 12716 12325 12717 return res; ··· 12416 12808 bool is_private; 12417 12809 bool is_getter; 12418 12810 bool is_setter; 12811 + bool is_static_block; 12812 + bool is_computed; 12813 + jsval_t computed_key; 12419 12814 jsoff_t field_start, field_end; 12420 12815 jsoff_t param_start; 12421 12816 } MethodInfo; ··· 12458 12853 if (next(js) == TOK_IDENTIFIER) { 12459 12854 bool is_get = (js->tlen == 3 && memcmp(&js->code[js->toff], "get", 3) == 0); 12460 12855 bool is_set = (js->tlen == 3 && memcmp(&js->code[js->toff], "set", 3) == 0); 12461 - if (!is_private_member && (is_get || is_set)) { 12856 + if (is_get || is_set) { 12462 12857 jsoff_t saved_pos = js->pos; 12463 12858 jsoff_t saved_toff = js->toff; 12464 12859 jsoff_t saved_tlen = js->tlen; 12465 12860 uint8_t saved_tok = js->tok; 12466 12861 int saved_stream_pos = js->token_stream_pos; 12467 12862 js->consumed = 1; 12468 - if (next(js) == TOK_IDENTIFIER) { 12863 + uint8_t peek = next(js); 12864 + bool next_is_name = (peek == TOK_IDENTIFIER); 12865 + if (!next_is_name && peek == TOK_HASH) { 12866 + js->consumed = 1; 12867 + if (next(js) == TOK_IDENTIFIER) { 12868 + next_is_name = true; 12869 + is_private_member = true; 12870 + } 12871 + } 12872 + if (next_is_name) { 12469 12873 is_getter_method = is_get; 12470 12874 is_setter_method = is_set; 12471 12875 } else { ··· 12479 12883 } 12480 12884 } 12481 12885 12886 + if (is_static_member && !is_getter_method && !is_setter_method && next(js) == TOK_LBRACE) { 12887 + jsoff_t block_start = js->pos - 1; js->consumed = 0; 12888 + jsval_t blk = js_block(js, false); 12889 + if (is_err(blk)) { js->flags = save_flags; utarray_free(methods); return blk; } 12890 + jsoff_t block_end = js->pos; 12891 + MethodInfo static_block = {0}; 12892 + static_block.is_static = true; 12893 + static_block.is_static_block = true; 12894 + static_block.field_start = block_start; 12895 + static_block.field_end = block_end; 12896 + utarray_push_back(methods, &static_block); 12897 + js->consumed = 1; 12898 + continue; 12899 + } 12900 + 12901 + if (next(js) == TOK_LBRACKET) { 12902 + jsoff_t key_start = js->pos; 12903 + js->consumed = 1; 12904 + jsval_t computed_key = js_expr(js); 12905 + if (is_err(computed_key)) { js->flags = save_flags; utarray_free(methods); return computed_key; } 12906 + if (next(js) != TOK_RBRACKET) { js->flags = save_flags; return js_mkerr_typed(js, JS_ERR_SYNTAX, "] expected"); } 12907 + jsoff_t key_end = js->pos - 1; 12908 + js->consumed = 1; 12909 + 12910 + MethodInfo computed = {0}; 12911 + computed.is_static = is_static_member; 12912 + computed.is_computed = true; 12913 + computed.name_off = key_start; 12914 + computed.name_len = key_end - key_start; 12915 + 12916 + if (next(js) == TOK_LPAREN) { 12917 + computed.is_field = false; 12918 + computed.is_async = is_async_method; 12919 + computed.is_getter = is_getter_method; 12920 + computed.is_setter = is_setter_method; 12921 + EXPECT(TOK_LPAREN, js->flags = save_flags); 12922 + jsoff_t method_params_start = js->pos - 1; 12923 + if (!parse_func_params(js, &save_flags, NULL)) { 12924 + js->flags = save_flags; 12925 + return js_mkerr_typed(js, JS_ERR_SYNTAX, "invalid method parameters"); 12926 + } 12927 + EXPECT(TOK_RPAREN, js->flags = save_flags); 12928 + EXPECT(TOK_LBRACE, js->flags = save_flags); 12929 + js->consumed = 0; 12930 + jsval_t blk = js_block(js, false); 12931 + if (is_err(blk)) { js->flags = save_flags; return blk; } 12932 + jsoff_t method_body_end = js->pos; 12933 + computed.fn_start = method_params_start; 12934 + computed.fn_end = method_body_end; 12935 + computed.param_start = method_params_start; 12936 + js->consumed = 1; 12937 + } else if (next(js) == TOK_ASSIGN) { 12938 + computed.is_field = true; 12939 + js->consumed = 1; 12940 + jsoff_t field_start = js->pos; 12941 + jsval_t field_expr = js_expr(js); 12942 + if (is_err(field_expr)) { js->flags = save_flags; utarray_free(methods); return field_expr; } 12943 + jsoff_t field_end = js->pos; 12944 + computed.field_start = field_start; 12945 + computed.field_end = field_end; 12946 + if (next(js) == TOK_SEMICOLON) js->consumed = 1; 12947 + } else { 12948 + computed.is_field = true; 12949 + if (next(js) == TOK_SEMICOLON) js->consumed = 1; 12950 + } 12951 + utarray_push_back(methods, &computed); 12952 + continue; 12953 + } 12954 + 12482 12955 if (next(js) != TOK_IDENTIFIER && (next(js) < TOK_ASYNC || next(js) > TOK_STATIC)) { 12483 12956 js->flags = save_flags; 12484 12957 return js_mkerr_typed(js, JS_ERR_SYNTAX, "method name expected"); 12485 12958 } 12486 - jsoff_t method_name_off = js->toff, method_name_len = js->tlen; 12959 + 12960 + jsoff_t method_name_off = is_private_member ? js->toff - 1 : js->toff; 12961 + jsoff_t method_name_len = is_private_member ? js->tlen + 1 : js->tlen; 12487 12962 js->consumed = 1; 12488 12963 12489 12964 if (next(js) == TOK_ASSIGN) { ··· 12607 13082 12608 13083 for (unsigned int i = 0; i < utarray_len(methods); i++) { 12609 13084 MethodInfo *m = (MethodInfo *)utarray_eltptr(methods, i); 13085 + if (!m->is_computed) continue; 13086 + jsval_t key_val = js_eval_slice(js, m->name_off, m->name_len); 13087 + key_val = resolveprop(js, key_val); 13088 + if (vtype(key_val) == T_SYMBOL) { 13089 + char sym_buf[48]; 13090 + sym_to_prop_key(key_val, sym_buf, sizeof(sym_buf)); 13091 + m->computed_key = js_mkstr(js, sym_buf, strlen(sym_buf)); 13092 + } else m->computed_key = coerce_to_str(js, key_val); 13093 + if (is_err(m->computed_key)) { utarray_free(methods); return m->computed_key; } 13094 + } 13095 + 13096 + for (unsigned int i = 0; i < utarray_len(methods); i++) { 13097 + MethodInfo *m = (MethodInfo *)utarray_eltptr(methods, i); 12610 13098 if (m->is_static) continue; 12611 13099 if (m->is_field) continue; 12612 13100 12613 - jsval_t method_name = js_mkstr(js, &js->code[m->name_off], m->name_len); 13101 + jsval_t method_name; 13102 + if (m->is_computed) method_name = m->computed_key; 13103 + else method_name = js_mkstr(js, &js->code[m->name_off], m->name_len); 12614 13104 if (is_err(method_name)) return method_name; 12615 13105 12616 13106 jsoff_t mlen = m->fn_end - m->fn_start; 12617 - 12618 13107 jsval_t method_obj = mkobj(js, 0); 12619 13108 if (is_err(method_obj)) return method_obj; 12620 13109 ··· 12668 13157 } 12669 13158 12670 13159 if (instance_field_count > 0) { 12671 - size_t metadata_size = instance_field_count * sizeof(jsoff_t) * 4; 13160 + size_t metadata_size = instance_field_count * sizeof(jsoff_t) * 5; 12672 13161 jsoff_t meta_len = (jsoff_t) (metadata_size + 1); 12673 13162 jsoff_t meta_header = (jsoff_t) ((meta_len << 3) | T_STR); 12674 13163 jsoff_t meta_off = js_alloc(js, meta_len + sizeof(meta_header)); ··· 12683 13172 if (m->is_static) continue; 12684 13173 if (!m->is_field) continue; 12685 13174 12686 - metadata[meta_idx * 4 + 0] = m->name_off; 12687 - metadata[meta_idx * 4 + 1] = m->name_len; 12688 - metadata[meta_idx * 4 + 2] = m->field_start; 12689 - metadata[meta_idx * 4 + 3] = m->field_end; 13175 + if (m->is_computed) { 13176 + metadata[meta_idx * 5 + 0] = (jsoff_t) m->computed_key; 13177 + metadata[meta_idx * 5 + 1] = 0; 13178 + } else { 13179 + metadata[meta_idx * 5 + 0] = m->name_off; 13180 + metadata[meta_idx * 5 + 1] = m->name_len; 13181 + } 13182 + 13183 + metadata[meta_idx * 5 + 2] = m->field_start; 13184 + metadata[meta_idx * 5 + 3] = m->field_end; 13185 + metadata[meta_idx * 5 + 4] = m->is_computed ? 1 : 0; 12690 13186 meta_idx++; 12691 13187 } 12692 13188 ··· 12738 13234 MethodInfo *m = (MethodInfo *)utarray_eltptr(methods, i); 12739 13235 if (!m->is_static) continue; 12740 13236 12741 - jsval_t member_name = js_mkstr(js, &js->code[m->name_off], m->name_len); 13237 + if (m->is_static_block) { 13238 + if (m->field_start > 0 && m->field_end > m->field_start) { 13239 + const char *saved_code = js->code; 13240 + jsoff_t saved_clen = js->clen, saved_pos = js->pos; 13241 + uint8_t saved_tok = js->tok, saved_consumed = js->consumed; 13242 + jsval_t blk_res = js_eval(js, &saved_code[m->field_start], m->field_end - m->field_start); 13243 + js->code = saved_code; js->clen = saved_clen; js->pos = saved_pos; 13244 + js->tok = saved_tok; js->consumed = saved_consumed; 13245 + if (is_err(blk_res)) { utarray_free(methods); return blk_res; } 13246 + } 13247 + continue; 13248 + } 13249 + 13250 + jsval_t member_name; 13251 + if (m->is_computed) member_name = m->computed_key; 13252 + else member_name = js_mkstr(js, &js->code[m->name_off], m->name_len); 13253 + 12742 13254 if (is_err(member_name)) return member_name; 12743 13255 12744 13256 if (m->is_field) { ··· 12763 13275 if (vtype(method_func_proto) == T_FUNC) set_proto(js, method_obj, method_func_proto); 12764 13276 12765 13277 jsval_t method_func = mkval(T_FUNC, (unsigned long) vdata(method_obj)); 12766 - jsval_t set_res = js_setprop(js, func_obj, member_name, method_func); 12767 - if (is_err(set_res)) return set_res; 13278 + 13279 + if (m->is_getter || m->is_setter) { 13280 + jsoff_t nm_len; 13281 + const char *nm_str = (const char *)&js->mem[vstr(js, member_name, &nm_len)]; 13282 + if (m->is_getter) { 13283 + js_set_getter_desc(js, func_obj, nm_str, nm_len, method_func, JS_DESC_C); 13284 + } else { 13285 + js_set_setter_desc(js, func_obj, nm_str, nm_len, method_func, JS_DESC_C); 13286 + } 13287 + } else { 13288 + jsval_t set_res = js_setprop(js, func_obj, member_name, method_func); 13289 + if (is_err(set_res)) return set_res; 13290 + } 12768 13291 } 12769 13292 } 12770 13293 ··· 15388 15911 15389 15912 if (vtype(prop) == T_SYMBOL) { 15390 15913 char keybuf[64]; 15391 - snprintf(keybuf, sizeof(keybuf), "__sym_%llu__", (unsigned long long)sym_get_id(prop)); 15914 + sym_to_prop_key(prop, keybuf, sizeof(keybuf)); 15392 15915 prop = js_mkstr(js, keybuf, strlen(keybuf)); 15393 15916 } else if (vtype(prop) != T_STR) { 15394 15917 char buf[64]; ··· 15790 16313 jsoff_t key_len; 15791 16314 15792 16315 if (vtype(key) == T_SYMBOL) { 15793 - snprintf(sym_buf, sizeof(sym_buf), "__sym_%llu__", (unsigned long long)sym_get_id(key)); 16316 + sym_to_prop_key(key, sym_buf, sizeof(sym_buf)); 15794 16317 key_str = sym_buf; 15795 16318 key_len = (jsoff_t)strlen(sym_buf); 15796 16319 } else if (vtype(key) == T_STR) { ··· 22774 23297 22775 23298 for (jshdl_t i = 0; i < c->js->gc_roots_len; i++) op_val(c, &c->js->gc_roots[i]); 22776 23299 if (c->js->ascii_cache_init) for (int i = 0; i < 128; i++) op_val(c, &c->js->ascii_char_cache[i]); 23300 + 23301 + if (c->js->tc.pending) { 23302 + op_val(c, &c->js->tc.func); op_val(c, &c->js->tc.closure_scope); 23303 + for (int i = 0; i < c->js->tc.argc; i++) op_val(c, &c->js->tc.args[i]); 23304 + } 22777 23305 } 22778 23306 22779 23307 void js_gc_reserve_roots(GC_RESERVE_ARGS) {
+5 -1
src/modules/symbol.c
··· 46 46 && key[key_len - 2] == '_'; 47 47 } 48 48 49 + int sym_to_prop_key(jsval_t sym, char *buf, size_t bufsz) { 50 + return snprintf(buf, bufsz, "__sym_%llu__", (unsigned long long)js_sym_id(sym)); 51 + } 52 + 49 53 jsval_t get_wellknown_sym_by_key(const char *key, size_t key_len) { 50 54 static const struct { wellknown_sym_t *sym; } table[] = { 51 55 { &g_iterator }, { &g_asyncIterator }, { &g_toStringTag }, ··· 74 78 75 79 static inline void init_symbol(struct js *js, wellknown_sym_t *sym_var, const char *name) { 76 80 sym_var->sym = js_mksym(js, name); 77 - snprintf(sym_var->key, sizeof(sym_var->key), "__sym_%llu__", (unsigned long long)js_sym_id(sym_var->sym)); 81 + sym_to_prop_key(sym_var->sym, sym_var->key, sizeof(sym_var->key)); 78 82 } 79 83 80 84 static jsval_t builtin_Symbol(struct js *js, jsval_t *args, int nargs) {
+194
tests/test_gc_tco.js
··· 1 + // Stress test: GC compaction during tail-call optimization 2 + // Verifies that tc.func, tc.closure_scope, and tc.args[] are properly 3 + // rooted so GC compaction doesn't corrupt them mid-trampoline. 4 + 5 + console.log('=== GC + Tail Call Stress Test ===\n'); 6 + 7 + function fmt(bytes) { 8 + if (bytes < 1024) return bytes + ' B'; 9 + if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + ' KB'; 10 + return (bytes / 1024 / 1024).toFixed(2) + ' MB'; 11 + } 12 + 13 + let failures = 0; 14 + function assert(cond, msg) { 15 + if (!cond) { 16 + console.log('FAIL:', msg); 17 + failures++; 18 + } 19 + } 20 + 21 + // Inflate arena past 10MB so Ant.gc() actually compacts 22 + function inflateArena() { 23 + let junk = []; 24 + for (let i = 0; i < 40000; i++) { 25 + junk.push({ idx: i, payload: 'padding_string_' + i + '_extra_data_to_bulk_up_the_arena_size' }); 26 + } 27 + let used = Ant.stats().arenaUsed; 28 + assert(used >= 10 * 1024 * 1024, 'arena should be >= 10MB, got ' + fmt(used)); 29 + console.log(' Arena inflated to', fmt(used)); 30 + return junk; 31 + } 32 + 33 + // --------------------------------------------------------------------------- 34 + // Test 1: Tail recursion passing heap objects as args, GC between iterations 35 + // --------------------------------------------------------------------------- 36 + console.log('Test 1: Tail-call args survive GC compaction'); 37 + let _garbage = inflateArena(); 38 + 39 + function tailWithObjects(n, obj) { 40 + if (n <= 0) return obj; 41 + Ant.gc(); 42 + return tailWithObjects(n - 1, { value: obj.value + 1, prev: obj }); 43 + } 44 + 45 + let result1 = tailWithObjects(200, { value: 0, prev: null }); 46 + assert(result1.value === 200, 'final value should be 200, got ' + result1.value); 47 + 48 + let walk = result1; 49 + let chainOk = true; 50 + for (let i = 200; i >= 0; i--) { 51 + if (walk.value !== i) { chainOk = false; break; } 52 + walk = walk.prev; 53 + } 54 + assert(chainOk, 'object chain should be intact through all 200 links'); 55 + console.log(' Object args through tail calls: OK\n'); 56 + 57 + // --------------------------------------------------------------------------- 58 + // Test 2: Mutual tail recursion with string args + GC pressure 59 + // --------------------------------------------------------------------------- 60 + console.log('Test 2: Mutual tail recursion with string args + GC'); 61 + _garbage = inflateArena(); 62 + 63 + function pingStr(n, s) { 64 + if (n <= 0) return s; 65 + Ant.gc(); 66 + return pongStr(n - 1, s + 'p'); 67 + } 68 + function pongStr(n, s) { 69 + if (n <= 0) return s; 70 + return pingStr(n - 1, s + 'o'); 71 + } 72 + 73 + let result2 = pingStr(100, 'start:'); 74 + assert(result2.length === 106, 'string length should be 106, got ' + result2.length); 75 + assert(result2.startsWith('start:'), 'should start with "start:"'); 76 + console.log(' Mutual tail recursion with strings: OK\n'); 77 + 78 + // --------------------------------------------------------------------------- 79 + // Test 3: Closure scope survives GC during tail calls 80 + // --------------------------------------------------------------------------- 81 + console.log('Test 3: Closure scope survives GC during tail calls'); 82 + _garbage = inflateArena(); 83 + 84 + function makeAccumulator() { 85 + let captured = { sum: 0 }; 86 + function loop(n) { 87 + if (n <= 0) return captured; 88 + captured.sum += n; 89 + Ant.gc(); 90 + return loop(n - 1); 91 + } 92 + return loop; 93 + } 94 + 95 + let accResult = makeAccumulator()(500); 96 + assert(accResult.sum === 500 * 501 / 2, 'sum should be 125250, got ' + accResult.sum); 97 + console.log(' Closure scope through tail calls: OK\n'); 98 + 99 + // --------------------------------------------------------------------------- 100 + // Test 4: Multi-arg tail calls with mixed heap types + GC 101 + // --------------------------------------------------------------------------- 102 + console.log('Test 4: Multi-arg tail calls with mixed types + GC'); 103 + _garbage = inflateArena(); 104 + 105 + function multiArg(n, arr, obj, str) { 106 + if (n <= 0) return { arr, obj, str }; 107 + arr.push(n); 108 + Ant.gc(); 109 + return multiArg(n - 1, arr, { v: obj.v + 1, inner: obj }, str + 'x'); 110 + } 111 + 112 + let result4 = multiArg(100, [], { v: 0, inner: null }, ''); 113 + assert(result4.arr.length === 100, 'array should have 100 elements, got ' + result4.arr.length); 114 + assert(result4.obj.v === 100, 'nested obj.v should be 100, got ' + result4.obj.v); 115 + assert(result4.str.length === 100, 'string should have 100 chars, got ' + result4.str.length); 116 + console.log(' Multi-arg mixed types: OK\n'); 117 + 118 + // --------------------------------------------------------------------------- 119 + // Test 5: Deep tail recursion with GC every N iterations 120 + // --------------------------------------------------------------------------- 121 + console.log('Test 5: Deep tail recursion (50k) with periodic GC'); 122 + _garbage = inflateArena(); 123 + 124 + function deepTail(n, acc) { 125 + if (n <= 0) return acc; 126 + if (n % 500 === 0) Ant.gc(); 127 + return deepTail(n - 1, acc + 1); 128 + } 129 + 130 + let result5 = deepTail(50000, 0); 131 + assert(result5 === 50000, 'deepTail should return 50000, got ' + result5); 132 + console.log(' Deep tail recursion with periodic GC: OK\n'); 133 + 134 + // --------------------------------------------------------------------------- 135 + // Test 6: Tail call where callee is a different function (not self-recursion) 136 + // --------------------------------------------------------------------------- 137 + console.log('Test 6: Tail call to different functions + GC'); 138 + _garbage = inflateArena(); 139 + 140 + function step1(n, data) { 141 + if (n <= 0) return data; 142 + data.a++; 143 + Ant.gc(); 144 + return step2(n - 1, data); 145 + } 146 + function step2(n, data) { 147 + if (n <= 0) return data; 148 + data.b++; 149 + return step3(n - 1, data); 150 + } 151 + function step3(n, data) { 152 + if (n <= 0) return data; 153 + data.c++; 154 + return step1(n - 1, data); 155 + } 156 + 157 + let result6 = step1(300, { a: 0, b: 0, c: 0 }); 158 + assert(result6.a === 100, 'a should be 100, got ' + result6.a); 159 + assert(result6.b === 100, 'b should be 100, got ' + result6.b); 160 + assert(result6.c === 100, 'c should be 100, got ' + result6.c); 161 + console.log(' Cross-function tail calls: OK\n'); 162 + 163 + // --------------------------------------------------------------------------- 164 + // Test 7: Array args allocated fresh each iteration + GC 165 + // --------------------------------------------------------------------------- 166 + console.log('Test 7: Fresh array allocation per tail-call iteration + GC'); 167 + _garbage = inflateArena(); 168 + 169 + function freshArrays(n, results) { 170 + if (n <= 0) return results; 171 + let data = new Array(50); 172 + for (let i = 0; i < 50; i++) data[i] = n; 173 + results.push(data); 174 + if (n % 10 === 0) Ant.gc(); 175 + return freshArrays(n - 1, results); 176 + } 177 + 178 + let result7 = freshArrays(100, []); 179 + assert(result7.length === 100, 'should have 100 arrays, got ' + result7.length); 180 + assert(result7[0][0] === 100, 'first array should contain 100'); 181 + assert(result7[99][0] === 1, 'last array should contain 1'); 182 + console.log(' Fresh array per iteration: OK\n'); 183 + 184 + // --------------------------------------------------------------------------- 185 + // Summary 186 + // --------------------------------------------------------------------------- 187 + _garbage = null; 188 + 189 + console.log('=== Summary ==='); 190 + if (failures === 0) { 191 + console.log('All GC + TCO stress tests passed!'); 192 + } else { 193 + console.log('Failures:', failures); 194 + }
+75
tests/test_tco.js
··· 1 + // Test tail call optimization 2 + 3 + // 1. Basic tail-recursive factorial 4 + function factorial(n, acc = 1) { 5 + if (n <= 1) return acc; 6 + return factorial(n - 1, n * acc); 7 + } 8 + console.log('factorial(10):', factorial(10)); // 3628800 9 + console.log('factorial(20):', factorial(20)); // 2432902008176640000 10 + 11 + // 2. Deep tail recursion - would stack overflow without TCO 12 + function countDown(n) { 13 + if (n <= 0) return 'done'; 14 + return countDown(n - 1); 15 + } 16 + console.log('countDown(100000):', countDown(100000)); // done 17 + 18 + // 3. Tail-recursive sum 19 + function sum(n, acc = 0) { 20 + if (n <= 0) return acc; 21 + return sum(n - 1, acc + n); 22 + } 23 + console.log('sum(100000):', sum(100000)); // 5000050000 24 + 25 + // 4. Tail-recursive fibonacci (iterative via accumulator) 26 + function fib(n, a = 0, b = 1) { 27 + if (n === 0) return a; 28 + if (n === 1) return b; 29 + return fib(n - 1, b, a + b); 30 + } 31 + console.log('fib(10):', fib(10)); // 55 32 + console.log('fib(30):', fib(30)); // 832040 33 + 34 + // 5. Mutual tail recursion 35 + function isEven(n) { 36 + if (n === 0) return true; 37 + return isOdd(n - 1); 38 + } 39 + function isOdd(n) { 40 + if (n === 0) return false; 41 + return isEven(n - 1); 42 + } 43 + console.log('isEven(10):', isEven(10)); // true 44 + console.log('isOdd(11):', isOdd(11)); // true 45 + console.log('isEven(10000):', isEven(10000)); // true 46 + 47 + // 6. Non-tail calls should still work correctly 48 + function normalFib(n) { 49 + if (n <= 1) return n; 50 + return normalFib(n - 1) + normalFib(n - 2); 51 + } 52 + console.log('normalFib(10):', normalFib(10)); // 55 53 + 54 + // 7. Tail call with different argument count 55 + function tailMultiArg(a, b, c) { 56 + if (a <= 0) return b + c; 57 + return tailMultiArg(a - 1, b + 1, c + 2); 58 + } 59 + console.log('tailMultiArg(1000, 0, 0):', tailMultiArg(1000, 0, 0)); // 1000 + 2000 = 3000 60 + 61 + // 8. Tail position in ternary 62 + function ternaryTail(n) { 63 + return n <= 0 ? 'done' : ternaryTail(n - 1); 64 + } 65 + console.log('ternaryTail(50000):', ternaryTail(50000)); // done 66 + 67 + // 9. Tail position in logical expressions should NOT be optimized 68 + // (the result goes through boolean coercion, not a true tail call) 69 + function lastElement(arr, i = 0) { 70 + if (i >= arr.length - 1) return arr[i]; 71 + return lastElement(arr, i + 1); 72 + } 73 + console.log('lastElement([1,2,3,4,5]):', lastElement([1, 2, 3, 4, 5])); // 5 74 + 75 + console.log('\nAll TCO tests passed!');