porffor 0.2.0-a759814 → 0.2.0-c597461

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.
package/README.md CHANGED
@@ -118,7 +118,7 @@ No particular order and no guarentees, just what could happen soon™
118
118
  - Objects
119
119
  - Basic object expressions (eg `{}`, `{ a: 0 }`)
120
120
  - Wasm
121
- - *Basic* Wasm engine (interpreter) in js
121
+ - *Basic* Wasm engine (interpreter) in JS
122
122
  - More math operators (`**`, etc)
123
123
  - `do { ... } while (...)`
124
124
  - Rewrite `console.log` to work with strings/arrays
@@ -130,7 +130,10 @@ No particular order and no guarentees, just what could happen soon™
130
130
  - Rewrite local indexes per func for smallest local header and remove unused idxs
131
131
  - Smarter inline selection (snapshots?)
132
132
  - Remove const ifs (`if (true)`, etc)
133
- - Use type(script) information to remove unneeded typechecker code
133
+ - Experiment with byte strings?
134
+ - Runtime
135
+ - WASI target
136
+ - Run precompiled Wasm file if given
134
137
  - Cool proposals
135
138
  - [Optional Chaining Assignment](https://github.com/tc39/proposal-optional-chaining-assignment)
136
139
  - [Modulus and Additional Integer Math](https://github.com/tc39/proposal-integer-and-modulus-math)
@@ -139,6 +142,9 @@ No particular order and no guarentees, just what could happen soon™
139
142
  - [Seeded Pseudo-Random Numbers](https://github.com/tc39/proposal-seeded-random)
140
143
  - [`do` expressions](https://github.com/tc39/proposal-do-expressions)
141
144
  - [String Trim Characters](https://github.com/Kingwl/proposal-string-trim-characters)
145
+ - Posts
146
+ - Inlining investigation
147
+ - Self hosted testing?
142
148
 
143
149
  ## Performance
144
150
  *For the things it supports most of the time*, Porffor is blazingly fast compared to most interpreters, and common engines running without JIT. For those with JIT, it is not that much slower like a traditional interpreter would be; mostly the same or a bit faster/slower depending on what.
@@ -165,10 +171,12 @@ Mostly for reducing size. I do not really care about compiler perf/time as long
165
171
  - Remove unneeded blocks (no `br`s inside)
166
172
  - Remove unused imports
167
173
  - Use data segments for initing arrays/strings
174
+ - (Likely more not documented yet, todo)
168
175
 
169
176
  ### Wasm module
170
177
  - Type cache/index (no repeated types)
171
178
  - No main func if empty (and other exports)
179
+ - No tags if unused/optimized out
172
180
 
173
181
  ## Test262
174
182
  Porffor can run Test262 via some hacks/transforms which remove unsupported features whilst still doing the same asserts (eg simpler error messages using literals only). It currently passes >10% (see latest commit desc for latest and details). Use `node test262` to test, it will also show a difference of overall results between the last commit and current results.
@@ -188,7 +196,7 @@ Porffor can run Test262 via some hacks/transforms which remove unsupported featu
188
196
  - `wasmSpec.js`: "enums"/info from wasm spec
189
197
  - `wrap.js`: wrapper for compiler which instantiates and produces nice exports
190
198
 
191
- - `runner`: contains utils for running js with the compiler
199
+ - `runner`: contains utils for running JS with the compiler
192
200
  - `index.js`: the main file, you probably want to use this
193
201
  - `info.js`: runs with extra info printed
194
202
  - `repl.js`: basic repl (uses `node:repl`)
@@ -201,10 +209,13 @@ Porffor can run Test262 via some hacks/transforms which remove unsupported featu
201
209
  - `test262`: test262 runner and utils
202
210
 
203
211
  ## Usecases
204
- Basically none (other than giving people headaches). Potential ideas to come?
212
+ Basically none right now (other than giving people headaches). Potential ideas:
213
+ - Safety. As Porffor is written in JS, a memory-safe language\*, and compiles JS to Wasm, a fully sandboxed environment\*, it is quite safe. (\* These rely on the underlying implementations being secure. You could also run Wasm, or even Porffor itself, with an interpreter instead of a JIT for bonus security points too.)
214
+ - Compiling JS to native binaries. This is still very early, [`2c`](#2c) is not that good yet :(
215
+ - More in future probably?
205
216
 
206
217
  ## Usage
207
- Basically nothing will work :). See files in `test` for examples.
218
+ Basically nothing will work :). See files in `test` and `bench` for examples.
208
219
 
209
220
  1. Clone repo
210
221
  2. `npm install`
@@ -241,20 +252,20 @@ You can also use Deno (`deno run -A ...` instead of `node ...`), or Bun (`bun ..
241
252
  - `-compile-hints` to enable V8 compilation hints (experimental + doesn't seem to do much?)
242
253
 
243
254
  ## VSCode extension
244
- There is a vscode extension in `porffor-for-vscode` which tweaks js syntax highlighting to be nicer with porffor features (eg highlighting wasm inside of inline asm).
255
+ There is a vscode extension in `porffor-for-vscode` which tweaks JS syntax highlighting to be nicer with porffor features (eg highlighting wasm inside of inline asm).
245
256
 
246
257
  ## Isn't this the same as AssemblyScript/other Wasm langs?
247
258
  No. they are not alike at all internally and have very different goals/ideals:
248
259
  - Porffor is made as a generic JS engine, not for Wasm stuff specifically
249
- - Porffor takes in JS, not a different language or typescript
250
- - Porffor is made in pure JS and compiles itself, not using Binaryen/etc
260
+ - Porffor primarily consumes JS
261
+ - Porffor is written in pure JS and compiles itself, not using Binaryen/etc
251
262
  - (Also I didn't know it existed when I started this, lol)
252
263
 
253
264
  ## FAQ
254
265
 
255
266
  ### 1. Why the name?
256
267
  `purple` in Welsh is `porffor`. Why purple?
257
- - No other js engine is purple colored
268
+ - No other JS engine is purple colored
258
269
  - Purple is pretty cool
259
270
  - Purple apparently represents "ambition", which is.. one word to describe this project
260
271
  - The hard to speak name is also the noise your brain makes in reaction to this idea!
@@ -1,4 +1,4 @@
1
- import { Blocktype, Opcodes, Valtype } from "./wasmSpec.js";
1
+ import { Blocktype, Opcodes, Valtype, ValtypeSize } from "./wasmSpec.js";
2
2
  import { number, i32x4 } from "./embedding.js";
3
3
 
4
4
  export const importedFuncs = [
@@ -29,6 +29,21 @@ for (let i = 0; i < importedFuncs.length; i++) {
29
29
 
30
30
  const char = c => number(c.charCodeAt(0));
31
31
 
32
+ const printStaticStr = str => {
33
+ const out = [];
34
+
35
+ for (let i = 0; i < str.length; i++) {
36
+ out.push(
37
+ // ...number(str.charCodeAt(i)),
38
+ ...number(str.charCodeAt(i), Valtype.i32),
39
+ Opcodes.i32_from_u,
40
+ [ Opcodes.call, importedFuncs.printChar ]
41
+ );
42
+ }
43
+
44
+ return out;
45
+ };
46
+
32
47
  // todo: somehow diff between these (undefined != null) while remaining falsey in wasm as a number value
33
48
  export const UNDEFINED = 0;
34
49
  export const NULL = 0;
@@ -187,12 +202,125 @@ export const BuiltinFuncs = function() {
187
202
 
188
203
 
189
204
  this.__console_log = {
190
- params: [ valtypeBinary ],
191
- locals: [],
205
+ params: [ valtypeBinary, Valtype.i32 ],
206
+ typedParams: true,
207
+ locals: [ Valtype.i32, Valtype.i32 ],
192
208
  returns: [],
193
- wasm: [
194
- [ Opcodes.local_get, 0 ],
195
- [ Opcodes.call, importedFuncs.print ],
209
+ wasm: (scope, { TYPES, typeSwitch }) => [
210
+ ...typeSwitch(scope, [ [ Opcodes.local_get, 1 ] ], {
211
+ [TYPES.number]: [
212
+ [ Opcodes.local_get, 0 ],
213
+ [ Opcodes.call, importedFuncs.print ],
214
+ ],
215
+ [TYPES.boolean]: [
216
+ [ Opcodes.local_get, 0 ],
217
+ Opcodes.i32_to_u,
218
+ [ Opcodes.if, Blocktype.void ],
219
+ ...printStaticStr('true'),
220
+ [ Opcodes.else ],
221
+ ...printStaticStr('false'),
222
+ [ Opcodes.end ]
223
+ ],
224
+ [TYPES.string]: [
225
+ // simply print a string :))
226
+ // cache input pointer as i32
227
+ [ Opcodes.local_get, 0 ],
228
+ Opcodes.i32_to_u,
229
+ [ Opcodes.local_tee, 2 ],
230
+
231
+ // make end pointer
232
+ [ Opcodes.i32_load, Math.log2(ValtypeSize.i32) - 1, 0 ],
233
+ ...number(ValtypeSize.i16, Valtype.i32),
234
+ [ Opcodes.i32_mul ],
235
+
236
+ [ Opcodes.local_get, 2 ],
237
+ [ Opcodes.i32_add ],
238
+ [ Opcodes.local_set, 3 ],
239
+
240
+ [ Opcodes.loop, Blocktype.void ],
241
+
242
+ // print current char
243
+ [ Opcodes.local_get, 2 ],
244
+ [ Opcodes.i32_load16_u, Math.log2(ValtypeSize.i16) - 1, ValtypeSize.i32 ],
245
+ Opcodes.i32_from_u,
246
+ [ Opcodes.call, importedFuncs.printChar ],
247
+
248
+ // increment pointer by sizeof i16
249
+ [ Opcodes.local_get, 2 ],
250
+ ...number(ValtypeSize.i16, Valtype.i32),
251
+ [ Opcodes.i32_add ],
252
+ [ Opcodes.local_tee, 2 ],
253
+
254
+ // if pointer != end pointer, loop
255
+ [ Opcodes.local_get, 3 ],
256
+ [ Opcodes.i32_ne ],
257
+ [ Opcodes.br_if, 0 ],
258
+
259
+ [ Opcodes.end ]
260
+ ],
261
+ [TYPES._array]: [
262
+ ...printStaticStr('[ '),
263
+
264
+ // cache input pointer as i32
265
+ [ Opcodes.local_get, 0 ],
266
+ Opcodes.i32_to_u,
267
+ [ Opcodes.local_tee, 2 ],
268
+
269
+ // make end pointer
270
+ [ Opcodes.i32_load, Math.log2(ValtypeSize.i32) - 1, 0 ],
271
+ ...number(ValtypeSize[valtype], Valtype.i32),
272
+ [ Opcodes.i32_mul ],
273
+
274
+ [ Opcodes.local_get, 2 ],
275
+ [ Opcodes.i32_add ],
276
+ [ Opcodes.local_set, 3 ],
277
+
278
+ [ Opcodes.loop, Blocktype.void ],
279
+
280
+ // print current char
281
+ [ Opcodes.local_get, 2 ],
282
+ [ Opcodes.load, Math.log2(ValtypeSize.i16) - 1, ValtypeSize.i32 ],
283
+ [ Opcodes.call, importedFuncs.print ],
284
+
285
+ // increment pointer by sizeof valtype
286
+ [ Opcodes.local_get, 2 ],
287
+ ...number(ValtypeSize[valtype], Valtype.i32),
288
+ [ Opcodes.i32_add ],
289
+ [ Opcodes.local_tee, 2 ],
290
+
291
+ // if pointer != end pointer, print separator and loop
292
+ [ Opcodes.local_get, 3 ],
293
+ [ Opcodes.i32_ne ],
294
+ [ Opcodes.if, Blocktype.void ],
295
+ ...printStaticStr(', '),
296
+ [ Opcodes.br, 1 ],
297
+ [ Opcodes.end ],
298
+
299
+ [ Opcodes.end ],
300
+
301
+ ...printStaticStr(' ]'),
302
+ ],
303
+ [TYPES.undefined]: [
304
+ ...printStaticStr('undefined')
305
+ ],
306
+ [TYPES.function]: [
307
+ ...printStaticStr('function () {}')
308
+ ],
309
+ [TYPES.object]: [
310
+ [ Opcodes.local_get, 0 ],
311
+ Opcodes.i32_to_u,
312
+ [ Opcodes.if, Blocktype.void ],
313
+ ...printStaticStr('{}'),
314
+ [ Opcodes.else ],
315
+ ...printStaticStr('null'),
316
+ [ Opcodes.end ]
317
+ ],
318
+ default: [
319
+ [ Opcodes.local_get, 0 ],
320
+ [ Opcodes.call, importedFuncs.print ],
321
+ ]
322
+ }, Blocktype.void),
323
+
196
324
  ...char('\n'),
197
325
  [ Opcodes.call, importedFuncs.printChar ]
198
326
  ]
@@ -1,5 +1,5 @@
1
1
  import { Blocktype, Opcodes, Valtype, PageSize, ValtypeSize } from "./wasmSpec.js";
2
- import { ieee754_binary64, signedLEB128, unsignedLEB128 } from "./encoding.js";
2
+ import { ieee754_binary64, signedLEB128, unsignedLEB128, encodeVector } from "./encoding.js";
3
3
  import { operatorOpcode } from "./expression.js";
4
4
  import { BuiltinFuncs, BuiltinVars, importedFuncs, NULL, UNDEFINED } from "./builtins.js";
5
5
  import { PrototypeFuncs } from "./prototype.js";
@@ -55,7 +55,7 @@ const todo = msg => {
55
55
  };
56
56
 
57
57
  const isFuncType = type => type === 'FunctionDeclaration' || type === 'FunctionExpression' || type === 'ArrowFunctionExpression';
58
- const generate = (scope, decl, global = false, name = undefined) => {
58
+ const generate = (scope, decl, global = false, name = undefined, valueUnused = false) => {
59
59
  switch (decl.type) {
60
60
  case 'BinaryExpression':
61
61
  return generateBinaryExp(scope, decl, global, name);
@@ -68,7 +68,12 @@ const generate = (scope, decl, global = false, name = undefined) => {
68
68
 
69
69
  case 'ArrowFunctionExpression':
70
70
  case 'FunctionDeclaration':
71
- generateFunc(scope, decl);
71
+ const func = generateFunc(scope, decl);
72
+
73
+ if (decl.type.endsWith('Expression')) {
74
+ return number(func.index);
75
+ }
76
+
72
77
  return [];
73
78
 
74
79
  case 'BlockStatement':
@@ -81,7 +86,7 @@ const generate = (scope, decl, global = false, name = undefined) => {
81
86
  return generateExp(scope, decl);
82
87
 
83
88
  case 'CallExpression':
84
- return generateCall(scope, decl, global, name);
89
+ return generateCall(scope, decl, global, name, valueUnused);
85
90
 
86
91
  case 'NewExpression':
87
92
  return generateNew(scope, decl, global, name);
@@ -680,6 +685,15 @@ const truthy = (scope, wasm, type, intIn = false, intOut = false) => {
680
685
  [ Opcodes.i32_eqz ], */
681
686
  ...(intOut ? [] : [ Opcodes.i32_from_u ])
682
687
  ],
688
+ [TYPES._bytestring]: [ // duplicate of string
689
+ [ Opcodes.local_get, tmp ],
690
+ ...(intIn ? [] : [ Opcodes.i32_to_u ]),
691
+
692
+ // get length
693
+ [ Opcodes.i32_load, Math.log2(ValtypeSize.i32) - 1, 0 ],
694
+
695
+ ...(intOut ? [] : [ Opcodes.i32_from_u ])
696
+ ],
683
697
  default: def
684
698
  }, intOut ? Valtype.i32 : valtypeBinary)
685
699
  ];
@@ -707,6 +721,17 @@ const falsy = (scope, wasm, type, intIn = false, intOut = false) => {
707
721
  [ Opcodes.i32_eqz ],
708
722
  ...(intOut ? [] : [ Opcodes.i32_from_u ])
709
723
  ],
724
+ [TYPES._bytestring]: [ // duplicate of string
725
+ [ Opcodes.local_get, tmp ],
726
+ ...(intIn ? [] : [ Opcodes.i32_to_u ]),
727
+
728
+ // get length
729
+ [ Opcodes.i32_load, Math.log2(ValtypeSize.i32) - 1, 0 ],
730
+
731
+ // if length == 0
732
+ [ Opcodes.i32_eqz ],
733
+ ...(intOut ? [] : [ Opcodes.i32_from_u ])
734
+ ],
710
735
  default: [
711
736
  // if value == 0
712
737
  [ Opcodes.local_get, tmp ],
@@ -947,6 +972,18 @@ const asmFunc = (name, { wasm, params, locals: localTypes, globals: globalTypes
947
972
  locals[nameParam(i)] = { idx: i, type: allLocals[i] };
948
973
  }
949
974
 
975
+ if (typeof wasm === 'function') {
976
+ const scope = {
977
+ name,
978
+ params,
979
+ locals,
980
+ returns,
981
+ localInd: allLocals.length,
982
+ };
983
+
984
+ wasm = wasm(scope, { TYPES, typeSwitch, makeArray });
985
+ }
986
+
950
987
  let baseGlobalIdx, i = 0;
951
988
  for (const type of globalTypes) {
952
989
  if (baseGlobalIdx === undefined) baseGlobalIdx = globalInd;
@@ -1027,7 +1064,8 @@ const TYPES = {
1027
1064
 
1028
1065
  // these are not "typeof" types but tracked internally
1029
1066
  _array: 0x10,
1030
- _regexp: 0x11
1067
+ _regexp: 0x11,
1068
+ _bytestring: 0x12
1031
1069
  };
1032
1070
 
1033
1071
  const TYPE_NAMES = {
@@ -1041,7 +1079,8 @@ const TYPE_NAMES = {
1041
1079
  [TYPES.bigint]: 'BigInt',
1042
1080
 
1043
1081
  [TYPES._array]: 'Array',
1044
- [TYPES._regexp]: 'RegExp'
1082
+ [TYPES._regexp]: 'RegExp',
1083
+ [TYPES._bytestring]: 'ByteString'
1045
1084
  };
1046
1085
 
1047
1086
  const getType = (scope, _name) => {
@@ -1094,6 +1133,8 @@ const getNodeType = (scope, node) => {
1094
1133
  if (node.type === 'Literal') {
1095
1134
  if (node.regex) return TYPES._regexp;
1096
1135
 
1136
+ if (typeof node.value === 'string' && byteStringable(node.value)) return TYPES._bytestring;
1137
+
1097
1138
  return TYPES[typeof node.value];
1098
1139
  }
1099
1140
 
@@ -1107,6 +1148,15 @@ const getNodeType = (scope, node) => {
1107
1148
 
1108
1149
  if (node.type === 'CallExpression' || node.type === 'NewExpression') {
1109
1150
  const name = node.callee.name;
1151
+ if (!name) {
1152
+ // iife
1153
+ if (scope.locals['#last_type']) return [ getLastType(scope) ];
1154
+
1155
+ // presume
1156
+ // todo: warn here?
1157
+ return TYPES.number;
1158
+ }
1159
+
1110
1160
  const func = funcs.find(x => x.name === name);
1111
1161
 
1112
1162
  if (func) {
@@ -1201,7 +1251,7 @@ const getNodeType = (scope, node) => {
1201
1251
  if (node.operator === '!') return TYPES.boolean;
1202
1252
  if (node.operator === 'void') return TYPES.undefined;
1203
1253
  if (node.operator === 'delete') return TYPES.boolean;
1204
- if (node.operator === 'typeof') return TYPES.string;
1254
+ if (node.operator === 'typeof') return process.argv.includes('-bytestring') ? TYPES._bytestring : TYPES.string;
1205
1255
 
1206
1256
  return TYPES.number;
1207
1257
  }
@@ -1227,6 +1277,23 @@ const getNodeType = (scope, node) => {
1227
1277
  return ret;
1228
1278
  };
1229
1279
 
1280
+ const toString = (scope, wasm, type) => {
1281
+ const tmp = localTmp(scope, '#tostring_tmp');
1282
+ return [
1283
+ ...wasm,
1284
+ [ Opcodes.local_set, tmp ],
1285
+
1286
+ ...typeSwitch(scope, type, {
1287
+ [TYPES.string]: [
1288
+ [ Opcodes.local_get, tmp ]
1289
+ ],
1290
+ [TYPES.undefined]: [
1291
+ // [ Opcodes.]
1292
+ ]
1293
+ })
1294
+ ]
1295
+ };
1296
+
1230
1297
  const generateLiteral = (scope, decl, global, name) => {
1231
1298
  if (decl.value === null) return number(NULL);
1232
1299
 
@@ -1244,16 +1311,7 @@ const generateLiteral = (scope, decl, global, name) => {
1244
1311
  return number(decl.value ? 1 : 0);
1245
1312
 
1246
1313
  case 'string':
1247
- const str = decl.value;
1248
- const rawElements = new Array(str.length);
1249
- let j = 0;
1250
- for (let i = 0; i < str.length; i++) {
1251
- rawElements[i] = str.charCodeAt(i);
1252
- }
1253
-
1254
- return makeArray(scope, {
1255
- rawElements
1256
- }, global, name, false, 'i16')[0];
1314
+ return makeString(scope, decl.value, global, name);
1257
1315
 
1258
1316
  default:
1259
1317
  return todo(`cannot generate literal of type ${typeof decl.value}`);
@@ -1274,9 +1332,9 @@ const countLeftover = wasm => {
1274
1332
 
1275
1333
  if (depth === 0)
1276
1334
  if ([Opcodes.throw,Opcodes.drop, Opcodes.local_set, Opcodes.global_set].includes(inst[0])) count--;
1277
- else if ([null, Opcodes.i32_eqz, Opcodes.i64_eqz, Opcodes.f64_ceil, Opcodes.f64_floor, Opcodes.f64_trunc, Opcodes.f64_nearest, Opcodes.f64_sqrt, Opcodes.local_tee, Opcodes.i32_wrap_i64, Opcodes.i64_extend_i32_s, Opcodes.i64_extend_i32_u, Opcodes.f32_demote_f64, Opcodes.f64_promote_f32, Opcodes.f64_convert_i32_s, Opcodes.f64_convert_i32_u, Opcodes.i32_clz, Opcodes.i32_ctz, Opcodes.i32_popcnt, Opcodes.f64_neg, Opcodes.end, Opcodes.i32_trunc_sat_f64_s[0], Opcodes.i32x4_extract_lane, Opcodes.i16x8_extract_lane, Opcodes.i32_load, Opcodes.i64_load, Opcodes.f64_load, Opcodes.v128_load, Opcodes.i32_load16_u, Opcodes.i32_load16_s, Opcodes.memory_grow].includes(inst[0]) && (inst[0] !== 0xfc || inst[1] < 0x0a)) {}
1335
+ else if ([null, Opcodes.i32_eqz, Opcodes.i64_eqz, Opcodes.f64_ceil, Opcodes.f64_floor, Opcodes.f64_trunc, Opcodes.f64_nearest, Opcodes.f64_sqrt, Opcodes.local_tee, Opcodes.i32_wrap_i64, Opcodes.i64_extend_i32_s, Opcodes.i64_extend_i32_u, Opcodes.f32_demote_f64, Opcodes.f64_promote_f32, Opcodes.f64_convert_i32_s, Opcodes.f64_convert_i32_u, Opcodes.i32_clz, Opcodes.i32_ctz, Opcodes.i32_popcnt, Opcodes.f64_neg, Opcodes.end, Opcodes.i32_trunc_sat_f64_s[0], Opcodes.i32x4_extract_lane, Opcodes.i16x8_extract_lane, Opcodes.i32_load, Opcodes.i64_load, Opcodes.f64_load, Opcodes.v128_load, Opcodes.i32_load16_u, Opcodes.i32_load16_s, Opcodes.i32_load8_u, Opcodes.i32_load8_s, Opcodes.memory_grow].includes(inst[0]) && (inst[0] !== 0xfc || inst[1] < 0x0a)) {}
1278
1336
  else if ([Opcodes.local_get, Opcodes.global_get, Opcodes.f64_const, Opcodes.i32_const, Opcodes.i64_const, Opcodes.v128_const].includes(inst[0])) count++;
1279
- else if ([Opcodes.i32_store, Opcodes.i64_store, Opcodes.f64_store, Opcodes.i32_store16].includes(inst[0])) count -= 2;
1337
+ else if ([Opcodes.i32_store, Opcodes.i64_store, Opcodes.f64_store, Opcodes.i32_store16, Opcodes.i32_store8].includes(inst[0])) count -= 2;
1280
1338
  else if (Opcodes.memory_copy[0] === inst[0] && Opcodes.memory_copy[1] === inst[1]) count -= 3;
1281
1339
  else if (inst[0] === Opcodes.return) count = 0;
1282
1340
  else if (inst[0] === Opcodes.call) {
@@ -1302,7 +1360,7 @@ const disposeLeftover = wasm => {
1302
1360
  const generateExp = (scope, decl) => {
1303
1361
  const expression = decl.expression;
1304
1362
 
1305
- const out = generate(scope, expression);
1363
+ const out = generate(scope, expression, undefined, undefined, true);
1306
1364
  disposeLeftover(out);
1307
1365
 
1308
1366
  return out;
@@ -1360,7 +1418,7 @@ const RTArrayUtil = {
1360
1418
  ]
1361
1419
  };
1362
1420
 
1363
- const generateCall = (scope, decl, _global, _name) => {
1421
+ const generateCall = (scope, decl, _global, _name, unusedValue = false) => {
1364
1422
  /* const callee = decl.callee;
1365
1423
  const args = decl.arguments;
1366
1424
 
@@ -1479,10 +1537,18 @@ const generateCall = (scope, decl, _global, _name) => {
1479
1537
  // use local for cached i32 length as commonly used
1480
1538
  const lengthLocal = localTmp(scope, '__proto_length_cache', Valtype.i32);
1481
1539
  const pointerLocal = localTmp(scope, '__proto_pointer_cache', Valtype.i32);
1482
- const getPointer = [ [ Opcodes.local_get, pointerLocal ] ];
1483
1540
 
1484
1541
  // TODO: long-term, prototypes should be their individual separate funcs
1485
1542
 
1543
+ const rawPointer = [
1544
+ ...generate(scope, target),
1545
+ Opcodes.i32_to_u
1546
+ ];
1547
+
1548
+ const usePointerCache = !Object.values(protoCands).every(x => x.noPointerCache === true);
1549
+ const getPointer = usePointerCache ? [ [ Opcodes.local_get, pointerLocal ] ] : rawPointer;
1550
+
1551
+ let allOptUnused = true;
1486
1552
  let lengthI32CacheUsed = false;
1487
1553
  const protoBC = {};
1488
1554
  for (const x in protoCands) {
@@ -1502,6 +1568,7 @@ const generateCall = (scope, decl, _global, _name) => {
1502
1568
  const protoLocal = protoFunc.local ? localTmp(scope, `__${protoName}_tmp`, protoFunc.local) : -1;
1503
1569
  const protoLocal2 = protoFunc.local2 ? localTmp(scope, `__${protoName}_tmp2`, protoFunc.local2) : -1;
1504
1570
 
1571
+ let optUnused = false;
1505
1572
  const protoOut = protoFunc(getPointer, {
1506
1573
  getCachedI32: () => {
1507
1574
  lengthI32CacheUsed = true;
@@ -1516,10 +1583,15 @@ const generateCall = (scope, decl, _global, _name) => {
1516
1583
  return makeArray(scope, {
1517
1584
  rawElements: new Array(length)
1518
1585
  }, _global, _name, true, itemType);
1586
+ }, () => {
1587
+ optUnused = true;
1588
+ return unusedValue;
1519
1589
  });
1520
1590
 
1591
+ if (!optUnused) allOptUnused = false;
1592
+
1521
1593
  protoBC[x] = [
1522
- [ Opcodes.block, valtypeBinary ],
1594
+ [ Opcodes.block, unusedValue && optUnused ? Blocktype.void : valtypeBinary ],
1523
1595
  ...protoOut,
1524
1596
 
1525
1597
  ...number(protoFunc.returnType ?? TYPES.number, Valtype.i32),
@@ -1528,11 +1600,13 @@ const generateCall = (scope, decl, _global, _name) => {
1528
1600
  ];
1529
1601
  }
1530
1602
 
1531
- return [
1532
- ...generate(scope, target),
1603
+ // todo: if some cands use optUnused and some don't, we will probably crash
1533
1604
 
1534
- Opcodes.i32_to_u,
1535
- [ Opcodes.local_set, pointerLocal ],
1605
+ return [
1606
+ ...(usePointerCache ? [
1607
+ ...rawPointer,
1608
+ [ Opcodes.local_set, pointerLocal ],
1609
+ ] : []),
1536
1610
 
1537
1611
  ...(!lengthI32CacheUsed ? [] : [
1538
1612
  ...RTArrayUtil.getLengthI32(getPointer),
@@ -1544,7 +1618,7 @@ const generateCall = (scope, decl, _global, _name) => {
1544
1618
 
1545
1619
  // TODO: error better
1546
1620
  default: internalThrow(scope, 'TypeError', `'${protoName}' proto func tried to be called on a type without an impl`)
1547
- }, valtypeBinary),
1621
+ }, allOptUnused && unusedValue ? Blocktype.void : valtypeBinary),
1548
1622
  ];
1549
1623
  }
1550
1624
  }
@@ -1591,7 +1665,9 @@ const generateCall = (scope, decl, _global, _name) => {
1591
1665
  const func = funcs.find(x => x.index === idx);
1592
1666
 
1593
1667
  const userFunc = (funcIndex[name] && !importedFuncs[name] && !builtinFuncs[name] && !internalConstrs[name]) || idx === -1;
1594
- const paramCount = func && (userFunc ? func.params.length / 2 : func.params.length);
1668
+ const typedParams = userFunc || builtinFuncs[name]?.typedParams;
1669
+ const typedReturn = userFunc || builtinFuncs[name]?.typedReturn;
1670
+ const paramCount = func && (typedParams ? func.params.length / 2 : func.params.length);
1595
1671
 
1596
1672
  let args = decl.arguments;
1597
1673
  if (func && args.length < paramCount) {
@@ -1609,12 +1685,12 @@ const generateCall = (scope, decl, _global, _name) => {
1609
1685
  let out = [];
1610
1686
  for (const arg of args) {
1611
1687
  out = out.concat(generate(scope, arg));
1612
- if (userFunc) out = out.concat(getNodeType(scope, arg));
1688
+ if (typedParams) out = out.concat(getNodeType(scope, arg));
1613
1689
  }
1614
1690
 
1615
1691
  out.push([ Opcodes.call, idx ]);
1616
1692
 
1617
- if (!userFunc) {
1693
+ if (!typedReturn) {
1618
1694
  // let type;
1619
1695
  // if (builtinFuncs[name]) type = TYPES[builtinFuncs[name].returnType ?? 'number'];
1620
1696
  // if (internalConstrs[name]) type = internalConstrs[name].type;
@@ -1665,14 +1741,102 @@ const knownType = (scope, type) => {
1665
1741
  return null;
1666
1742
  };
1667
1743
 
1744
+ const brTable = (input, bc, returns) => {
1745
+ const out = [];
1746
+ const keys = Object.keys(bc);
1747
+ const count = keys.length;
1748
+
1749
+ if (count === 1) {
1750
+ // return [
1751
+ // ...input,
1752
+ // ...bc[keys[0]]
1753
+ // ];
1754
+ return bc[keys[0]];
1755
+ }
1756
+
1757
+ if (count === 2) {
1758
+ // just use if else
1759
+ const other = keys.find(x => x !== 'default');
1760
+ return [
1761
+ ...input,
1762
+ ...number(other, Valtype.i32),
1763
+ [ Opcodes.i32_eq ],
1764
+ [ Opcodes.if, returns ],
1765
+ ...bc[other],
1766
+ [ Opcodes.else ],
1767
+ ...bc.default,
1768
+ [ Opcodes.end ]
1769
+ ];
1770
+ }
1771
+
1772
+ for (let i = 0; i < count; i++) {
1773
+ if (i === 0) out.push([ Opcodes.block, returns, 'br table start' ]);
1774
+ else out.push([ Opcodes.block, Blocktype.void ]);
1775
+ }
1776
+
1777
+ const nums = keys.filter(x => +x);
1778
+ const offset = Math.min(...nums);
1779
+ const max = Math.max(...nums);
1780
+
1781
+ const table = [];
1782
+ let br = 1;
1783
+
1784
+ for (let i = offset; i <= max; i++) {
1785
+ // if branch for this num, go to that block
1786
+ if (bc[i]) {
1787
+ table.push(br);
1788
+ br++;
1789
+ continue;
1790
+ }
1791
+
1792
+ // else default
1793
+ table.push(0);
1794
+ }
1795
+
1796
+ out.push(
1797
+ [ Opcodes.block, Blocktype.void ],
1798
+ ...input,
1799
+ ...(offset > 0 ? [
1800
+ ...number(offset, Valtype.i32),
1801
+ [ Opcodes.i32_sub ]
1802
+ ] : []),
1803
+ [ Opcodes.br_table, ...encodeVector(table), 0 ]
1804
+ );
1805
+
1806
+ // if you can guess why we sort the wrong way and then reverse
1807
+ // (instead of just sorting the correct way)
1808
+ // dm me and if you are correct and the first person
1809
+ // I will somehow shout you out or something
1810
+ const orderedBc = keys.sort((a, b) => b - a).reverse();
1811
+
1812
+ br = count - 1;
1813
+ for (const x of orderedBc) {
1814
+ out.push(
1815
+ [ Opcodes.end ],
1816
+ ...bc[x],
1817
+ ...(br === 0 ? [] : [ [ Opcodes.br, br ] ])
1818
+ );
1819
+ br--;
1820
+ }
1821
+
1822
+ return [
1823
+ ...out,
1824
+ [ Opcodes.end, 'br table end' ]
1825
+ ];
1826
+ };
1827
+
1668
1828
  const typeSwitch = (scope, type, bc, returns = valtypeBinary) => {
1829
+ if (!process.argv.includes('-bytestring')) delete bc[TYPES._bytestring];
1830
+
1669
1831
  const known = knownType(scope, type);
1670
1832
  if (known != null) {
1671
1833
  return bc[known] ?? bc.default;
1672
1834
  }
1673
1835
 
1674
- const tmp = localTmp(scope, '#typeswitch_tmp', Valtype.i32);
1836
+ if (process.argv.includes('-typeswitch-use-brtable'))
1837
+ return brTable(type, bc, returns);
1675
1838
 
1839
+ const tmp = localTmp(scope, '#typeswitch_tmp', Valtype.i32);
1676
1840
  const out = [
1677
1841
  ...type,
1678
1842
  [ Opcodes.local_set, tmp ],
@@ -1794,6 +1958,11 @@ const generateVar = (scope, decl) => {
1794
1958
  }
1795
1959
 
1796
1960
  let idx = allocVar(scope, name, global);
1961
+
1962
+ if (typedInput && x.id.typeAnnotation) {
1963
+ addVarMetadata(scope, name, global, extractTypeAnnotation(x.id));
1964
+ }
1965
+
1797
1966
  if (x.init) {
1798
1967
  out = out.concat(generate(scope, x.init, global, name));
1799
1968
 
@@ -1803,10 +1972,6 @@ const generateVar = (scope, decl) => {
1803
1972
 
1804
1973
  // hack: this follows spec properly but is mostly unneeded 😅
1805
1974
  // out.push(...setType(scope, name, x.init ? getNodeType(scope, x.init) : TYPES.undefined));
1806
-
1807
- if (typedInput && x.id.typeAnnotation) {
1808
- addVarMetadata(scope, name, global, extractTypeAnnotation(x.id));
1809
- }
1810
1975
  }
1811
1976
 
1812
1977
  return out;
@@ -2053,6 +2218,8 @@ const generateUnary = (scope, decl) => {
2053
2218
  [TYPES.undefined]: makeString(scope, 'undefined', false, '#typeof_result'),
2054
2219
  [TYPES.function]: makeString(scope, 'function', false, '#typeof_result'),
2055
2220
 
2221
+ [TYPES._bytestring]: makeString(scope, 'string', false, '#typeof_result'),
2222
+
2056
2223
  // object and internal types
2057
2224
  default: makeString(scope, 'object', false, '#typeof_result'),
2058
2225
  });
@@ -2241,6 +2408,7 @@ const generateForOf = (scope, decl) => {
2241
2408
  }
2242
2409
 
2243
2410
  // set type for local
2411
+ // todo: optimize away counter and use end pointer
2244
2412
  out.push(...typeSwitch(scope, getNodeType(scope, decl.right), {
2245
2413
  [TYPES._array]: [
2246
2414
  ...setType(scope, leftName, TYPES.number),
@@ -2459,7 +2627,8 @@ const StoreOps = {
2459
2627
  f64: Opcodes.f64_store,
2460
2628
 
2461
2629
  // expects i32 input!
2462
- i16: Opcodes.i32_store16
2630
+ i8: Opcodes.i32_store8,
2631
+ i16: Opcodes.i32_store16,
2463
2632
  };
2464
2633
 
2465
2634
  let data = [];
@@ -2478,6 +2647,15 @@ const compileBytes = (val, itemType, signed = true) => {
2478
2647
  }
2479
2648
  };
2480
2649
 
2650
+ const getAllocType = itemType => {
2651
+ switch (itemType) {
2652
+ case 'i8': return 'bytestring';
2653
+ case 'i16': return 'string';
2654
+
2655
+ default: return 'array';
2656
+ }
2657
+ };
2658
+
2481
2659
  const makeArray = (scope, decl, global = false, name = '$undeclared', initEmpty = false, itemType = valtype) => {
2482
2660
  const out = [];
2483
2661
 
@@ -2487,7 +2665,7 @@ const makeArray = (scope, decl, global = false, name = '$undeclared', initEmpty
2487
2665
 
2488
2666
  // todo: can we just have 1 undeclared array? probably not? but this is not really memory efficient
2489
2667
  const uniqueName = name === '$undeclared' ? name + Math.random().toString().slice(2) : name;
2490
- arrays.set(name, allocPage(`${itemType === 'i16' ? 'string' : 'array'}: ${uniqueName}`, itemType) * pageSize);
2668
+ arrays.set(name, allocPage(`${getAllocType(itemType)}: ${uniqueName}`, itemType) * pageSize);
2491
2669
  }
2492
2670
 
2493
2671
  const pointer = arrays.get(name);
@@ -2533,7 +2711,7 @@ const makeArray = (scope, decl, global = false, name = '$undeclared', initEmpty
2533
2711
  out.push(
2534
2712
  ...number(0, Valtype.i32),
2535
2713
  ...(useRawElements ? number(elements[i], Valtype[valtype]) : generate(scope, elements[i])),
2536
- [ storeOp, Math.log2(ValtypeSize[itemType]) - 1, ...unsignedLEB128(pointer + ValtypeSize.i32 + i * ValtypeSize[itemType]) ]
2714
+ [ storeOp, (Math.log2(ValtypeSize[itemType]) || 1) - 1, ...unsignedLEB128(pointer + ValtypeSize.i32 + i * ValtypeSize[itemType]) ]
2537
2715
  );
2538
2716
  }
2539
2717
 
@@ -2543,15 +2721,29 @@ const makeArray = (scope, decl, global = false, name = '$undeclared', initEmpty
2543
2721
  return [ out, pointer ];
2544
2722
  };
2545
2723
 
2724
+ const byteStringable = str => {
2725
+ if (!process.argv.includes('-bytestring')) return false;
2726
+
2727
+ for (let i = 0; i < str.length; i++) {
2728
+ if (str.charCodeAt(i) > 0xFF) return false;
2729
+ }
2730
+
2731
+ return true;
2732
+ };
2733
+
2546
2734
  const makeString = (scope, str, global = false, name = '$undeclared') => {
2547
2735
  const rawElements = new Array(str.length);
2736
+ let byteStringable = process.argv.includes('-bytestring');
2548
2737
  for (let i = 0; i < str.length; i++) {
2549
- rawElements[i] = str.charCodeAt(i);
2738
+ const c = str.charCodeAt(i);
2739
+ rawElements[i] = c;
2740
+
2741
+ if (byteStringable && c > 0xFF) byteStringable = false;
2550
2742
  }
2551
2743
 
2552
2744
  return makeArray(scope, {
2553
2745
  rawElements
2554
- }, global, name, false, 'i16')[0];
2746
+ }, global, name, false, byteStringable ? 'i8' : 'i16')[0];
2555
2747
  };
2556
2748
 
2557
2749
  let arrays = new Map();
@@ -2581,7 +2773,7 @@ export const generateMember = (scope, decl, _global, _name) => {
2581
2773
 
2582
2774
  // // todo: we should only do this for strings but we don't know at compile-time :(
2583
2775
  // hack: this is naughty and will break things!
2584
- let newOut = number(0, Valtype.f64), newPointer = -1;
2776
+ let newOut = number(0, valtypeBinary), newPointer = -1;
2585
2777
  if (pages.hasString) {
2586
2778
  0, [ newOut, newPointer ] = makeArray(scope, {
2587
2779
  rawElements: new Array(1)
@@ -2642,6 +2834,34 @@ export const generateMember = (scope, decl, _global, _name) => {
2642
2834
  ...number(TYPES.string, Valtype.i32),
2643
2835
  setLastType(scope)
2644
2836
  ],
2837
+ [TYPES._bytestring]: [
2838
+ // setup new/out array
2839
+ ...newOut,
2840
+ [ Opcodes.drop ],
2841
+
2842
+ ...number(0, Valtype.i32), // base 0 for store later
2843
+
2844
+ ...generate(scope, decl.property),
2845
+ Opcodes.i32_to_u,
2846
+
2847
+ ...(aotPointer ? [] : [
2848
+ ...generate(scope, decl.object),
2849
+ Opcodes.i32_to_u,
2850
+ [ Opcodes.i32_add ]
2851
+ ]),
2852
+
2853
+ // load current string ind {arg}
2854
+ [ Opcodes.i32_load8_u, Math.log2(ValtypeSize.i16) - 1, ...unsignedLEB128((aotPointer ? pointer : 0) + ValtypeSize.i32) ],
2855
+
2856
+ // store to new string ind 0
2857
+ [ Opcodes.i32_store8, Math.log2(ValtypeSize.i16) - 1, ...unsignedLEB128(newPointer + ValtypeSize.i32) ],
2858
+
2859
+ // return new string (page)
2860
+ ...number(newPointer),
2861
+
2862
+ ...number(TYPES._bytestring, Valtype.i32),
2863
+ setLastType(scope)
2864
+ ],
2645
2865
 
2646
2866
  default: [ [ Opcodes.unreachable ] ]
2647
2867
  });
@@ -15,7 +15,7 @@ export default (wasm, name = '', ind = 0, locals = {}, params = [], returns = []
15
15
  if (name) out += `${makeSignature(params, returns)} ;; $${name} (${ind})\n`;
16
16
 
17
17
  const justLocals = Object.values(locals).sort((a, b) => a.idx - b.idx).slice(params.length);
18
- if (justLocals.length > 0) out += ` local ${justLocals.map(x => invValtype[x.type]).join(' ')}\n`;
18
+ if (name && justLocals.length > 0) out += ` local ${justLocals.map(x => invValtype[x.type]).join(' ')}\n`;
19
19
 
20
20
  let i = -1, lastInst;
21
21
  let byte = 0;
@@ -32,7 +32,7 @@ export default (wasm, name = '', ind = 0, locals = {}, params = [], returns = []
32
32
  inst = [ [ inst[0], inst[1] ], ...inst.slice(2) ];
33
33
  }
34
34
 
35
- if (inst[0] === Opcodes.end || inst[0] === Opcodes.else || inst[0] === Opcodes.catch_all) depth--;
35
+ if (depth > 0 && (inst[0] === Opcodes.end || inst[0] === Opcodes.else || inst[0] === Opcodes.catch_all)) depth--;
36
36
 
37
37
  out += ' '.repeat(Math.max(0, depth * 2));
38
38
 
@@ -119,7 +119,7 @@ export default (wasm, name = '', ind = 0, locals = {}, params = [], returns = []
119
119
  export const highlightAsm = asm => asm
120
120
  .replace(/(local|global|memory)\.[^\s]*/g, _ => `\x1B[31m${_}\x1B[0m`)
121
121
  .replace(/(i(8|16|32|64)x[0-9]+|v128)(\.[^\s]*)?/g, _ => `\x1B[34m${_}\x1B[0m`)
122
- .replace(/[^m](i32|i64|f32|f64|drop)(\.[^\s]*)?/g, _ => `${_[0]}\x1B[36m${_.slice(1)}\x1B[0m`)
122
+ .replace(/(i32|i64|f32|f64|drop)(\.[^\s]*)?/g, _ => `\x1B[36m${_}\x1B[0m`)
123
123
  .replace(/(return_call|call|br_if|br|return|rethrow|throw)/g, _ => `\x1B[35m${_}\x1B[0m`)
124
124
  .replace(/(block|loop|if|end|else|try|catch_all|catch|delegate)/g, _ => `\x1B[95m${_}\x1B[0m`)
125
125
  .replace(/unreachable/g, _ => `\x1B[91m${_}\x1B[0m`)
package/compiler/index.js CHANGED
@@ -52,7 +52,7 @@ export default (code, flags) => {
52
52
  if (process.argv.includes('-funcs')) logFuncs(funcs, globals, exceptions);
53
53
 
54
54
  const t2 = performance.now();
55
- opt(funcs, globals, pages);
55
+ opt(funcs, globals, pages, tags);
56
56
  if (flags.includes('info')) console.log(`3. optimized code in ${(performance.now() - t2).toFixed(2)}ms`);
57
57
 
58
58
  if (process.argv.includes('-opt-funcs')) logFuncs(funcs, globals, exceptions);
package/compiler/opt.js CHANGED
@@ -11,7 +11,7 @@ const performWasmOp = (op, a, b) => {
11
11
  }
12
12
  };
13
13
 
14
- export default (funcs, globals, pages) => {
14
+ export default (funcs, globals, pages, tags) => {
15
15
  const optLevel = parseInt(process.argv.find(x => x.startsWith('-O'))?.[2] ?? 1);
16
16
  if (optLevel === 0) return;
17
17
 
@@ -99,6 +99,8 @@ export default (funcs, globals, pages) => {
99
99
 
100
100
  if (process.argv.includes('-opt-inline-only')) return;
101
101
 
102
+ const tagUse = tags.reduce((acc, x) => { acc[x.idx] = 0; return acc; }, {});
103
+
102
104
  // wasm transform pass
103
105
  for (const f of funcs) {
104
106
  const wasm = f.wasm;
@@ -127,6 +129,8 @@ export default (funcs, globals, pages) => {
127
129
  if (inst[0] === Opcodes.local_get) getCount[inst[1]]++;
128
130
  if (inst[0] === Opcodes.local_set || inst[0] === Opcodes.local_tee) setCount[inst[1]]++;
129
131
 
132
+ if (inst[0] === Opcodes.throw) tagUse[inst[1]]++;
133
+
130
134
  if (inst[0] === Opcodes.block) {
131
135
  // remove unneeded blocks (no brs inside)
132
136
  // block
@@ -143,7 +147,7 @@ export default (funcs, globals, pages) => {
143
147
  depth--;
144
148
  if (depth <= 0) break;
145
149
  }
146
- if (op === Opcodes.br || op === Opcodes.br_if) {
150
+ if (op === Opcodes.br || op === Opcodes.br_if || op === Opcodes.br_table) {
147
151
  hasBranch = true;
148
152
  break;
149
153
  }
@@ -235,7 +239,7 @@ export default (funcs, globals, pages) => {
235
239
  }
236
240
 
237
241
  // remove setting last type if it is never gotten
238
- if (!f.gotLastType && inst[0] === Opcodes.local_set && inst[1] === lastType.idx) {
242
+ if (!f.gotLastType && inst[0] === Opcodes.local_set && inst[1] === lastType?.idx) {
239
243
  // replace this inst with drop
240
244
  wasm.splice(i, 1, [ Opcodes.drop ]); // remove this and last inst
241
245
  if (i > 0) i--;
@@ -541,5 +545,12 @@ export default (funcs, globals, pages) => {
541
545
  }
542
546
  }
543
547
 
548
+ for (const x in tagUse) {
549
+ if (tagUse[x] === 0) {
550
+ const el = tags.find(y => y.idx === x);
551
+ tags.splice(tags.indexOf(el), 1);
552
+ }
553
+ }
554
+
544
555
  // return funcs;
545
556
  };
@@ -16,7 +16,8 @@ const TYPES = {
16
16
 
17
17
  // these are not "typeof" types but tracked internally
18
18
  _array: 0x10,
19
- _regexp: 0x11
19
+ _regexp: 0x11,
20
+ _bytestring: 0x12
20
21
  };
21
22
 
22
23
  // todo: turn these into built-ins once arrays and these become less hacky
@@ -71,7 +72,7 @@ export const PrototypeFuncs = function() {
71
72
  ],
72
73
 
73
74
  // todo: only for 1 argument
74
- push: (pointer, length, wNewMember) => [
75
+ push: (pointer, length, wNewMember, _1, _2, _3, unusedValue) => [
75
76
  // get memory offset of array at last index (length)
76
77
  ...length.getCachedI32(),
77
78
  ...number(ValtypeSize[valtype], Valtype.i32),
@@ -92,22 +93,28 @@ export const PrototypeFuncs = function() {
92
93
  ...number(1, Valtype.i32),
93
94
  [ Opcodes.i32_add ],
94
95
 
95
- ...length.setCachedI32(),
96
- ...length.getCachedI32(),
96
+ ...(unusedValue() ? [] : [
97
+ ...length.setCachedI32(),
98
+ ...length.getCachedI32(),
99
+ ])
97
100
  ]),
98
101
 
99
- ...length.getCachedI32(),
100
- Opcodes.i32_from_u
102
+ ...(unusedValue() ? [] : [
103
+ ...length.getCachedI32(),
104
+ Opcodes.i32_from_u
105
+ ])
101
106
 
102
107
  // ...length.get()
103
108
  ],
104
109
 
105
- pop: (pointer, length) => [
110
+ pop: (pointer, length, _1, _2, _3, _4, unusedValue) => [
106
111
  // if length == 0, noop
107
112
  ...length.getCachedI32(),
108
113
  [ Opcodes.i32_eqz ],
109
114
  [ Opcodes.if, Blocktype.void ],
110
- ...number(UNDEFINED),
115
+ ...(unusedValue() ? [] : [
116
+ ...number(UNDEFINED),
117
+ ]),
111
118
  [ Opcodes.br, 1 ],
112
119
  [ Opcodes.end ],
113
120
 
@@ -119,19 +126,23 @@ export const PrototypeFuncs = function() {
119
126
  ...number(1, Valtype.i32),
120
127
  [ Opcodes.i32_sub ],
121
128
 
122
- ...length.setCachedI32(),
123
- ...length.getCachedI32(),
129
+ ...(unusedValue() ? [] : [
130
+ ...length.setCachedI32(),
131
+ ...length.getCachedI32(),
132
+ ])
124
133
  ]),
125
134
 
126
135
  // load last element
127
- ...length.getCachedI32(),
128
- ...number(ValtypeSize[valtype], Valtype.i32),
129
- [ Opcodes.i32_mul ],
136
+ ...(unusedValue() ? [] : [
137
+ ...length.getCachedI32(),
138
+ ...number(ValtypeSize[valtype], Valtype.i32),
139
+ [ Opcodes.i32_mul ],
130
140
 
131
- ...pointer,
132
- [ Opcodes.i32_add ],
141
+ ...pointer,
142
+ [ Opcodes.i32_add ],
133
143
 
134
- [ Opcodes.load, Math.log2(ValtypeSize[valtype]) - 1, ...unsignedLEB128(ValtypeSize.i32) ]
144
+ [ Opcodes.load, Math.log2(ValtypeSize[valtype]) - 1, ...unsignedLEB128(ValtypeSize.i32) ]
145
+ ])
135
146
  ],
136
147
 
137
148
  shift: (pointer, length) => [
@@ -472,8 +483,152 @@ export const PrototypeFuncs = function() {
472
483
  this[TYPES.string].at.returnType = TYPES.string;
473
484
  this[TYPES.string].charAt.returnType = TYPES.string;
474
485
  this[TYPES.string].charCodeAt.local = Valtype.i32;
486
+ this[TYPES.string].charCodeAt.noPointerCache = zeroChecks.charcodeat;
475
487
 
476
488
  this[TYPES.string].isWellFormed.local = Valtype.i32;
477
489
  this[TYPES.string].isWellFormed.local2 = Valtype.i32;
478
490
  this[TYPES.string].isWellFormed.returnType = TYPES.boolean;
491
+
492
+ if (process.argv.includes('-bytestring')) {
493
+ this[TYPES._bytestring] = {
494
+ at: (pointer, length, wIndex, iTmp, _, arrayShell) => {
495
+ const [ newOut, newPointer ] = arrayShell(1, 'i16');
496
+
497
+ return [
498
+ // setup new/out array
499
+ ...newOut,
500
+ [ Opcodes.drop ],
501
+
502
+ ...number(0, Valtype.i32), // base 0 for store later
503
+
504
+ ...wIndex,
505
+ Opcodes.i32_to_u,
506
+ [ Opcodes.local_tee, iTmp ],
507
+
508
+ // if index < 0: access index + array length
509
+ ...number(0, Valtype.i32),
510
+ [ Opcodes.i32_lt_s ],
511
+ [ Opcodes.if, Blocktype.void ],
512
+ [ Opcodes.local_get, iTmp ],
513
+ ...length.getCachedI32(),
514
+ [ Opcodes.i32_add ],
515
+ [ Opcodes.local_set, iTmp ],
516
+ [ Opcodes.end ],
517
+
518
+ // if still < 0 or >= length: return undefined
519
+ [ Opcodes.local_get, iTmp ],
520
+ ...number(0, Valtype.i32),
521
+ [ Opcodes.i32_lt_s ],
522
+
523
+ [ Opcodes.local_get, iTmp ],
524
+ ...length.getCachedI32(),
525
+ [ Opcodes.i32_ge_s ],
526
+ [ Opcodes.i32_or ],
527
+
528
+ [ Opcodes.if, Blocktype.void ],
529
+ ...number(UNDEFINED),
530
+ [ Opcodes.br, 1 ],
531
+ [ Opcodes.end ],
532
+
533
+ [ Opcodes.local_get, iTmp ],
534
+
535
+ ...pointer,
536
+ [ Opcodes.i32_add ],
537
+
538
+ // load current string ind {arg}
539
+ [ Opcodes.i32_load8_u, 0, ...unsignedLEB128(ValtypeSize.i32) ],
540
+
541
+ // store to new string ind 0
542
+ [ Opcodes.i32_store8, 0, ...unsignedLEB128(newPointer + ValtypeSize.i32) ],
543
+
544
+ // return new string (pointer)
545
+ ...number(newPointer)
546
+ ];
547
+ },
548
+
549
+ // todo: out of bounds properly
550
+ charAt: (pointer, length, wIndex, _1, _2, arrayShell) => {
551
+ const [ newOut, newPointer ] = arrayShell(1, 'i16');
552
+
553
+ return [
554
+ // setup new/out array
555
+ ...newOut,
556
+ [ Opcodes.drop ],
557
+
558
+ ...number(0, Valtype.i32), // base 0 for store later
559
+
560
+ ...wIndex,
561
+
562
+ Opcodes.i32_to,
563
+
564
+ ...pointer,
565
+ [ Opcodes.i32_add ],
566
+
567
+ // load current string ind {arg}
568
+ [ Opcodes.i32_load8_u, 0, ...unsignedLEB128(ValtypeSize.i32) ],
569
+
570
+ // store to new string ind 0
571
+ [ Opcodes.i32_store8, 0, ...unsignedLEB128(newPointer + ValtypeSize.i32) ],
572
+
573
+ // return new string (page)
574
+ ...number(newPointer)
575
+ ];
576
+ },
577
+
578
+ charCodeAt: (pointer, length, wIndex, iTmp) => {
579
+ return [
580
+ ...wIndex,
581
+ Opcodes.i32_to,
582
+
583
+ ...(zeroChecks.charcodeat ? [] : [
584
+ [ Opcodes.local_set, iTmp ],
585
+
586
+ // index < 0
587
+ ...(noUnlikelyChecks ? [] : [
588
+ [ Opcodes.local_get, iTmp ],
589
+ ...number(0, Valtype.i32),
590
+ [ Opcodes.i32_lt_s ],
591
+ ]),
592
+
593
+ // index >= length
594
+ [ Opcodes.local_get, iTmp ],
595
+ ...length.getCachedI32(),
596
+ [ Opcodes.i32_ge_s ],
597
+
598
+ ...(noUnlikelyChecks ? [] : [ [ Opcodes.i32_or ] ]),
599
+ [ Opcodes.if, Blocktype.void ],
600
+ ...number(NaN),
601
+ [ Opcodes.br, 1 ],
602
+ [ Opcodes.end ],
603
+
604
+ [ Opcodes.local_get, iTmp ],
605
+ ]),
606
+
607
+ ...pointer,
608
+ [ Opcodes.i32_add ],
609
+
610
+ // load current string ind {arg}
611
+ [ Opcodes.i32_load8_u, 0, ...unsignedLEB128(ValtypeSize.i32) ],
612
+ Opcodes.i32_from_u
613
+ ];
614
+ },
615
+
616
+ isWellFormed: () => {
617
+ return [
618
+ // we know it must be true as it is a bytestring lol
619
+ ...number(1)
620
+ ]
621
+ }
622
+ };
623
+
624
+ this[TYPES._bytestring].at.local = Valtype.i32;
625
+ this[TYPES._bytestring].at.returnType = TYPES._bytestring;
626
+ this[TYPES._bytestring].charAt.returnType = TYPES._bytestring;
627
+ this[TYPES._bytestring].charCodeAt.local = Valtype.i32;
628
+ this[TYPES._bytestring].charCodeAt.noPointerCache = zeroChecks.charcodeat;
629
+
630
+ this[TYPES._bytestring].isWellFormed.local = Valtype.i32;
631
+ this[TYPES._bytestring].isWellFormed.local2 = Valtype.i32;
632
+ this[TYPES._bytestring].isWellFormed.returnType = TYPES.boolean;
633
+ }
479
634
  };
@@ -150,7 +150,7 @@ export default (funcs, globals, tags, pages, data, flags) => {
150
150
 
151
151
  if (typeCount !== 0) localDecl.push(encodeLocal(typeCount, lastType));
152
152
 
153
- return encodeVector([ ...encodeVector(localDecl), ...x.wasm.flat().filter(x => x <= 0xff), Opcodes.end ]);
153
+ return encodeVector([ ...encodeVector(localDecl), ...x.wasm.flat().filter(x => x != null && x <= 0xff), Opcodes.end ]);
154
154
  }))
155
155
  );
156
156
 
@@ -32,8 +32,6 @@ export const Opcodes = {
32
32
  throw: 0x08,
33
33
  rethrow: 0x09,
34
34
 
35
- return: 0x0F,
36
-
37
35
  call: 0x10,
38
36
  call_indirect: 0x11,
39
37
  return_call: 0x12,
@@ -42,7 +40,10 @@ export const Opcodes = {
42
40
  end: 0x0b,
43
41
  br: 0x0c,
44
42
  br_if: 0x0d,
43
+ br_table: 0x0e,
44
+ return: 0x0f,
45
45
  call: 0x10,
46
+
46
47
  drop: 0x1a,
47
48
 
48
49
  local_get: 0x20,
@@ -56,9 +57,12 @@ export const Opcodes = {
56
57
  i64_load: 0x29,
57
58
  f64_load: 0x2b,
58
59
 
60
+ i32_load8_s: 0x2c,
61
+ i32_load8_u: 0x2d,
59
62
  i32_load16_s: 0x2e,
60
63
  i32_load16_u: 0x2f,
61
64
 
65
+ i32_store8: 0x3a,
62
66
  i32_store16: 0x3b,
63
67
 
64
68
  i32_store: 0x36,
package/compiler/wrap.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import compile from './index.js';
2
2
  import decompile from './decompile.js';
3
+ import { encodeVector, encodeLocal } from './encoding.js';
3
4
  // import fs from 'node:fs';
4
5
 
5
6
  const bold = x => `\u001b[1m${x}\u001b[0m`;
@@ -18,7 +19,8 @@ const TYPES = {
18
19
 
19
20
  // internal
20
21
  [internalTypeBase]: '_array',
21
- [internalTypeBase + 1]: '_regexp'
22
+ [internalTypeBase + 1]: '_regexp',
23
+ [internalTypeBase + 2]: '_bytestring'
22
24
  };
23
25
 
24
26
  export default async (source, flags = [ 'module' ], customImports = {}, print = str => process.stdout.write(str)) => {
@@ -35,14 +37,95 @@ export default async (source, flags = [ 'module' ], customImports = {}, print =
35
37
  if (flags.includes('info')) console.log(bold(`compiled in ${times[0].toFixed(2)}ms`));
36
38
 
37
39
  const t2 = performance.now();
38
- const { instance } = await WebAssembly.instantiate(wasm, {
39
- '': {
40
- p: valtype === 'i64' ? i => print(Number(i).toString()) : i => print(i.toString()),
41
- c: valtype === 'i64' ? i => print(String.fromCharCode(Number(i))) : i => print(String.fromCharCode(i)),
42
- t: _ => performance.now(),
43
- ...customImports
40
+
41
+ let instance;
42
+ try {
43
+ 0, { instance } = await WebAssembly.instantiate(wasm, {
44
+ '': {
45
+ p: valtype === 'i64' ? i => print(Number(i).toString()) : i => print(i.toString()),
46
+ c: valtype === 'i64' ? i => print(String.fromCharCode(Number(i))) : i => print(String.fromCharCode(i)),
47
+ t: _ => performance.now(),
48
+ ...customImports
49
+ }
50
+ });
51
+ } catch (e) {
52
+ const funcInd = parseInt(e.message.match(/function #([0-9]+) /)[1]);
53
+ const blobOffset = parseInt(e.message.split('@')[1]);
54
+
55
+ // convert blob offset -> function wasm offset.
56
+ // this is not good code and is somewhat duplicated
57
+ // I just want it to work for debugging, I don't care about perf/yes
58
+
59
+ const func = funcs.find(x => x.index === funcInd);
60
+ const locals = Object.values(func.locals).sort((a, b) => a.idx - b.idx).slice(func.params.length).sort((a, b) => a.idx - b.idx);
61
+
62
+ let localDecl = [], typeCount = 0, lastType;
63
+ for (let i = 0; i < locals.length; i++) {
64
+ const local = locals[i];
65
+ if (i !== 0 && local.type !== lastType) {
66
+ localDecl.push(encodeLocal(typeCount, lastType));
67
+ typeCount = 0;
68
+ }
69
+
70
+ typeCount++;
71
+ lastType = local.type;
72
+ }
73
+
74
+ if (typeCount !== 0) localDecl.push(encodeLocal(typeCount, lastType));
75
+
76
+ const toFind = encodeVector(localDecl).concat(func.wasm.flat().filter(x => x != null && x <= 0xff).slice(0, 40));
77
+
78
+ let i = 0;
79
+ for (; i < wasm.length; i++) {
80
+ let mismatch = false;
81
+ for (let j = 0; j < toFind.length; j++) {
82
+ if (wasm[i + j] !== toFind[j]) {
83
+ mismatch = true;
84
+ break;
85
+ }
86
+ }
87
+
88
+ if (!mismatch) break;
89
+ }
90
+
91
+ if (i === wasm.length) throw e;
92
+
93
+ const offset = (blobOffset - i) + encodeVector(localDecl).length;
94
+
95
+ let cumLen = 0;
96
+ i = 0;
97
+ for (; i < func.wasm.length; i++) {
98
+ cumLen += func.wasm[i].filter(x => x != null && x <= 0xff).length;
99
+ if (cumLen === offset) break;
100
+ }
101
+
102
+ if (cumLen !== offset) throw e;
103
+
104
+ i -= 1;
105
+
106
+ console.log(`\x1B[35m\x1B[1mporffor backtrace\u001b[0m`);
107
+
108
+ console.log('\x1B[4m' + func.name + '\x1B[0m');
109
+
110
+ const surrounding = 6;
111
+
112
+ const decomp = decompile(func.wasm.slice(i - surrounding, i + surrounding + 1), '', 0, func.locals, func.params, func.returns, funcs, globals, exceptions).slice(0, -1).split('\n');
113
+
114
+ const noAnsi = s => s.replace(/\u001b\[[0-9]+m/g, '');
115
+ let longest = 0;
116
+ for (let j = 0; j < decomp.length; j++) {
117
+ longest = Math.max(longest, noAnsi(decomp[j]).length);
44
118
  }
45
- });
119
+
120
+ const middle = Math.floor(decomp.length / 2);
121
+ decomp[middle] = `\x1B[47m\x1B[30m${noAnsi(decomp[middle])}${'\u00a0'.repeat(longest - noAnsi(decomp[middle]).length)}\x1B[0m`;
122
+
123
+ console.log('\x1B[90m...\x1B[0m');
124
+ console.log(decomp.join('\n'));
125
+ console.log('\x1B[90m...\x1B[0m\n');
126
+
127
+ throw e;
128
+ }
46
129
 
47
130
  times.push(performance.now() - t2);
48
131
  if (flags.includes('info')) console.log(`instantiated in ${times[1].toFixed(2)}ms`);
@@ -95,6 +178,13 @@ export default async (source, flags = [ 'module' ], customImports = {}, print =
95
178
  return Array.from(new Uint16Array(memory.buffer, pointer + 4, length)).map(x => String.fromCharCode(x)).join('');
96
179
  }
97
180
 
181
+ case '_bytestring': {
182
+ const pointer = ret;
183
+ const length = new Int32Array(memory.buffer, pointer, 1);
184
+
185
+ return Array.from(new Uint8Array(memory.buffer, pointer + 4, length)).map(x => String.fromCharCode(x)).join('');
186
+ }
187
+
98
188
  case 'function': {
99
189
  // wasm func index, including all imports
100
190
  const func = funcs.find(x => (x.originalIndex ?? x.index) === ret);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "porffor",
3
3
  "description": "a basic experimental wip aot optimizing js -> wasm engine/compiler/runtime in js",
4
- "version": "0.2.0-a759814",
4
+ "version": "0.2.0-c597461",
5
5
  "author": "CanadaHonk",
6
6
  "license": "MIT",
7
7
  "dependencies": {