porffor 0.25.1 → 0.25.3

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/CONTRIBUTING.md CHANGED
@@ -23,7 +23,6 @@ The repo comes with easy alias scripts for Unix and Windows, which you can use l
23
23
 
24
24
  You can also swap out `node` in the alias to use another runtime like Deno (`deno run -A ...`) or Bun (`bun ...`), or just use it yourself (eg `node runner/index.js ...`, `bun runner/index.js ...`). Node, Deno, Bun should work.
25
25
 
26
-
27
26
  ### Precompile
28
27
 
29
28
  **If you update any file inside `compiler/builtins` you will need to do this for it to update inside Porffor otherwise your changes will have no effect.** Run `./porf precompile` to precompile. It may error during this, if so, you might have an error in your code or there could be a compiler error with Porffor (feel free to ask for help as soon as you encounter any errors with it).
@@ -98,7 +97,7 @@ Loads the character code at the pointer `pointer` **for a String**.[^1]
98
97
  Porffor.wasm.i32.store(pointer, length, 0, 0)
99
98
  ```
100
99
 
101
- Stores the length `length` at pointer `pointer`, setting the length of an object. This is mostly unneeded today as you can just do `obj.length = length`. (The `0, 4` args are necessary for the Wasm instruction, but you don't need to worry about them (`0` alignment, `0` byte offset).
100
+ Stores the length `length` at pointer `pointer`, setting the length of an object. This is mostly unneeded today as you can just do `obj.length = length`. [^1]
102
101
 
103
102
  <br>
104
103
 
@@ -200,13 +199,147 @@ Store the character code into the `out` pointer variable, and increment it.
200
199
 
201
200
  - For declaring variables, you must use explicit type annotations currently (eg `let a: number = 1`, not `let a = 1`).
202
201
  - You might spot `Porffor.fastOr`/`Porffor.fastAnd`, these are non-short circuiting versions of `||`/`&&`, taking any number of conditions as arguments. You shouldn't don't need to use or worry about these.
203
- - **There are ~no objects, you cannot use them.**
204
- - Attempt to avoid string/array-heavy code and use more variables instead if possible, easier on memory and CPU/perf.
202
+ - Attempt to avoid object/string/array-heavy code and use more variables instead if possible, easier on memory and CPU/perf.
205
203
  - Do not set a return type for prototype methods, it can cause errors/unexpected results.
206
204
  - You cannot use other functions in the file not exported, or variables not inside the current function.
207
205
  - `if (...)` uses a fast truthy implementation which is not spec-compliant as most conditions should be strictly checked. To use spec-compliant behavior, use `if (Boolean(...))`.
208
206
  - For object (string/array/etc) literals, you must use a variable eg `const out: bytestring = 'foobar'; console.log(out);` instead of `console.log('foobar')` due to precompile's allocator constraints.
209
- - Generally prefer/use non-strict equality ops (`==`/`!=`).
207
+ - You should generally use non-strict equality ops (`==`/`!=`).
208
+
209
+ <br>
210
+
211
+ ### Porffor.wasm
212
+ This is a macro that is essentially equivalent to C's `asm` macro. It allows you to write inline Wasm bytecode in a similar format to [WAT](https://developer.mozilla.org/en-US/docs/WebAssembly/Understanding_the_text_format).
213
+
214
+ Let's look at an example to better illustrate how the format works.
215
+
216
+ ```ts
217
+ export const add_i32 = (a: any, b: any) => {
218
+ Porffor.wasm`
219
+ local aCasted i32
220
+ local bCasted i32
221
+ returns i32 i32
222
+
223
+ ;; if both types are number
224
+ local.get ${a+1}
225
+ i32.const 1
226
+ i32.eq
227
+ local.get ${b+1}
228
+ i32.const 1
229
+ i32.eq
230
+ i32.and
231
+ if
232
+ local.get ${a}
233
+ i32.from
234
+ local.set aCasted
235
+
236
+ local.get ${b}
237
+ i32.from
238
+ local.set bCasted
239
+
240
+ local.get aCasted
241
+ local.get bCasted
242
+ i32.add
243
+ i32.const 1
244
+ return
245
+ end
246
+
247
+ ;; return (0, 0) otherwise
248
+ i32.const 0
249
+ i32.const 0
250
+ return`;
251
+ }
252
+ ```
253
+
254
+ ---
255
+
256
+ ```
257
+ local aCasted i32
258
+ local bCasted i32
259
+ ```
260
+
261
+ Here we define two locals, which you can think of as typed variables. Here both of them have the type of `i32`, which was explained above. This type can also be `f64` or `i64`, which are doubles and 64-bit integers respectively.
262
+
263
+ ---
264
+
265
+ ```
266
+ returns i32 i32
267
+ ```
268
+
269
+ This sets the return type of the function, what the stack must look like before a `return` instruction. Normally Porffor functions have the return type `(f64, i32)`, which represents the valtype (usually f64) and an i32 type.
270
+
271
+ > [!WARNING]
272
+ > This is something you have to be incredibly careful with, as Porffor expects most functions to return `(valtype, i32)`. Be incredibly careful when using this.
273
+
274
+ ---
275
+
276
+ ```
277
+ ;; if both types are number
278
+ ```
279
+
280
+ This is a comment. `;;` is Wasm's `//`.
281
+
282
+ ---
283
+
284
+ ```
285
+ local.get ${a+1}
286
+ i32.const 1
287
+ i32.eq
288
+ local.get ${b+1}
289
+ i32.const 1
290
+ i32.eq
291
+ i32.and
292
+ ```
293
+
294
+ This part is a little more complicated, first you have to understand how Wasm represents function parameters and local variables in general. When looking at the decompiled output of something like `let a = 1;`, you'll likely see something like this:
295
+ ```
296
+ f64.const 1
297
+ i32.const 1
298
+ local.set 1 ;; a#type (i32)
299
+ local.set 0 ;; a
300
+ ```
301
+ Here the `i32.const 1` is equivalent to `TYPES.number`, which aligns with what we told Porffor to do, but what's up with the `local.set`s to a number? Well, internally locals are represented with indexes, and in this example `a` was assigned 0, and `a#type` was assigned 1.
302
+
303
+ That's where `local.get ${a+1}` comes from, it's Porffor's way of saying "get the local variable at index of `a` plus one". In most cases, this is the variable's type. The rest of the snippet is just checking if both of the parameters' types are equal to `TYPES.number`.
304
+
305
+ ---
306
+
307
+ ```
308
+ if
309
+ local.get ${a}
310
+ i32.from
311
+ local.set aCasted
312
+
313
+ local.get ${b}
314
+ i32.from
315
+ local.set bCasted
316
+ ```
317
+
318
+ Here we start an if block, equivalent to JS's `if (...) {}`, and as the locals' names imply, cast them to `i32`s. There is one strange thing about this section though, if you look at Wasm's list of instructions you won't find a `i32.from`. This is because Porffor has custom instructions for converting to and from the valtype. In this case, converting the valtype into an `i32`. There are a few more of these instructions, but in general these instructions come in the format of `type.from` (create `type` from valtype) and `type.to` (create valtype from `type`). You can find a full list at the bottom of `codegen.js`.
319
+
320
+ ---
321
+
322
+ ```
323
+ local.get aCasted
324
+ local.get bCasted
325
+ i32.add
326
+ i32.const 1
327
+ return
328
+ end
329
+ ```
330
+
331
+ Here, we get our two casted locals and add them together, returning the result and a `i32` with the value of 1. We then end the if block with the `end` instruction.
332
+
333
+ ---
334
+
335
+ ```
336
+ ;; return (0, 0) otherwise
337
+ i32.const 0
338
+ i32.const 0
339
+ return
340
+ ```
341
+
342
+ Finally, we return `(0, 0)` if either type is not a number. This example was very contrived, but should give you a good sense of how to use `Porffor.wasm`.
210
343
 
211
344
  <br>
212
345
 
@@ -259,4 +392,9 @@ It will also log new passes/fails. Be careful as sometimes the overall passes ca
259
392
 
260
393
  <br>
261
394
 
262
- [^1]: The `0, 4` args are necessary for the Wasm instruction, but you don't need to worry about them (`0` alignment, `4` byte offset for length).
395
+ ### Resources
396
+
397
+ - [MDN](https://developer.mozilla.org/en-US/), not only a great resource for learning JS, but also for implementing it, as it has high level descriptions of functionality, as well as links to the relevant portions of the spec that govern the feature.
398
+ - [WebAssembly Opcodes](https://pengowray.github.io/wasm-ops/), this website not only describes what each wasm instruction does but the necessary stack needed, and contains some other useful resources as well.
399
+
400
+ [^1]: The last two args are necessary for the Wasm instruction, but you don't need to worry about them (the first is alignment, the second is byte offset).
package/README.md CHANGED
@@ -68,8 +68,8 @@ Expect nothing to work! Only very limited JS is currently supported. See files i
68
68
  - `-O3` to enable advanceder opt (precompute const math). unstable!
69
69
 
70
70
  ## Limitations
71
- - No full object support yet
72
71
  - Little built-ins/prototype
72
+ - No object prototypes yet
73
73
  - No async/promise/await
74
74
  - No variables between scopes (except args and globals)
75
75
  - No `eval()` etc (since it is AOT)
@@ -103,7 +103,7 @@ These include some early (stage 1/0) and/or dead (last commit years ago) proposa
103
103
  - Declaring functions
104
104
  - Calling functions
105
105
  - `return`
106
- - `let`/`const`/`var` basic declarations
106
+ - Basic declarations (`let`/`const`/`var`)
107
107
  - Some basic integer operators (`+-/*%`)
108
108
  - Some basic integer bitwise operators (`&|`)
109
109
  - Equality operators (`==`, `!=`, etc)
@@ -111,18 +111,18 @@ These include some early (stage 1/0) and/or dead (last commit years ago) proposa
111
111
  - Some unary operators (`!`, `+`, `-`)
112
112
  - Logical operators (`&&`, `||`)
113
113
  - Declaring multiple variables in one (`let a, b = 0`)
114
+ - Array destructuring (`let [a, ...b] = foo`)
114
115
  - Global variables (`var`/none in top scope)
115
- - Functions returning 1 number
116
- - Bool literals as ints (not real type)
116
+ - Booleans
117
117
  - `if` and `if ... else`
118
118
  - Anonymous functions
119
119
  - Setting functions using vars (`const foo = function() { ... }`)
120
120
  - Arrow functions
121
- - `undefined`/`null` as ints (hack)
121
+ - `undefined`/`null`
122
122
  - Update expressions (`a++`, `++b`, `c--`, etc)
123
123
  - `for` loops (`for (let i = 0; i < N; i++)`, etc)
124
- - *Basic* objects (hack)
125
- - `console.log` (hack)
124
+ - Basic objects (no prototypes)
125
+ - `console.log`
126
126
  - `while` loops
127
127
  - `break` and `continue`
128
128
  - Named export funcs
@@ -131,7 +131,7 @@ These include some early (stage 1/0) and/or dead (last commit years ago) proposa
131
131
  - Conditional/ternary operator (`cond ? a : b`)
132
132
  - Recursive functions
133
133
  - Bare returns (`return`)
134
- - `throw` (literals only)
134
+ - `throw` (literals only, hack for `new Error`)
135
135
  - Basic `try { ... } catch { ... }` (no error given)
136
136
  - Calling functions with non-matching arguments (eg `f(a, b); f(0); f(1, 2, 3);`)
137
137
  - `typeof`
@@ -145,11 +145,15 @@ These include some early (stage 1/0) and/or dead (last commit years ago) proposa
145
145
  - String comparison (eg `'a' == 'a'`, `'a' != 'b'`)
146
146
  - Nullish coalescing operator (`??`)
147
147
  - `for...of` (arrays and strings)
148
+ - `for...in`
148
149
  - Array member setting (`arr[0] = 2`, `arr[0] += 2`, etc)
149
150
  - Array constructor (`Array(5)`, `new Array(1, 2, 3)`)
150
151
  - Labelled statements (`foo: while (...)`)
151
152
  - `do...while` loops
152
153
  - Optional parameters (`(foo = 'bar') => { ... }`)
154
+ - Rest parameters (`(...foo) => { ... }`)
155
+ - `this`
156
+ - Constructors (`new Foo`)
153
157
 
154
158
  ### Built-ins
155
159
 
@@ -223,16 +227,27 @@ Porffor can run Test262 via some hacks/transforms which remove unsupported featu
223
227
 
224
228
  ## Codebase
225
229
  - `compiler`: contains the compiler itself
226
- - `builtins.js`: all built-ins of the engine (spec, custom. vars, funcs)
230
+ - `2c.js`: porffor's custom wasm-to-c engine
231
+ - `allocators.js`: static and dynamic allocators to power various language features
232
+ - `assemble.js`: assembles wasm ops and metadata into a wasm module/file
233
+ - `builtins.js`: all manually written built-ins of the engine (spec, custom. vars, funcs)
234
+ - `builtins_object.js`: all the various built-in objects (think `String`, `globalThis`, etc.)
235
+ - `builtins_precompiled.js`: dynamically generated builtins from the `builtins/` folder
227
236
  - `codegen.js`: code (wasm) generation, ast -> wasm. The bulk of the effort
237
+ - `cyclone.js`: wasm partial constant evaluator (it is fast and dangerous hence "cyclone")
228
238
  - `decompile.js`: basic wasm decompiler for debug info
229
239
  - `embedding.js`: utils for embedding consts
230
240
  - `encoding.js`: utils for encoding things as bytes as wasm expects
231
241
  - `expression.js`: mapping most operators to an opcode (advanced are as built-ins eg `f64_%`)
242
+ - `havoc.js`: wasm rewrite library (it wreaks havoc upon wasm bytecode hence "havoc")
232
243
  - `index.js`: doing all the compiler steps, takes code in, wasm out
233
244
  - `opt.js`: self-made wasm bytecode optimizer
234
245
  - `parse.js`: parser simply wrapping acorn
235
- - `assemble.js`: assembles wasm ops and metadata into a wasm module/file
246
+ - `pgo.js`: a profile guided optimizer
247
+ - `precompile.js`: the tool to generate `builtins_precompied.js`
248
+ - `prefs.js`: a utility to read command line arguments
249
+ - `prototype.js`: some builtin prototype functions
250
+ - `types.js`: definitions for each of the builtin types
236
251
  - `wasmSpec.js`: "enums"/info from wasm spec
237
252
  - `wrap.js`: wrapper for compiler which instantiates and produces nice exports
238
253
 
@@ -249,16 +264,16 @@ Porffor can run Test262 via some hacks/transforms which remove unsupported featu
249
264
  - `test262`: test262 runner and utils
250
265
 
251
266
  ## Usecases
252
- Basically none right now (other than giving people headaches). Potential ideas:
267
+ Currently, Porffor is seriously limited in features and functionality, however it has some key benefits:
253
268
  - 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.)
254
269
  - Compiling JS to native binaries. This is still very early!
270
+ - Inline Wasm for when you want to beat the compiler in performance, or just want fine grained functionality.
271
+ - Potential for SIMD operations and other lower level concepts.
255
272
  - More in future probably?
256
273
 
257
274
  ## Todo
258
275
  No particular order and no guarentees, just what could happen soon™
259
276
 
260
- - Objects
261
- - Basic object expressions (eg `{}`, `{ a: 0 }`)
262
277
  - Asur
263
278
  - Support memory
264
279
  - Support exceptions
@@ -272,8 +287,6 @@ No particular order and no guarentees, just what could happen soon™
272
287
  - Runtime
273
288
  - WASI target
274
289
  - Run precompiled Wasm file if given
275
- - Docs
276
- - Update codebase readme section
277
290
  - Cool proposals
278
291
  - [Optional Chaining Assignment](https://github.com/tc39/proposal-optional-chaining-assignment)
279
292
  - [Modulus and Additional Integer Math](https://github.com/tc39/proposal-integer-and-modulus-math)
@@ -303,7 +316,6 @@ Porffor intentionally does not use Wasm proposals which are not commonly impleme
303
316
  - Exception handling (optional, only for errors)
304
317
  - Tail calls (opt-in, off by default)
305
318
 
306
-
307
319
  ## FAQ
308
320
 
309
321
  ### 1. Why the name?
@@ -190,7 +190,7 @@ export const BuiltinFuncs = function() {
190
190
  ...number(TYPES.number, Valtype.i32),
191
191
  [ Opcodes.local_get, 1 ],
192
192
  ...number(TYPES.number, Valtype.i32),
193
- [ Opcodes.call, builtin('__Math_pow') ],
193
+ [ Opcodes.call, ...builtin('__Math_pow') ],
194
194
  [ Opcodes.drop ],
195
195
  ]
196
196
  };
@@ -1,4 +1,4 @@
1
- import { Opcodes, PageSize, Valtype } from './wasmSpec.js';
1
+ import { Blocktype, Opcodes, PageSize, Valtype } from './wasmSpec.js';
2
2
  import { TYPES } from './types.js';
3
3
  import { number } from './embedding.js';
4
4
 
@@ -6,64 +6,92 @@ export default function({ builtinFuncs }, Prefs) {
6
6
  const done = new Set();
7
7
  const object = (name, props) => {
8
8
  done.add(name);
9
+ const prefix = name === 'globalThis' ? '' : `__${name}_`;
10
+
11
+ builtinFuncs['#get_' + name] = {
12
+ params: [],
13
+ locals: [],
14
+ globals: [ Valtype.i32 ],
15
+ globalNames: [ '#getptr_' + name ],
16
+ returns: [ Valtype.i32 ],
17
+ returnType: TYPES.object,
18
+ wasm: (scope, { allocPage, makeString, generate, getNodeType, builtin }) => {
19
+ if (globalThis.precompile) return [ [ 'get object', name ] ];
20
+
21
+ // todo/perf: precompute bytes here instead of calling real funcs if we really care about perf later
22
+
23
+ const page = allocPage(scope, `builtin object: ${name}`);
24
+ const ptr = page === 0 ? 4 : page * PageSize;
25
+
26
+ const out = [
27
+ // check if already made/cached
28
+ [ Opcodes.global_get, 0 ],
29
+ [ Opcodes.if, Blocktype.void ],
30
+ [ Opcodes.global_get, 0 ],
31
+ [ Opcodes.return ],
32
+ [ Opcodes.end ],
33
+
34
+ // set cache & ptr for use
35
+ ...number(ptr, Valtype.i32),
36
+ [ Opcodes.global_set, 0 ],
37
+ ];
9
38
 
10
- let cached;
11
- this[name] = (scope, { allocPage, makeString, generateIdent, getNodeType, builtin }) => {
12
- if (cached) {
13
- return number(cached);
14
- }
15
-
16
- // todo: precompute bytes here instead of calling real funcs if we really care about perf later
39
+ for (const x in props) {
40
+ let value = {
41
+ type: 'Identifier',
42
+ name: prefix + x
43
+ };
17
44
 
18
- const page = allocPage(scope, `builtin object: ${name}`);
19
- const ptr = page === 0 ? 4 : page * PageSize;
20
- cached = ptr;
45
+ let flags = 0b0000;
21
46
 
22
- const out = [];
47
+ const d = props[x];
48
+ if (d.configurable) flags |= 0b0010;
49
+ if (d.enumerable) flags |= 0b0100;
50
+ if (d.writable) flags |= 0b1000;
23
51
 
24
- for (const x in props) {
25
- const value = {
26
- type: 'Identifier',
27
- name: '__' + name + '_' + x
28
- };
52
+ // hack: do not generate objects inside of objects as it causes issues atm
53
+ if (this[prefix + x]?.type === TYPES.object) value = { type: 'ObjectExpression', properties: [] };
29
54
 
30
- let flags = 0b0000;
55
+ out.push(
56
+ [ Opcodes.global_get, 0 ],
57
+ ...number(TYPES.object, Valtype.i32),
31
58
 
32
- const d = props[x];
33
- if (d.configurable) flags |= 0b0010;
34
- if (d.enumerable) flags |= 0b0100;
35
- if (d.writable) flags |= 0b1000;
36
-
37
- out.push(
38
- ...number(ptr, Valtype.i32),
39
- ...number(TYPES.object, Valtype.i32),
59
+ ...makeString(scope, x, false, `#builtin_object_${name}_${x}`),
60
+ Opcodes.i32_to_u,
61
+ ...number(TYPES.bytestring, Valtype.i32),
40
62
 
41
- ...makeString(scope, x, false, `#builtin_object_${name}_${x}`),
42
- Opcodes.i32_to_u,
43
- ...number(TYPES.bytestring, Valtype.i32),
63
+ ...generate(scope, value),
64
+ ...getNodeType(scope, value),
44
65
 
45
- ...generateIdent(scope, value),
46
- ...getNodeType(scope, value),
66
+ ...number(flags, Valtype.i32),
67
+ ...number(TYPES.number, Valtype.i32),
47
68
 
48
- ...number(flags, Valtype.i32),
49
- ...number(TYPES.number, Valtype.i32),
69
+ [ Opcodes.call, ...builtin('__Porffor_object_define') ],
70
+ [ Opcodes.drop ],
71
+ [ Opcodes.drop ]
72
+ );
73
+ }
50
74
 
51
- [ Opcodes.call, ...builtin('__Porffor_object_define') ],
52
- [ Opcodes.drop ],
53
- [ Opcodes.drop ]
75
+ out.push(
76
+ // return ptr
77
+ [ Opcodes.global_get, 0 ]
54
78
  );
79
+ return out;
55
80
  }
56
-
57
- out.push(...number(ptr));
58
- return out;
59
81
  };
82
+
83
+
84
+ this[name] = (scope, { builtin }) => [
85
+ [ Opcodes.call, ...builtin('#get_' + name) ],
86
+ Opcodes.i32_from_u
87
+ ];
60
88
  this[name].type = TYPES.object;
61
89
 
62
90
  for (const x in props) {
63
91
  const d = props[x];
64
92
 
65
93
  if (d.value) {
66
- const k = '__' + name + '_' + x;
94
+ const k = prefix + x;
67
95
 
68
96
  if (typeof d.value === 'number') {
69
97
  this[k] = number(d.value);
@@ -189,7 +217,42 @@ export default function({ builtinFuncs }, Prefs) {
189
217
  });
190
218
  }
191
219
 
192
- object('globalThis', {})
220
+ const enumerableGlobals = [ 'atob', 'btoa', 'performance', 'crypto', 'navigator' ];
221
+ object('globalThis', {
222
+ // 19.1 Value Properties of the Global Object
223
+ // https://tc39.es/ecma262/#sec-value-properties-of-the-global-object
224
+ // 19.1.1 globalThis
225
+ globalThis: {
226
+ writable: true,
227
+ enumerable: false,
228
+ configurable: true
229
+ },
230
+
231
+ // 19.1.2 Infinity
232
+ // 19.1.3 NaN
233
+ // 19.1.4 undefined
234
+ ...props({
235
+ writable: false,
236
+ enumerable: false,
237
+ configurable: false
238
+ }, [ 'Infinity', 'NaN', 'undefined' ]),
239
+
240
+ // 19.2 Function Properties of the Global Object
241
+ // https://tc39.es/ecma262/#sec-function-properties-of-the-global-object
242
+ // 19.3 Constructor Properties of the Global Object
243
+ // https://tc39.es/ecma262/#sec-constructor-properties-of-the-global-object
244
+ ...props({
245
+ writable: true,
246
+ enumerable: false,
247
+ configurable: true
248
+ }, builtinFuncKeys.filter(x => !x.startsWith('__') && !enumerableGlobals.includes(x) && !x.startsWith('f64') && !x.startsWith('i32'))),
249
+
250
+ ...props({
251
+ writable: true,
252
+ enumerable: true,
253
+ configurable: true
254
+ }, enumerableGlobals)
255
+ });
193
256
 
194
257
  if (Prefs.logMissingObjects) for (const x of Object.keys(builtinFuncs).concat(Object.keys(this))) {
195
258
  if (!x.startsWith('__')) continue;