DEV Community

Cover image for The Port Was the Easy Part: Proving a JavaScript Query-String Codec in Go
Parity Forge
Parity Forge

Posted on

The Port Was the Easy Part: Proving a JavaScript Query-String Codec in Go

I picked ljharb/qs from the official Port Mortem 2026 Track H pool because its size was deceptive. Roughly a thousand lines of query-string code looked manageable in 72 hours. Its observable behavior was not.

A query such as a[0]=x&a[2]=y is not merely text becoming a map. It can encode nested objects, arrays, sparse holes, duplicate keys, prototype-sensitive names, configurable depth, and JavaScript's property-ordering rules. Moving that behavior to Go meant translating a runtime model, not syntax.

The result is qs-go: a standard-library Go module, standalone CLI, and no-install Go/WebAssembly demo. The release path contains no Node.js, subprocess fallback, cgo, or second parser hidden in the demo.

Try it first: live Go/WASM demo · 4:40 evidence video · submission release

Here is the result in one deliberately qualified paragraph. The frozen upstream JavaScript baseline passes 1,045 / 1,045 Tape assertions. Those assertions did not all run directly against Go. The Go equivalence claim is supported separately by focused Go tests, a hash-verified JavaScript oracle, and a final deterministic differential record of 672,321 comparisons in 60 seconds, with zero observed mismatches and zero execution errors inside a declared dense, JSON-compatible scope.

That distinction is the thesis of this project: a large number is useful only when its provenance and boundary are equally visible.

The evidence contract

Before discussing implementation, this is what each headline claim does and does not prove.

Claim Published evidence Boundary
The original snapshot is healthy Frozen JavaScript run: 1,045 / 1,045 assertions Baseline only; not 1,045 assertions executed against Go
The port builds and runs independently One-command build, clean-archive rebuild, Linux/Windows CI, native CLI, Go/WASM demo The browser demo exposes parse and normalize, not every library option
Dense values matched the oracle 672,321 deterministic parse/stringify comparisons; zero observed differences or errors JSON-compatible dense trees and supported serializable options, not callbacks, cycles, or sparse holes
Sparse behavior exists in the Go model Explicit Element.Present representation and Go regression tests Ordinary JSON cannot transport a JavaScript array hole faithfully
Performance was measured comparatively Shared workloads, 40 retained raw samples, p99, startup, and externally sampled Working Set One sequential Windows session; not a universal performance claim

This table prevented several tempting but false sentences from entering the README.

A query string is not map[string]any

The shortest Go API would have been map[string]any. It also would have erased the distinctions I most needed to preserve: object versus array, a sparse hole versus explicit undefined, deterministic property order, and type-specific stringify behavior.

Instead, the public API uses a closed value algebra. Its state is private, and callers construct values through typed constructors:

type Member struct {
    Key   string
    Value Value
}

type Element struct {
    Present bool // false means a sparse hole
    Value   Value
}

type Value struct {
    kind        Kind
    objectValue []Member
    objectIndex map[string]int
    arrayValue  []Element
    // private scalar fields omitted
}
Enter fullscreen mode Exit fullscreen mode

There is no any or interface{} escape hatch in the public Go value model. Ordered members provide stable enumeration while an internal index preserves efficient lookup. Sparse elements distinguish a missing slot from a present undefined or null value.

The parser also uses a balanced bracket scanner instead of one convenient regular expression. Nested bracket segments and unclosed suffixes have observable upstream behavior; a flat split is shorter, but it is not the same parser. Compatibility thresholds such as arrayLimit remain separate from resource budgets such as input bytes, nodes, and nesting depth.

This design costs more code than a map. It pays that cost once, at the language boundary, instead of rediscovering ambiguity throughout the parser and stringifier.

Freeze the oracle before asking it questions

Differential testing can create false confidence when the reference moves underneath it. My oracle refuses to start unless it verifies:

  • upstream commit 3a890d4ecd3deb72a45d90be36f4f8c5970467c7;
  • test-tree identity bef346f180a38793ec6d47e11f25f88a7eb579ca;
  • source-tree identity;
  • four recorded original-test SHA-256 values.

The comparison path is intentionally boring:

deterministic case + supported options
                  |
          +-------+-------+
          |               |
  frozen JS oracle     Go implementation
          |               |
          +-------+-------+
                  |
       structural result comparison
                  |
       mismatch log + final report
Enter fullscreen mode Exit fullscreen mode

Node.js exists only on the left side of that development pipeline. The library, native CLI, and WebAssembly build do not invoke it.

The first oracle wire uses versioned NDJSON and intentionally accepts only dense JSON-compatible trees. Plain JSON cannot preserve sparse holes, undefined, negative zero, invalid UTF-16 surrogates, callbacks, cycles, or reference identity. I could have invented encodings and then advertised a larger number, but that would have mixed transport design with implementation proof. I kept the scope smaller, versioned it, and tested sparse semantics directly in Go.

The scope statement is not fine print. It is part of the result.

The fuzzer found a JavaScript rule I had modeled incorrectly

The most valuable counterexample was only 23 characters:

a[3]=4zf&a[1]=ui_ir
Enter fullscreen mode Exit fullscreen mode

with:

{"arrayLimit": 2}
Enter fullscreen mode Exit fullscreen mode

Because index 3 crosses that representation threshold, qs materializes object properties instead of a dense array. My first Go implementation preserved arrival order: 3, then 1.

JavaScript exposes a different order. Under OrdinaryOwnPropertyKeys, array-index keys are returned numerically before ordinary string keys. The observable upstream result is therefore conceptually:

{"1":"ui_ir","3":"4zf"}
Enter fullscreen mode Exit fullscreen mode

The correct fix was not "sort every key." That would destroy insertion order for ordinary properties. The Go object constructor now classifies canonical unsigned 32-bit property indices below 4294967295, sorts only that class numerically, and retains insertion order for everything else. Keys such as 01 and 4294967295 remain ordinary strings. A focused regression test locks the counterexample.

This was a bug in my developing port, not a latent bug in upstream qs, so I did not claim the separate Bug Catcher prize.

The integer property-ordering counterexample and regression evidence

Then the validator failed its own test

The quieter discovery mattered even more.

The differential runner alternated parse and stringify using one global case number. Parse received global indices 0, 2, 4, ...; stringify received 1, 3, 5, .... The generators contained 32 parse templates and 24 stringify templates.

Modulo an even schedule length, repeatedly adding two can visit only half the residues:

gcd(2, 32) = 2  ->  16 of 32 parse templates reachable
gcd(2, 24) = 2  ->  12 of 24 stringify templates reachable
Enter fullscreen mode Exit fullscreen mode

The comparison count kept rising. The output stayed green. Half the scheduled families were still unreachable.

I replaced the shared counter with operation-local indices, then added runner-level tests that enumerate the executed schedule and prove all 32 / 32 parse and 24 / 24 stringify templates are reachable and distinct. The earlier 564,651-case run was marked superseded and excluded from final evidence.

The replacement run used seed 0x5153474f for exactly 60,000 ms:

  • 336,161 parse comparisons;
  • 336,160 stringify comparisons;
  • 672,321 total comparisons;
  • zero observed mismatches;
  • zero Go errors;
  • zero oracle errors.

This is why I now treat a fuzz harness as production code. A green validator can be wrong about its own coverage while being perfectly correct about every case it actually executes.

The benchmark refused to become a victory slide

The comparative benchmark runs Node and Go sequentially on the same host and shared flat and nested workloads. It retains all 40 latency samples, all 40 cold-start samples, environment metadata, source identity, and externally sampled Working Set observations. With the recorded percentile rule and 40 samples, p99 is the maximum; the report says so.

The throughput story split cleanly by direction:

Workload Go median vs Node Go p99 vs Node
Parse 100 flat pairs 20.0% slower 36.8% slower
Parse 20 nested values 11.6% slower 61.2% slower
Stringify 100 flat pairs 40.3% faster 36.6% faster
Stringify 20 nested values 60.7% faster 56.1% faster

Process-level measurements were also mixed:

Measurement Node Go Interpretation
Median cold start 73.2933 ms 14.9117 ms Go about 4.9x faster
p99 cold start 79.6998 ms 384.8232 ms Go worse because one retained first-start sample is the maximum
Polled peak Working Set 65.9102 MiB 35.8477 MiB Go 45.6% lower in this run

Raw-retained comparative benchmark summary

The 384.8232 ms Go observation was inconvenient. It would have been easy to call it warm-up noise, delete the run, and rerun until p99 looked attractive. I kept it.

The defensible conclusion is narrower: this port has a strong median startup, memory, and stringify story on the recorded host; its parser requires optimization before anyone should claim general performance superiority. The complete benchmark evidence archive includes the raw samples, scripts, hashes, host snapshots, and correctness outputs.

The live demo reports per-run browser timing for feedback. Those numbers are deliberately excluded from the cross-runtime benchmark claim.

The decision I would take back

I would not let one global case index serve as both an operation selector and a generator selector. It looked deterministic and economical; mathematically, it coupled two independent schedules and hid half the corpus. Separate counters should have existed from the first version, together with reachability assertions.

I would also define the cross-language value model and transport boundary before touching parser code. Once I stopped treating JSON as a transparent carrier, decisions about sparse arrays, undefined, negative zero, and ordering became much easier to explain and test.

Finally, I would design benchmark retention before the first benchmark. The initial generation kept aggregates but not every observation. Those samples cannot be reconstructed honestly. The v2 runner therefore writes a new immutable evidence directory instead of overwriting history.

A five-minute verification path

The fastest inspection starts in the live demo. GitHub Actions compiles the existing cmd/qsgo entry point to WebAssembly; the page does not contain an alternate JavaScript parser. Run both parse and normalize, then reproduce the native checks from a fresh clone:

go test ./... -count=1
go vet ./...
go build -trimpath ./...
go test . -cover -count=1
Enter fullscreen mode Exit fullscreen mode

The expected root-package statement coverage is 81.5%. Then inspect three receipts:

  1. fuzz/report.json and fuzz/log.txt for the final differential record and counterexample;
  2. testdata/oracle/oracle_manifest.json for the frozen source and test identity;
  3. DECISIONS.md and the Evaluation Guide for architectural tradeoffs and claim boundaries.

One check remains unavailable in the recorded Windows environment: the portable Go toolchain had cgo disabled, so the race detector could not run. This library is not concurrency-heavy, but unavailable is not the same word as passed.

The most important artifact is not the zero in the mismatch column. It is the chain that makes the zero interpretable:

  1. freeze the source identity;
  2. establish the original baseline;
  3. define the equivalence boundary;
  4. test the port;
  5. test the validator;
  6. publish the counterexample and regression;
  7. retain the benchmark result that hurts the story;
  8. state what remains unproved.

AI has made cross-language ports cheap to generate. Evidence is still expensive. That is exactly why it is the part worth engineering.


Built by Parity Forge for Port Mortem 2026, organized by Hackathon Raptors, Track H — Open Pair: JavaScript to Go. The official pool lists ljharb/qs under both the JavaScript-runtime track and the Open Pair pool; this entry uses Track H and documents the migration rationale in its README.

Repository: https://github.com/agentic-build-lab/qs-go

Release: https://github.com/agentic-build-lab/qs-go/releases/tag/port-mortem-2026-submission

Live demo: https://agentic-build-lab.github.io/qs-go/

Evidence video: https://drive.google.com/file/d/1dL5DhhTMIiO67chlSwGq0GieninJYw9c/view?usp=sharing

Top comments (0)