Decision Log

Chronological record of key architectural and implementation decisions, newest first. Each entry has a date, an area tag, a summary, and links to the PR and/or detailed documentation.

How to add an entry#

**YYYY-MM-DD** · `area` — Summary of the decision and why. [#PR](https://github.com/frostney/GocciaScript/pull/N). [detail link](file.md#anchor).
  • Date — When the decision was made
  • Area — One of: bytecode, interpreter, parser, data-structures, strings, fpc-platform, runtime, testing, build
  • Summary — 1–3 sentences explaining the decision and why
  • PR / Detail — Link to the pull request and/or the docs/ section where the full rationale lives
Entries are immutable. Each entry records what was decided at that point in time. Do not retroactively update entries to match the current implementation. If the implementation changes, add a new entry. Links may be updated if targets are renamed.

2026-05-04 · testing · #513 — test262 conformance harness reframed around the standard tc39 convention. Previously the wrapper ran inside GocciaTestRunner and had to selectively hide / capture / restore the test-library globals (expect, describe, test, runTests, etc.) it registered, with failure capture leaning on an undefined sentinel that collided with thrown undefined and chunked-runner crashes that masked thousands of conformance failures as wrapper failures (#491 history). Replaced with: per-test GocciaScriptLoaderBare subprocess, stock tc39/test262 harness files read directly from the pinned checkout's harness/ directory (with a small set of bundled adaptations under scripts/test262_harness/ for stock files that depend on language features Goccia excludes by design or that work around specific engine bugs — see test262.md § Bundled harness adaptations), exit-code + stdout-marker wire protocol identical to test262-harness/eshost/test262.fyi, and a thin TypeScript orchestrator (scripts/run_test262_suite.ts). No eligibility filter — every discovered test runs; per-test subprocess + --timeout + --max-memory bound the blast radius. Wrapper-template drift is now structurally impossible because the "template" is harness + body string concatenation. Wrapper-infra failures are classified separately and gated to zero in CI. Surfaced eleven engine bugs (all milestoned 0.8.0, all labeled engine per the architecture split where engine covers TGocciaEngine — language semantics + ECMAScript built-ins — and runtime is reserved for TGocciaRuntime host extensions like console/fetch/JSON5): #514 (Iterator.concat SIGSEGV), #515 (RegExp.test SIGSEGV), #516 (Reflect.construct rejects function decls/exprs), #517 (script-mode unattached call this), #518 (bytecode VM Range-check on top-level Promise.then drain), #519 (Error.prototype.constructor missing), #520 (module arrow this lexical inheritance), #521 (var/function shadowing built-in globals), #522 (String(obj) doesn't invoke toString), #523 (yield* accesses .next on null), #524 (for-of re-fetches iterator.next each iteration). Each bundled-harness adaptation under scripts/test262_harness/ references its tracking issue and is to be removed when the underlying engine bug is fixed. test262.md.

2026-05-04 · runtime · #510 — Script loaders are silent-by-default about the script's last evaluated value, mirroring node script.js / bun script.js / deno run script.js / qjs script.js. Printing is opt-in via --print (or "print": true in goccia.json for GocciaScriptLoader), which emits the bare value on its own line including undefined — the same semantics as node -p / bun --print / deno eval -p. Replaces the previous "always print Result: <value> banner" behavior, which was the outlier among mainstream JS runtimes and surfaced as a false-positive vector in substring-graded environments such as test262.fyi (#509). GocciaScriptLoader's human-readable timing banner ("Running script (interpreted): ..." + Lex/Parse/Execute line) is unchanged; only the final value line moved behind the flag. JSON output (--output=json) still always carries the result in the structured result field regardless of --print. build-system.md § Compile and Run.

2026-05-03 · bytecode — Bytecode compilation now has an internal compiler-side constant optimizer for pure primitive folding, primitive const propagation, and dead branch/tail removal. The work deliberately stays format-neutral: no new VM opcodes or .gbc changes, and coverage mode preserves branch shape so missing branches remain reportable. bytecode-vm.md § Compiler Optimizer.

2026-05-02 · runtime · #493 — Engine and runtime entry points are split explicitly. TGocciaEngine owns language execution, core built-ins, source strings/source lists, and Script-vs-Module entry semantics through SourceType; TGocciaRuntime owns host/runtime globals, special-purpose opt-ins such as test assertions/benchmarks/FFI, file-backed convenience helpers, and the default filesystem module content provider. GocciaScriptLoaderBare remains a core-engine-only frontend that can read file/stdin into source text without attaching runtime globals, and the CLI CI suite now covers that contract. architecture.md § Main Layers. embedding.md § Engine API.

2026-04-30 · runtime — Strict-types runtime enforcement is now an explicit --strict-types CLI / "strict-types" config flag (default off) that works in both interpreter and bytecode mode. Previously the bytecode VM implicitly enforced type annotations and the interpreter silently ignored them, with a read-only Goccia.strictTypes JS global advertising the mode. The new model: enforcement is opt-in regardless of execution mode, the bytecode compiler gates OP_CHECK_TYPE emission on the flag, the interpreter records type hints on lexical bindings (TLexicalBinding.TypeHint) and enforces them in AssignBinding / variable declaration / parameter binding via the shared Goccia.Types.Enforcement unit, and the Goccia.strictTypes global is removed in favour of directory-level goccia.json config (mirroring --asi / --compat-var). language.md § Types as Comments.

2026-04-29 · parser — Parser hot paths now avoid repeated cursor-helper and chained Match(...) dispatch in Advance, Check, Match, remaining expression operators, call/member continuations, and primary expressions. Same-corpus production timing with GocciaScriptLoader --output=json showed 35.1-49.1% parser-phase improvements versus clean HEAD, so this supersedes the narrower binary-only parser optimization while keeping recursive descent and avoiding parser unit splits. spikes/parser-hot-dispatch-performance.md.

2026-04-29 · parser — Binary-expression precedence parsing now uses direct Peek.TokenType loops on the measured hot precedence layers instead of routing through the generic open-array ParseBinaryExpression helper. Production timing showed 3.6-18.9% parser-phase improvements across larger synthetic parser maps and 6.3-14.3% improvements across existing benchmark-file parse medians; rejected alternatives included explicit Advance inline, eager source-text removal, and repeated-Check(...) loops. spikes/parser-binary-expression-performance.md.

2026-04-29 · parser — Lexer hot paths favor direct scanning and measured tiny-method inline hints over abstraction in the numeric scanner. Comment skipping, block comments, regex literal slicing, and template line-terminator handling were simplified and benchmarked with GocciaBenchmarkRunner lex timing; the numeric separator helper was rejected because it shortened the code but regressed decimal and radix-heavy cases. spikes/lexer-performance-simplification.md.

2026-04-28 · runtime · #440 — Temporal named time zones use layered providers in this order: GOCCIA_TZDIR, Unix TZif files, Windows ICU, then embedded TZif resource fallback. The embedded IANA payload is generated as Generated.TimeZoneData.res with a small linker unit instead of a large Pascal array, keeping portability while leaving lookup and parsing logic in hand-authored units. Temporal Time Zone Data. Generated Timezone Data.

2026-04-27 · runtime — Generator and async generator support. Generator method shorthand (*method() and async *method()) is now part of the default method syntax, while function* and async function* remain behind --compat-function because they use the compatibility-gated function keyword. The interpreter uses resumable generator continuations and the bytecode format reserves a single OP_YIELD opcode; yield* is represented as delegation logic rather than a second opcode. language.md § Generators.

2026-04-25 · runtime · #403 — Per-engine realm for built-in prototype isolation. TGocciaRealm (Goccia.Realm.pas) owns the mutable intrinsic state — Array.prototype, Object.prototype, Map.prototype, every error prototype, every Temporal prototype, etc. Each TGocciaEngine constructs its own realm and frees it in Destroy, which unpins every prototype the realm owns; the next engine on the same worker thread sees pristine intrinsics. Replaces the unit-level threadvar caches that previously survived engine teardown and the prototypeIsolation.js harness that tried to undo mutations from JS (and could not reverse non-configurable property additions). Two slot kinds: TGocciaRealmSlotId for TGCManagedObject prototypes (pinned in SetSlot, unpinned at tear-down), and TGocciaRealmOwnedSlotId for plain-TObject helpers like TGocciaSharedPrototype (Free-d at tear-down before pin release so destructors can still unpin owned objects). core-patterns.md § Realm Ownership & Slot Registration. embedding.md § Engine Lifecycle & Realm Isolation.

2026-04-24 · runtime — Fetch-only asynchronous I/O. fetch() now returns a pending Promise before network completion and runs blocking HTTP work on fetch-specific background workers. Completions are settled back on the owning runtime thread, then normal Promise reactions drain through the existing microtask queue; the microtask queue remains a Promise-reaction queue, not an I/O queue. await fetch(...) deliberately keeps the current synchronous await model by pumping fetch completions while it waits; a general event loop and true async continuations remain future work. built-ins.md § fetch.

2026-04-23 · parser — Opt-in function declarations (--compat-function). Added function declaration and expression support behind a --compat-function CLI flag for ECMAScript compatibility when porting legacy code. Function declarations desugar to var-scoped bindings backed by TGocciaMethodExpression, which produces call-site this binding (not lexical). Declarations are hoisted per ES spec: both name and value are available before the declaration is reached, using the same DefineVariableBinding infrastructure as --compat-var. Async functions are supported; generators (function*) and arguments remain excluded. No new AST node types — uses IsFunctionDeclaration flag on TGocciaVariableDeclaration. language.md § function Keyword.

2026-04-20 · runtime · #368 — Opt-in var declarations (--compat-var). Added var support behind a --compat-var CLI flag for ECMAScript compatibility when porting legacy code. Var bindings are stored in a separate FVarBindings map on function/module/global scopes (distinct from the lexical binding map used by let/const), matching the ES spec's separation between VariableEnvironment and LexicalEnvironment. This avoids forcing var semantics (function-scoped, redeclarable, no TDZ) through lexical machinery designed around block-scoping, TDZ, and no-redeclaration guarantees. The bytecode compiler uses a parallel DeclareVarLocal approach with depth-0 locals that survive EndScope unwinding. Naming follows spec terminology: DefineLexicalBinding / DefineVariableBinding / GetBinding / AssignBinding. language.md § var Declarations.

2026-04-20 · runtime · #365 — BigInt64Array/BigUint64Array (ES2020). Adds the two BigInt-typed array constructors, completing typed array coverage. Elements are BigInt values; setting non-BigInt throws TypeError. All prototype methods (sort, indexOf, includes, fill, find, map, filter, reduce, etc.) handle BigInt kinds correctly. built-ins-binary-data.md.

2026-04-20 · bytecode · #356 — Call stack depth limit with heap-resident trampoline. Added a configurable call depth limit (--stack-size=N, default 3 500) that throws RangeError when exceeded. The bytecode VM dispatches bytecode-to-bytecode calls iteratively via an explicit frame stack (FFrameStack) on the heap rather than recursing on the Pascal call stack. V8/JSC check the native stack pointer against a guard address and are bounded by the OS thread stack size; this implementation uses a heap-allocated frame array and a counter, which are independent concepts — the depth limit is a runtime safety guard, the trampoline is an implementation detail of call dispatch, and neither is related to JIT compilation. The interpreter mode retains Pascal recursion with the depth check as a guard. bytecode-vm.md. embedding.md § Call Stack Depth Limit.

2026-04-18 · runtime — BigInt primitive type (ES2020). Previously excluded from scope because GocciaScript's use cases did not demand arbitrary-precision integers. Reconsidered because (1) test262 tests for the Temporal API require BigInt as a prerequisite — nanosecond-precision epochs (Temporal.Instant.epochNanoseconds) return BigInt values, and (2) several Temporal internals are simplified with BigInt primitives available rather than workarounds. The implementation is self-contained (TBigInteger record in BigInteger.pas, TGocciaBigIntValue wrapping it) with no impact on existing Number paths. built-ins.md § BigInt. language-tables.md.

2026-04-12 · bytecode · #289 — Source Maps (v3). Source map generation for bytecode compilation and preprocessors (JSX transformer). Maps compiled bytecode instructions back to original source locations for debugging. bytecode-vm.md § Design Rationale.

2026-04-11 · runtime · #276Goccia.build platform metadata. Deno.build-compatible shape exposing os, arch, and target for runtime platform detection. built-ins.md § Global Constants.

2026-04-09 · bytecode · #237 — Bytecode VM profiling. Opcode histograms, pair frequency, function self-time, allocation tracking, and flame graph export via --profile flag. profiling.md.

2026-04-08 · parser · #220 — Opt-in automatic semicolon insertion. Added ASI behind a --asi CLI flag. Previously against the project philosophy (GocciaScript requires explicit semicolons), added for compatibility with code written for standard ECMAScript. Off by default; does not change the language subset, only relaxes the semicolon requirement. language.md § Automatic Semicolon Insertion.

2026-03-23 → 2026-04-10 · bytecode · #107 #109 #110 #136 #137 #254 — VM unification. Fold the separate Souffle VM into a GocciaScript-specific bytecode VM: eliminate interpreter bridges for modules (#107) and async/await (#110), add native-value-backed Map/Set (#109), refactor built-in object model registration (#136), fold the VM directly (#137), and extract the GC into a standalone Goccia.GarbageCollector unit (#254). bytecode-vm.md § Design Rationale. garbage-collector.md.

2026-03-25 · bytecode — Unify heap object hierarchy. TGocciaValue now inherits from TGocciaCompiledHeapObject, removing the bridge between interpreter and bytecode value models. bytecode-vm.md § Design Rationale.

2026-03-18 · data-structures — Custom hash maps over TDictionary. Purpose-built TOrderedStringMap, THashMap, and TOrderedMap replace TDictionary on hot paths with 4–6× faster inserts at typical sizes. #66. spikes/fpc-hashmap-performance.md.

2026-03-11 · strings — TStringBuffer over TStringBuilder. Both TUnicodeStringBuilder and TAnsiStringBuilder trigger a 750× slowdown from FPC's heap manager without preallocation. TStringBuffer (advanced record with doubling growth) is ~2× faster even with preallocation. #65. spikes/fpc-string-performance.md.

2026-03-08 · interpreter — VMT dispatch on AST nodes. Expression and statement evaluation uses virtual dispatch instead of if ... is type check chains. Eliminates TObject.InheritsFrom overhead (18.4% of interpreted instructions in callgrind profiling). #51. core-patterns.md § Virtual Dispatch Value System.

2026-03-05 · runtime — Switch number representation from enum to IEEE 754. Removed TGocciaNumberSpecialValue enum and FSpecialValue field from TGocciaNumberLiteralValue. Numbers now store a single Double using standard IEEE 754 bit patterns for NaN, Infinity, and -0. Enabled by masking all FPU exceptions via SetExceptionMask so that operations like 0.0 / 0.0 produce IEEE 754 NaN instead of raising a Pascal exception. The IsNaN/IsInfinity/IsNegativeZero property accessors delegate to Math.IsNaN/Math.IsInfinite and an endian-neutral sign-bit check. The engine saves and restores the previous exception mask on creation/destruction. #39. value-system.md § Number Representation.

2026-03-08 · interpreterTGocciaControlFlow for break/return. break and return use result records (cfkBreak, cfkReturn) instead of Pascal exceptions, eliminating FPC_SETJMP overhead from the interpreter's hot path. #45. interpreter.md § Error Handling Strategy.

2026-03-05 · bytecode — Feature parity between bytecode and interpreter. The bytecode backend passes the full JavaScript test suite. #39. bytecode-vm.md.

2026-02-20 · strings — No string interning. Dictionary-based string interning was benchmarked at −4% across 172 benchmarks. FPC's COW semantics make allocation effectively free; the hash + lookup cost exceeds it. core-patterns.md § String Interning.

2026-02-16 · interpreter — Eliminate global mutable state. All runtime state flows through TGocciaEvaluationContext, the scope chain, and value objects. The GlobalEvaluationContext mutable global was removed. interpreter.md § Pure Evaluator Functions.

2026-02-16 · interpreterCreateChild factory for scopes. Direct TGocciaScope.Create replaced with ParentScope.CreateChild(kind) to ensure proper parent linkage and callback propagation. interpreter.md § Scope Chain Design.

2026-03-18 · data-structures — TOrderedStringMap for scope bindings. A TScopeMap with linear scan was attempted and abandoned after profiling showed 2.7× slowdown vs hash-based lookup. #66. spikes/fpc-hashmap-performance.md.

2026-03-18 · data-structures — No TFPDataHashTable. Catastrophic insert performance (400,000 ns/insert vs 50 ns for TOrderedStringMap). #66. spikes/fpc-hashmap-performance.md.

2026-03-18 · fpc-platform — Generics have zero runtime cost. Type aliases and multi-level generic inheritance produce byte-identical machine code. #105. spikes/fpc-generics-performance.md.

2026-03-18 · fpc-platform — Virtual dispatch is constant-time. Depth 1 and depth 5 cost the same ~2.5ns. #105. spikes/fpc-dispatch-performance.md.

2026-03-18 · fpc-platform — ObjFPC vs Delphi mode: zero performance impact. Both produce byte-identical machine code. #105. spikes/fpc-dispatch-performance.md.

2026-02-26 · bytecode — Register-based bytecode. The VM uses a register machine because it reduces instruction count and avoids redundant operand shuffling compared with a stack VM. #38. bytecode-vm.md § Why Register-Based.

2026-02-26 · bytecode — Compiler-side desugaring as default. Nullish coalescing, template literals, and object spread compile into existing instruction sequences rather than expanding the opcode surface. #38. bytecode-vm.md § Compiler-Side Desugaring.

2026-02-26 · bytecode — Core vs semantic opcode split follows hotness, not old runtime layering. #38. bytecode-vm.md § Opcode Layout.

2025-06-25 · runtime — Singleton special values. undefined, null, true, false, NaN, Infinity are singletons enabling pointer equality instead of type checks. core-patterns.md § Singleton Special Values.

2025-06-25 · runtime — Number dual representation. Numbers use Double + TGocciaNumberSpecialValue enum to handle NaN, Infinity, -0 correctly. value-system.md § Number Representation.