|
| 1 | +# The JavaScript runtime is embedded, not invoked |
| 2 | + |
| 3 | +Status: accepted |
| 4 | + |
| 5 | +## Context |
| 6 | + |
| 7 | +Plans are authored as pure TypeScript and must be evaluated to produce a stored |
| 8 | +version. Evaluation can either shell out to an external runtime or run in |
| 9 | +process against an embedded engine. |
| 10 | + |
| 11 | +Two properties make this more than a packaging preference. Identity is a hash of |
| 12 | +the evaluated result, so **evaluation must be deterministic** or the same source |
| 13 | +yields different versions. And a plan is a program, so **evaluation must not |
| 14 | +grant the program any capability** — a plan has no business reading a file, |
| 15 | +opening a socket, or asking the time. |
| 16 | + |
| 17 | +## Evidence and Argument |
| 18 | + |
| 19 | +Both requirements point the same way, and the second decides it. |
| 20 | + |
| 21 | +An external runtime hands the program a complete platform. Purity can only be |
| 22 | +*policed* — globals stubbed before the program loads, a boundary that is a speed |
| 23 | +bump rather than a wall, since anything the runtime provides may be reachable by |
| 24 | +a path the stubbing did not anticipate. An embedded engine starts with nothing |
| 25 | +and receives only what the host names. That is a difference in kind: the |
| 26 | +capability is absent rather than removed. |
| 27 | + |
| 28 | +Measurement made the packaging side one-sided too. Two independent prototypes |
| 29 | +built the full pipeline. The embedded path costs roughly three and a half |
| 30 | +megabytes of binary and half a minute of build, links only the C library, and |
| 31 | +adds **no marginal closure at all**. An external runtime costs an order of |
| 32 | +magnitude more, delivered as a separate artifact that must be present, found, |
| 33 | +and version-matched at runtime. |
| 34 | + |
| 35 | +A full-featured engine was ruled out on a sharper ground than size: its build |
| 36 | +requires network access on every path — either downloading a prebuilt archive or |
| 37 | +downloading its own toolchain. A hermetic build must therefore pin fetched |
| 38 | +artifacts by hash, which turns every engine upgrade into a fixed-output repair. |
| 39 | +That cost recurs forever and is paid in the most tedious currency available. |
| 40 | + |
| 41 | +A pure-native-language engine avoids a C dependency but costs more than twice |
| 42 | +the binary for a less proven implementation. The C dependency is a small, fast, |
| 43 | +network-free build; that is not the constraint worth optimizing. |
| 44 | + |
| 45 | +Determinism was measured rather than assumed: repeated evaluation of one input |
| 46 | +produced a single distinct result across hundreds of runs, with a negative |
| 47 | +control confirming the check detects change, and results were byte-identical |
| 48 | +across three engine versions spanning a wide range. |
| 49 | + |
| 50 | +## Options |
| 51 | + |
| 52 | +| Option | Tradeoffs | |
| 53 | +| --- | --- | |
| 54 | +| Embed a small interpreter | No external dependency, no marginal closure, capabilities absent by construction; adds a C build dependency and a 0.x transformer | |
| 55 | +| Invoke an external runtime | Familiar and no engine to embed; an order of magnitude more closure, a process boundary, and purity can only be policed | |
| 56 | +| Embed a full optimizing engine | Best-proven determinism for floating point; network-dependent builds that make every upgrade a fixed-output repair, for an order of magnitude more size | |
| 57 | +| Embed a pure-native-language engine | No C dependency at all; more than twice the binary, less proven, for a constraint that was not binding | |
| 58 | + |
| 59 | +## Decision |
| 60 | + |
| 61 | +The engine is embedded in the binary. Compass invokes no external runtime and |
| 62 | +requires none to be installed. |
| 63 | + |
| 64 | +Evaluation happens against a global object the host **constructs explicitly**. |
| 65 | +Nothing is available unless it was deliberately added. This is an allowlist |
| 66 | +rather than a denylist, because a denylist widens silently whenever the engine |
| 67 | +gains a global. |
| 68 | + |
| 69 | +Excluded, and each for a stated reason: the clock and anything derived from it; |
| 70 | +randomness; locale-aware behaviour, whose data is versioned independently of the |
| 71 | +engine and changes underneath a stable engine version; anything that makes |
| 72 | +garbage collection observable; and the transcendental floating-point functions, |
| 73 | +which the engine delegates to the platform library and which therefore disagree |
| 74 | +between platforms in the final bits. |
| 75 | + |
| 76 | +Compilation and evaluation use **separate contexts**. Compiling a module |
| 77 | +requires an evaluation capability that the sandbox must not retain, so a module |
| 78 | +is compiled in a disposable context and the compiled form is loaded into a |
| 79 | +locked one. Dynamic code construction consequently exists but is inert. |
| 80 | + |
| 81 | +**Identity is the hash of the evaluated result, never of transpiled source.** The |
| 82 | +transformer is young and changes often; hashing its output would make every |
| 83 | +upgrade an identity migration. Hashing the evaluated value absorbs that entirely, |
| 84 | +so long as semantics are preserved. |
| 85 | + |
| 86 | +## Consequences |
| 87 | + |
| 88 | +- Compass remains a single binary with no runtime prerequisite. |
| 89 | +- A plan cannot read a file, open a socket, or observe the time, because those |
| 90 | + capabilities are never introduced rather than being taken away. |
| 91 | +- One exception is honest: the randomness primitive arrives with the engine's |
| 92 | + base objects and cannot be structurally omitted. It is removed explicitly, and |
| 93 | + it is the only capability whose absence depends on the host remembering. |
| 94 | +- Excluding the transcendental functions is what makes the same source produce |
| 95 | + the same version on every platform. Without it, identity is platform-local. |
| 96 | +- The transformer is a young dependency that changes frequently. Hashing |
| 97 | + evaluated values rather than emitted source is what makes that acceptable, and |
| 98 | + it must not be quietly reversed for convenience. |
| 99 | +- Determinism is a property to be tested, not assumed. It rests on an explicit |
| 100 | + allowlist, and any addition to that list is a change to this decision. |
0 commit comments