porffor 0.14.0-af9ac5ad4 → 0.14.0-b1e1c2265

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -40,11 +40,11 @@ const todo = (scope, msg, expectsValue = undefined) => {
40
40
  }
41
41
  };
42
42
 
43
- const isFuncType = type => type === 'FunctionDeclaration' || type === 'FunctionExpression' || type === 'ArrowFunctionExpression';
44
- const hasFuncWithName = name => {
45
- const func = funcs.find(x => x.name === name);
46
- return !!(func || builtinFuncs[name] || importedFuncs[name] || internalConstrs[name]);
47
- };
43
+ const isFuncType = type =>
44
+ type === 'FunctionDeclaration' || type === 'FunctionExpression' || type === 'ArrowFunctionExpression';
45
+ const hasFuncWithName = name =>
46
+ funcIndex[name] != null || builtinFuncs[name] != null || importedFuncs[name] != null || internalConstrs[name] != null;
47
+
48
48
  const generate = (scope, decl, global = false, name = undefined, valueUnused = false) => {
49
49
  switch (decl.type) {
50
50
  case 'BinaryExpression':
@@ -147,14 +147,16 @@ const generate = (scope, decl, global = false, name = undefined, valueUnused = f
147
147
  return generateMember(scope, decl, global, name);
148
148
 
149
149
  case 'ExportNamedDeclaration':
150
- // hack to flag new func for export
151
- const funcsBefore = funcs.length;
150
+ const funcsBefore = funcs.map(x => x.name);
152
151
  generate(scope, decl.declaration);
153
152
 
154
- if (funcsBefore !== funcs.length) {
155
- // new func added
156
- const newFunc = funcs[funcs.length - 1];
157
- newFunc.export = true;
153
+ // set new funcs as exported
154
+ if (funcsBefore.length !== funcs.length) {
155
+ const newFuncs = funcs.filter(x => !funcsBefore.includes(x.name)).filter(x => !x.internal);
156
+
157
+ for (const x of newFuncs) {
158
+ x.export = true;
159
+ }
158
160
  }
159
161
 
160
162
  return [];
@@ -361,7 +363,7 @@ const localTmp = (scope, name, type = valtypeBinary) => {
361
363
  };
362
364
 
363
365
  const isIntOp = op => op && ((op[0] >= 0x45 && op[0] <= 0x4f) || (op[0] >= 0x67 && op[0] <= 0x78) || op[0] === 0x41);
364
- const isFloatToIntOp = op => op && (op[0] >= 0xb7 && op[0] <= 0xba);
366
+ const isIntToFloatOp = op => op && (op[0] >= 0xb7 && op[0] <= 0xba);
365
367
 
366
368
  const performLogicOp = (scope, op, left, right, leftType, rightType) => {
367
369
  const checks = {
@@ -378,10 +380,10 @@ const performLogicOp = (scope, op, left, right, leftType, rightType) => {
378
380
 
379
381
  // if we can, use int tmp and convert at the end to help prevent unneeded conversions
380
382
  // (like if we are in an if condition - very common)
381
- const leftIsInt = isFloatToIntOp(left[left.length - 1]);
382
- const rightIsInt = isFloatToIntOp(right[right.length - 1]);
383
+ const leftWasInt = isIntToFloatOp(left[left.length - 1]);
384
+ const rightWasInt = isIntToFloatOp(right[right.length - 1]);
383
385
 
384
- const canInt = leftIsInt && rightIsInt;
386
+ const canInt = leftWasInt && rightWasInt;
385
387
 
386
388
  if (canInt) {
387
389
  // remove int -> float conversions from left and right
@@ -646,9 +648,9 @@ const compareStrings = (scope, left, right, bytestrings = false) => {
646
648
  [ Opcodes.i32_add ],
647
649
  [ Opcodes.local_tee, index ],
648
650
 
649
- // if index != index end (length * sizeof valtype), loop
651
+ // if index < index end (length * sizeof valtype), loop
650
652
  [ Opcodes.local_get, indexEnd ],
651
- [ Opcodes.i32_ne ],
653
+ [ Opcodes.i32_lt_s ],
652
654
  [ Opcodes.br_if, 0 ],
653
655
  [ Opcodes.end ],
654
656
 
@@ -666,8 +668,8 @@ const compareStrings = (scope, left, right, bytestrings = false) => {
666
668
  ];
667
669
  };
668
670
 
669
- const truthy = (scope, wasm, type, intIn = false, intOut = false) => {
670
- if (isFloatToIntOp(wasm[wasm.length - 1])) return [
671
+ const truthy = (scope, wasm, type, intIn = false, intOut = false, forceTruthyMode = undefined) => {
672
+ if (isIntToFloatOp(wasm[wasm.length - 1])) return [
671
673
  ...wasm,
672
674
  ...(!intIn && intOut ? [ Opcodes.i32_to_u ] : [])
673
675
  ];
@@ -676,47 +678,38 @@ const truthy = (scope, wasm, type, intIn = false, intOut = false) => {
676
678
  const useTmp = knownType(scope, type) == null;
677
679
  const tmp = useTmp && localTmp(scope, `#logicinner_tmp${intIn ? '_int' : ''}`, intIn ? Valtype.i32 : valtypeBinary);
678
680
 
679
- const def = [
680
- // if value != 0
681
- ...(!useTmp ? [] : [ [ Opcodes.local_get, tmp ] ]),
681
+ const def = (truthyMode => {
682
+ if (truthyMode === 'full') return [
683
+ // if value != 0 or NaN
684
+ ...(!useTmp ? [] : [ [ Opcodes.local_get, tmp ] ]),
685
+ ...(intIn ? [ ] : [ Opcodes.i32_to ]),
682
686
 
683
- // ...(intIn ? [ [ Opcodes.i32_eqz ] ] : [ ...Opcodes.eqz ]),
684
- // [ Opcodes.i32_eqz ],
687
+ [ Opcodes.i32_eqz ],
688
+ [ Opcodes.i32_eqz ],
685
689
 
686
- // ...(intIn ? [
687
- // ...number(0, Valtype.i32),
688
- // [ Opcodes.i32_gt_s ]
689
- // ] : [
690
- // ...number(0),
691
- // [ Opcodes.f64_gt ]
692
- // ]),
693
-
694
- // ...(intOut ? [] : [ Opcodes.i32_from ]),
695
-
696
- // ...(intIn ? [] : [ Opcodes.i32_to ]),
697
- // [ Opcodes.if, intOut ? Valtype.i32 : valtypeBinary ],
698
- // ...number(1, intOut ? Valtype.i32 : valtypeBinary),
699
- // [ Opcodes.else ],
700
- // ...number(0, intOut ? Valtype.i32 : valtypeBinary),
701
- // [ Opcodes.end ],
702
-
703
- ...(!intOut || (intIn && intOut) ? [] : [ Opcodes.i32_to_u ]),
704
-
705
- /* Opcodes.eqz,
706
- [ Opcodes.i32_eqz ],
707
- Opcodes.i32_from */
708
- ];
690
+ ...(intOut ? [] : [ Opcodes.i32_from ]),
691
+ ];
692
+
693
+ if (truthyMode === 'no_negative') return [
694
+ // if value != 0 or NaN, non-binary output. negative numbers not truthy :/
695
+ ...(!useTmp ? [] : [ [ Opcodes.local_get, tmp ] ]),
696
+ ...(intIn ? [] : [ Opcodes.i32_to ]),
697
+ ...(intOut ? [] : [ Opcodes.i32_from ])
698
+ ];
699
+
700
+ if (truthyMode === 'no_nan_negative') return [
701
+ // simpler and faster but makes NaN truthy and negative numbers not truthy,
702
+ // plus non-binary output
703
+ ...(!useTmp ? [] : [ [ Opcodes.local_get, tmp ] ]),
704
+ ...(!intOut || (intIn && intOut) ? [] : [ Opcodes.i32_to_u ])
705
+ ];
706
+ })(forceTruthyMode ?? Prefs.truthy ?? 'full');
709
707
 
710
708
  return [
711
709
  ...wasm,
712
710
  ...(!useTmp ? [] : [ [ Opcodes.local_set, tmp ] ]),
713
711
 
714
712
  ...typeSwitch(scope, type, {
715
- // [TYPES.number]: def,
716
- [TYPES.array]: [
717
- // arrays are always truthy
718
- ...number(1, intOut ? Valtype.i32 : valtypeBinary)
719
- ],
720
713
  [TYPES.string]: [
721
714
  ...(!useTmp ? [] : [ [ Opcodes.local_get, tmp ] ]),
722
715
  ...(intIn ? [] : [ Opcodes.i32_to_u ]),
@@ -752,10 +745,6 @@ const falsy = (scope, wasm, type, intIn = false, intOut = false) => {
752
745
  ...(!useTmp ? [] : [ [ Opcodes.local_set, tmp ] ]),
753
746
 
754
747
  ...typeSwitch(scope, type, {
755
- [TYPES.array]: [
756
- // arrays are always truthy
757
- ...number(0, intOut ? Valtype.i32 : valtypeBinary)
758
- ],
759
748
  [TYPES.string]: [
760
749
  ...(!useTmp ? [] : [ [ Opcodes.local_get, tmp ] ]),
761
750
  ...(intIn ? [] : [ Opcodes.i32_to_u ]),
@@ -999,7 +988,7 @@ const performOp = (scope, op, left, right, leftType, rightType, _global = false,
999
988
  // if both are true
1000
989
  [ Opcodes.i32_and ],
1001
990
  [ Opcodes.if, Blocktype.void ],
1002
- ...compareStrings(scope, [ [ Opcodes.local_get, tmpLeft ] ], [ [ Opcodes.local_get, tmpRight ] ]),
991
+ ...compareStrings(scope, [ [ Opcodes.local_get, tmpLeft ] ], [ [ Opcodes.local_get, tmpRight ] ], false),
1003
992
  ...(op === '!==' || op === '!=' ? [ [ Opcodes.i32_eqz ] ] : []),
1004
993
  [ Opcodes.br, 1 ],
1005
994
  [ Opcodes.end ],
@@ -1048,14 +1037,14 @@ const generateBinaryExp = (scope, decl, _global, _name) => {
1048
1037
  return out;
1049
1038
  };
1050
1039
 
1051
- const asmFuncToAsm = (func, { name = '#unknown_asm_func', params = [], locals = [], returns = [], localInd = 0 }) => {
1052
- return func({ name, params, locals, returns, localInd }, {
1040
+ const asmFuncToAsm = (func, scope) => {
1041
+ return func(scope, {
1053
1042
  TYPES, TYPE_NAMES, typeSwitch, makeArray, makeString, allocPage, internalThrow,
1054
- builtin: name => {
1055
- let idx = funcIndex[name] ?? importedFuncs[name];
1056
- if (idx === undefined && builtinFuncs[name]) {
1057
- includeBuiltin(null, name);
1058
- idx = funcIndex[name];
1043
+ builtin: n => {
1044
+ let idx = funcIndex[n] ?? importedFuncs[n];
1045
+ if (idx == null && builtinFuncs[n]) {
1046
+ includeBuiltin(null, n);
1047
+ idx = funcIndex[n];
1059
1048
  }
1060
1049
 
1061
1050
  return idx;
@@ -1063,7 +1052,7 @@ const asmFuncToAsm = (func, { name = '#unknown_asm_func', params = [], locals =
1063
1052
  });
1064
1053
  };
1065
1054
 
1066
- const asmFunc = (name, { wasm, params, locals: localTypes, globals: globalTypes = [], globalInits, returns, returnType, localNames = [], globalNames = [], data: _data = [], callsSelf = false }) => {
1055
+ const asmFunc = (name, { wasm, params, locals: localTypes, globals: globalTypes = [], globalInits, returns, returnType, localNames = [], globalNames = [], data: _data = [], table = false }) => {
1067
1056
  const existing = funcs.find(x => x.name === name);
1068
1057
  if (existing) return existing;
1069
1058
 
@@ -1081,7 +1070,22 @@ const asmFunc = (name, { wasm, params, locals: localTypes, globals: globalTypes
1081
1070
  data.push(copy);
1082
1071
  }
1083
1072
 
1084
- if (typeof wasm === 'function') wasm = asmFuncToAsm(wasm, { name, params, locals, returns, localInd: allLocals.length });
1073
+ const func = {
1074
+ name,
1075
+ params,
1076
+ locals,
1077
+ localInd: allLocals.length,
1078
+ returns,
1079
+ returnType: returnType ?? TYPES.number,
1080
+ internal: true,
1081
+ index: currentFuncIndex++,
1082
+ table
1083
+ };
1084
+
1085
+ funcs.push(func);
1086
+ funcIndex[name] = func.index;
1087
+
1088
+ if (typeof wasm === 'function') wasm = asmFuncToAsm(wasm, func);
1085
1089
 
1086
1090
  let baseGlobalIdx, i = 0;
1087
1091
  for (const type of globalTypes) {
@@ -1100,25 +1104,14 @@ const asmFunc = (name, { wasm, params, locals: localTypes, globals: globalTypes
1100
1104
  }
1101
1105
  }
1102
1106
 
1103
- const func = {
1104
- name,
1105
- params,
1106
- locals,
1107
- returns,
1108
- returnType: returnType ?? TYPES.number,
1109
- wasm,
1110
- internal: true,
1111
- index: currentFuncIndex++
1112
- };
1113
-
1114
- if (callsSelf) for (const inst of wasm) {
1115
- if (inst[0] === Opcodes.call && inst[1] === -1) {
1116
- inst[1] = func.index;
1107
+ if (table) for (const inst of wasm) {
1108
+ if (inst[0] === Opcodes.i32_load16_u && inst.at(-1) === 'read_argc') {
1109
+ inst.splice(2, 99);
1110
+ inst.push(...unsignedLEB128(allocPage({}, 'func argc lut') * pageSize));
1117
1111
  }
1118
1112
  }
1119
1113
 
1120
- funcs.push(func);
1121
- funcIndex[name] = func.index;
1114
+ func.wasm = wasm;
1122
1115
 
1123
1116
  return func;
1124
1117
  };
@@ -1470,16 +1463,11 @@ const countLeftover = wasm => {
1470
1463
  else if (inst[0] === Opcodes.return) count = 0;
1471
1464
  else if (inst[0] === Opcodes.call) {
1472
1465
  let func = funcs.find(x => x.index === inst[1]);
1473
- if (inst[1] === -1) {
1474
- // todo: count for calling self
1475
- } else if (!func && inst[1] < importedFuncs.length) {
1476
- count -= importedFuncs[inst[1]].params;
1477
- count += importedFuncs[inst[1]].returns;
1466
+ if (inst[1] < importedFuncs.length) {
1467
+ func = importedFuncs[inst[1]];
1468
+ count = count - func.params + func.returns;
1478
1469
  } else {
1479
- if (func) {
1480
- count -= func.params.length;
1481
- } else count--;
1482
- if (func) count += func.returns.length;
1470
+ count = count - func.params.length + func.returns.length;
1483
1471
  }
1484
1472
  } else if (inst[0] === Opcodes.call_indirect) {
1485
1473
  count--; // funcidx
@@ -1632,6 +1620,7 @@ const generateCall = (scope, decl, _global, _name, unusedValue = false) => {
1632
1620
 
1633
1621
  if (!funcIndex[rhemynName]) {
1634
1622
  const func = Rhemyn[funcName](regex, currentFuncIndex++, rhemynName);
1623
+ func.internal = true;
1635
1624
 
1636
1625
  funcIndex[func.name] = func.index;
1637
1626
  funcs.push(func);
@@ -1803,11 +1792,6 @@ const generateCall = (scope, decl, _global, _name, unusedValue = false) => {
1803
1792
 
1804
1793
  if (idx === undefined && internalConstrs[name]) return internalConstrs[name].generate(scope, decl, _global, _name);
1805
1794
 
1806
- if (idx === undefined && name === scope.name) {
1807
- // hack: calling self, func generator will fix later
1808
- idx = -1;
1809
- }
1810
-
1811
1795
  if (idx === undefined && name.startsWith('__Porffor_wasm_')) {
1812
1796
  const wasmOps = {
1813
1797
  // pointer, align, offset
@@ -1859,36 +1843,128 @@ const generateCall = (scope, decl, _global, _name, unusedValue = false) => {
1859
1843
  const [ local, global ] = lookupName(scope, name);
1860
1844
  if (!Prefs.indirectCalls || local == null) return internalThrow(scope, 'TypeError', `${unhackName(name)} is not a function`, true);
1861
1845
 
1862
- // todo: only works when:
1863
- // 1. arg count matches arg count of function
1864
- // 2. function uses typedParams and typedReturns
1846
+ // todo: only works when function uses typedParams and typedReturns
1847
+
1848
+ const indirectMode = Prefs.indirectCallMode ?? 'vararg';
1849
+ // options: vararg, strict
1850
+ // - strict: simpler, smaller size usage, no func argc lut needed.
1851
+ // ONLY works when arg count of call == arg count of function being called
1852
+ // - vararg: large size usage, cursed.
1853
+ // works when arg count of call != arg count of function being called*
1854
+ // * most of the time, some edgecases
1865
1855
 
1866
1856
  funcs.table = true;
1857
+ scope.table = true;
1867
1858
 
1868
1859
  let args = decl.arguments;
1869
- let argWasm = [];
1860
+ let out = [];
1861
+
1862
+ let locals = [];
1863
+
1864
+ if (indirectMode === 'vararg') {
1865
+ const minArgc = Prefs.indirectCallMinArgc ?? 3;
1866
+
1867
+ if (args.length < minArgc) {
1868
+ args = args.concat(new Array(minArgc - args.length).fill(DEFAULT_VALUE));
1869
+ }
1870
+ }
1870
1871
 
1871
1872
  for (let i = 0; i < args.length; i++) {
1872
1873
  const arg = args[i];
1873
- argWasm = argWasm.concat(generate(scope, arg));
1874
+ out = out.concat(generate(scope, arg));
1874
1875
 
1875
1876
  if (valtypeBinary !== Valtype.i32 && (
1876
1877
  (builtinFuncs[name] && builtinFuncs[name].params[i * (typedParams ? 2 : 1)] === Valtype.i32) ||
1877
1878
  (importedFuncs[name] && name.startsWith('profile'))
1878
1879
  )) {
1879
- argWasm.push(Opcodes.i32_to);
1880
+ out.push(Opcodes.i32_to);
1881
+ }
1882
+
1883
+ out = out.concat(getNodeType(scope, arg));
1884
+
1885
+ if (indirectMode === 'vararg') {
1886
+ const typeLocal = localTmp(scope, `#indirect_arg${i}_type`, Valtype.i32);
1887
+ const valLocal = localTmp(scope, `#indirect_arg${i}_val`);
1888
+
1889
+ locals.push([valLocal, typeLocal]);
1890
+
1891
+ out.push(
1892
+ [ Opcodes.local_set, typeLocal ],
1893
+ [ Opcodes.local_set, valLocal ]
1894
+ );
1895
+ }
1896
+ }
1897
+
1898
+ if (indirectMode === 'strict') {
1899
+ return typeSwitch(scope, getNodeType(scope, decl.callee), {
1900
+ [TYPES.function]: [
1901
+ ...argWasm,
1902
+ [ global ? Opcodes.global_get : Opcodes.local_get, local.idx ],
1903
+ Opcodes.i32_to_u,
1904
+ [ Opcodes.call_indirect, args.length, 0 ],
1905
+ ...setLastType(scope)
1906
+ ],
1907
+ default: internalThrow(scope, 'TypeError', `${unhackName(name)} is not a function`, true)
1908
+ });
1909
+ }
1910
+
1911
+ // hi, I will now explain how vararg mode works:
1912
+ // wasm's indirect_call instruction requires you know the func type at compile-time
1913
+ // since we have varargs (variable argument count), we do not know it.
1914
+ // we could just store args in memory and not use wasm func args,
1915
+ // but that is slow (probably) and breaks js exports.
1916
+ // instead, we generate every* possibility of argc and use different indirect_call
1917
+ // ops for each one, with type depending on argc for that branch.
1918
+ // then we load the argc for the wanted function from a memory lut,
1919
+ // and call the branch with the matching argc we require.
1920
+ // sorry, yes it is very cursed (and size inefficient), but indirect calls
1921
+ // are kind of rare anyway (mostly callbacks) so I am not concerned atm.
1922
+ // *for argc 0-3, in future (todo:) the max number should be
1923
+ // dynamically changed to the max argc of any func in the js file.
1924
+
1925
+ const funcLocal = localTmp(scope, `#indirect_func`, Valtype.i32);
1926
+
1927
+ const gen = argc => {
1928
+ const out = [];
1929
+ for (let i = 0; i < argc; i++) {
1930
+ out.push(
1931
+ [ Opcodes.local_get, locals[i][0] ],
1932
+ [ Opcodes.local_get, locals[i][1] ]
1933
+ );
1880
1934
  }
1881
1935
 
1882
- argWasm = argWasm.concat(getNodeType(scope, arg));
1936
+ out.push(
1937
+ [ Opcodes.local_get, funcLocal ],
1938
+ [ Opcodes.call_indirect, argc, 0 ],
1939
+ ...setLastType(scope)
1940
+ )
1941
+
1942
+ return out;
1943
+ };
1944
+
1945
+ const tableBc = {};
1946
+ for (let i = 0; i <= args.length; i++) {
1947
+ tableBc[i] = gen(i);
1883
1948
  }
1884
1949
 
1950
+ // todo/perf: check if we should use br_table here or just generate our own big if..elses
1951
+
1885
1952
  return typeSwitch(scope, getNodeType(scope, decl.callee), {
1886
1953
  [TYPES.function]: [
1887
- ...argWasm,
1954
+ ...out,
1955
+
1888
1956
  [ global ? Opcodes.global_get : Opcodes.local_get, local.idx ],
1889
1957
  Opcodes.i32_to_u,
1890
- [ Opcodes.call_indirect, args.length, 0 ],
1891
- ...setLastType(scope)
1958
+ [ Opcodes.local_set, funcLocal ],
1959
+
1960
+ ...brTable([
1961
+ // get argc of func we are calling
1962
+ [ Opcodes.local_get, funcLocal ],
1963
+ ...number(ValtypeSize.i16, Valtype.i32),
1964
+ [ Opcodes.i32_mul ],
1965
+
1966
+ [ Opcodes.i32_load16_u, 0, ...unsignedLEB128(allocPage(scope, 'func argc lut') * pageSize), 'read_argc' ]
1967
+ ], tableBc, valtypeBinary)
1892
1968
  ],
1893
1969
  default: internalThrow(scope, 'TypeError', `${unhackName(name)} is not a function`, true)
1894
1970
  });
@@ -1897,11 +1973,10 @@ const generateCall = (scope, decl, _global, _name, unusedValue = false) => {
1897
1973
  return internalThrow(scope, 'ReferenceError', `${unhackName(name)} is not defined`, true);
1898
1974
  }
1899
1975
 
1900
- const func = funcs.find(x => x.index === idx);
1901
-
1902
- const userFunc = (funcIndex[name] && !importedFuncs[name] && !builtinFuncs[name] && !internalConstrs[name]) || idx === -1;
1976
+ const func = funcs[idx - importedFuncs.length]; // idx === scope.index ? scope : funcs.find(x => x.index === idx);
1977
+ const userFunc = func && !func.internal;
1903
1978
  const typedParams = userFunc || builtinFuncs[name]?.typedParams;
1904
- const typedReturns = (func ? func.returnType == null : userFunc) || builtinFuncs[name]?.typedReturns;
1979
+ const typedReturns = (func && func.returnType == null) || builtinFuncs[name]?.typedReturns;
1905
1980
  const paramCount = func && (typedParams ? func.params.length / 2 : func.params.length);
1906
1981
 
1907
1982
  let args = decl.arguments;
@@ -2046,16 +2121,17 @@ const brTable = (input, bc, returns) => {
2046
2121
  }
2047
2122
 
2048
2123
  for (let i = 0; i < count; i++) {
2049
- if (i === 0) out.push([ Opcodes.block, returns, 'br table start' ]);
2124
+ // if (i === 0) out.push([ Opcodes.block, returns, 'br table start' ]);
2125
+ if (i === 0) out.push([ Opcodes.block, returns ]);
2050
2126
  else out.push([ Opcodes.block, Blocktype.void ]);
2051
2127
  }
2052
2128
 
2053
- const nums = keys.filter(x => +x);
2129
+ const nums = keys.filter(x => +x >= 0);
2054
2130
  const offset = Math.min(...nums);
2055
2131
  const max = Math.max(...nums);
2056
2132
 
2057
2133
  const table = [];
2058
- let br = 1;
2134
+ let br = 0;
2059
2135
 
2060
2136
  for (let i = offset; i <= max; i++) {
2061
2137
  // if branch for this num, go to that block
@@ -2095,10 +2171,9 @@ const brTable = (input, bc, returns) => {
2095
2171
  br--;
2096
2172
  }
2097
2173
 
2098
- return [
2099
- ...out,
2100
- [ Opcodes.end, 'br table end' ]
2101
- ];
2174
+ out.push([ Opcodes.end ]);
2175
+
2176
+ return out;
2102
2177
  };
2103
2178
 
2104
2179
  const typeSwitch = (scope, type, bc, returns = valtypeBinary) => {
@@ -2504,6 +2579,7 @@ const generateUnary = (scope, decl) => {
2504
2579
  ];
2505
2580
 
2506
2581
  case '!':
2582
+ // todo/perf: optimize !!
2507
2583
  // !=
2508
2584
  return falsy(scope, generate(scope, decl.argument), getNodeType(scope, decl.argument), false, false);
2509
2585
 
@@ -3043,14 +3119,18 @@ const generateThrow = (scope, decl) => {
3043
3119
  };
3044
3120
 
3045
3121
  const generateTry = (scope, decl) => {
3046
- if (decl.finalizer) return todo(scope, 'try finally not implemented yet');
3122
+ // todo: handle control-flow pre-exit for finally
3123
+ // "Immediately before a control-flow statement (return, throw, break, continue) is executed in the try block or catch block."
3047
3124
 
3048
3125
  const out = [];
3049
3126
 
3127
+ const finalizer = decl.finalizer ? generate(scope, decl.finalizer) : [];
3128
+
3050
3129
  out.push([ Opcodes.try, Blocktype.void ]);
3051
3130
  depth.push('try');
3052
3131
 
3053
3132
  out.push(...generate(scope, decl.block));
3133
+ out.push(...finalizer);
3054
3134
 
3055
3135
  if (decl.handler) {
3056
3136
  depth.pop();
@@ -3058,6 +3138,7 @@ const generateTry = (scope, decl) => {
3058
3138
 
3059
3139
  out.push([ Opcodes.catch_all ]);
3060
3140
  out.push(...generate(scope, decl.handler.body));
3141
+ out.push(...finalizer);
3061
3142
  }
3062
3143
 
3063
3144
  out.push([ Opcodes.end ]);
@@ -3155,8 +3236,13 @@ const makeArray = (scope, decl, global = false, name = '$undeclared', initEmpty
3155
3236
  // todo: can we just have 1 undeclared array? probably not? but this is not really memory efficient
3156
3237
  const uniqueName = name === '$undeclared' ? name + Math.random().toString().slice(2) : name;
3157
3238
 
3158
- if (Prefs.scopedPageNames) scope.arrays.set(name, allocPage(scope, `${getAllocType(itemType)}: ${scope.name}/${uniqueName}`, itemType) * pageSize);
3159
- else scope.arrays.set(name, allocPage(scope, `${getAllocType(itemType)}: ${uniqueName}`, itemType) * pageSize);
3239
+ let page;
3240
+ if (Prefs.scopedPageNames) page = allocPage(scope, `${getAllocType(itemType)}: ${scope.name}/${uniqueName}`, itemType);
3241
+ else page = allocPage(scope, `${getAllocType(itemType)}: ${uniqueName}`, itemType);
3242
+
3243
+ // hack: use 1 for page 0 pointer for fast truthiness
3244
+ const ptr = page === 0 ? 1 : (page * pageSize);
3245
+ scope.arrays.set(name, ptr);
3160
3246
  }
3161
3247
 
3162
3248
  const pointer = scope.arrays.get(name);
@@ -3365,8 +3451,7 @@ const generateMember = (scope, decl, _global, _name) => {
3365
3451
  if (decl.property.name === 'length') {
3366
3452
  const func = funcs.find(x => x.name === name);
3367
3453
  if (func) {
3368
- const userFunc = funcIndex[name] && !importedFuncs[name] && !builtinFuncs[name] && !internalConstrs[name];
3369
- const typedParams = userFunc || builtinFuncs[name]?.typedParams;
3454
+ const typedParams = !func.internal || builtinFuncs[name]?.typedParams;
3370
3455
  return withType(scope, number(typedParams ? func.params.length / 2 : func.params.length), TYPES.number);
3371
3456
  }
3372
3457
 
@@ -3592,33 +3677,39 @@ const generateFunc = (scope, decl) => {
3592
3677
  const name = decl.id ? decl.id.name : `anonymous_${randId()}`;
3593
3678
  const params = decl.params ?? [];
3594
3679
 
3595
- // const innerScope = { ...scope };
3596
3680
  // TODO: share scope/locals between !!!
3597
- const innerScope = {
3681
+ const func = {
3598
3682
  locals: {},
3599
3683
  localInd: 0,
3600
3684
  // value, type
3601
3685
  returns: [ valtypeBinary, Valtype.i32 ],
3602
3686
  throws: false,
3603
- name
3687
+ name,
3688
+ index: currentFuncIndex++
3604
3689
  };
3605
3690
 
3606
3691
  if (typedInput && decl.returnType) {
3607
3692
  const { type } = extractTypeAnnotation(decl.returnType);
3608
- if (type != null && !Prefs.indirectCalls) {
3609
- innerScope.returnType = type;
3610
- innerScope.returns = [ valtypeBinary ];
3693
+ // if (type != null && !Prefs.indirectCalls) {
3694
+ if (type != null) {
3695
+ func.returnType = type;
3696
+ func.returns = [ valtypeBinary ];
3611
3697
  }
3612
3698
  }
3613
3699
 
3614
3700
  for (let i = 0; i < params.length; i++) {
3615
- allocVar(innerScope, params[i].name, false);
3701
+ const name = params[i].name;
3702
+ // if (name == null) return todo('non-identifier args are not supported');
3703
+
3704
+ allocVar(func, name, false);
3616
3705
 
3617
3706
  if (typedInput && params[i].typeAnnotation) {
3618
- addVarMetadata(innerScope, params[i].name, false, extractTypeAnnotation(params[i]));
3707
+ addVarMetadata(func, name, false, extractTypeAnnotation(params[i]));
3619
3708
  }
3620
3709
  }
3621
3710
 
3711
+ func.params = Object.values(func.locals).map(x => x.type);
3712
+
3622
3713
  let body = objectHack(decl.body);
3623
3714
  if (decl.type === 'ArrowFunctionExpression' && decl.expression) {
3624
3715
  // hack: () => 0 -> () => return 0
@@ -3628,37 +3719,23 @@ const generateFunc = (scope, decl) => {
3628
3719
  };
3629
3720
  }
3630
3721
 
3631
- const wasm = generate(innerScope, body);
3632
- const func = {
3633
- name,
3634
- params: Object.values(innerScope.locals).slice(0, params.length * 2).map(x => x.type),
3635
- index: currentFuncIndex++,
3636
- ...innerScope
3637
- };
3638
3722
  funcIndex[name] = func.index;
3723
+ funcs.push(func);
3639
3724
 
3640
- if (name === 'main') func.gotLastType = true;
3725
+ const wasm = generate(func, body);
3726
+ func.wasm = wasm;
3641
3727
 
3642
- // quick hack fixes
3643
- for (const inst of wasm) {
3644
- if (inst[0] === Opcodes.call && inst[1] === -1) {
3645
- inst[1] = func.index;
3646
- }
3647
- }
3728
+ if (name === 'main') func.gotLastType = true;
3648
3729
 
3649
3730
  // add end return if not found
3650
3731
  if (name !== 'main' && wasm[wasm.length - 1]?.[0] !== Opcodes.return && countLeftover(wasm) === 0) {
3651
3732
  wasm.push(
3652
3733
  ...number(0),
3653
- ...(innerScope.returnType != null ? [] : number(TYPES.undefined, Valtype.i32)),
3734
+ ...(func.returnType != null ? [] : number(TYPES.undefined, Valtype.i32)),
3654
3735
  [ Opcodes.return ]
3655
3736
  );
3656
3737
  }
3657
3738
 
3658
- func.wasm = wasm;
3659
-
3660
- funcs.push(func);
3661
-
3662
3739
  return func;
3663
3740
  };
3664
3741
 
@@ -3762,7 +3839,7 @@ const internalConstrs = {
3762
3839
  generate: (scope, decl) => {
3763
3840
  // todo: boolean object when used as constructor
3764
3841
  const arg = decl.arguments[0] ?? DEFAULT_VALUE;
3765
- return truthy(scope, generate(scope, arg), getNodeType(scope, arg));
3842
+ return truthy(scope, generate(scope, arg), getNodeType(scope, arg), false, false, 'full');
3766
3843
  },
3767
3844
  type: TYPES.boolean,
3768
3845
  length: 1
@@ -3907,9 +3984,8 @@ export default program => {
3907
3984
 
3908
3985
  if (Prefs.astLog) console.log(JSON.stringify(program.body.body, null, 2));
3909
3986
 
3910
- generateFunc(scope, program);
3987
+ const main = generateFunc(scope, program);
3911
3988
 
3912
- const main = funcs[funcs.length - 1];
3913
3989
  main.export = true;
3914
3990
  main.returns = [ valtypeBinary, Valtype.i32 ];
3915
3991
 
@@ -3936,7 +4012,7 @@ export default program => {
3936
4012
  }
3937
4013
 
3938
4014
  // if blank main func and other exports, remove it
3939
- if (main.wasm.length === 0 && funcs.reduce((acc, x) => acc + (x.export ? 1 : 0), 0) > 1) funcs.splice(funcs.length - 1, 1);
4015
+ if (main.wasm.length === 0 && funcs.reduce((acc, x) => acc + (x.export ? 1 : 0), 0) > 1) funcs.splice(main.index - importedFuncs.length, 1);
3940
4016
 
3941
4017
  return { funcs, globals, tags, exceptions, pages, data };
3942
4018
  };