Research / Compiler, backends and verification

P50 · experience · draft

Pinning the Semantics a Six-Target Compiler Silently Inherits: String Units, Sized and Trapping Integers, Total Division, Iteration Order and Injective Mangling

partial experience Compiler, backends and verification

State of the work. Decisions, docs and per-tier probes are on main for strings, integers, division, map order and mangling; the cost figures are single prose measurements and there is no divergence ledger yet.

This is a draft, not peer reviewed. Repository references link to revl at commit 45bec431f.

Planned to be combined with P48 (Six Backends, One Semantics: A Conformance Matrix, Authored-Probe Differential Execution and a Cross-Tier Agreement Theorem), P49 (Type-Directed Cross-Tier Fuzzing With a Reference Oracle, an Honesty Classifier and a Delta-Debug Shrinker) into a single paper.

Pitch

When one IR feeds six host languages, every unspecified detail (what a string's length counts, what happens at integer overflow, how a map is iterated, whether two identifiers can collapse into one) is silently decided six different ways, and each time revl decided it once, wrote the choice down, and turned it into a probe that runs on every tier.

Abstract

A compiler that emits Python, TypeScript, Go, Rust, Java and WebAssembly from one typed IR inherits, for every operation the specification leaves open, six host-language answers. We report on a series of decisions in revl that closed such gaps, in each case the same way: measure what each tier actually computes on a separating input, choose one meaning by an argument from the language's own rules, make the frontend mark the information the backends lacked, add an execution probe on every tier that can run it, and document the closure so it cannot regress. The cases are: the unit of a string (code points; "😀".length() was 1, 2, 4 or a rejected literal depending on the tier), Int as 64-bit with trapping overflow on all six tiers (a measured cost of about 9% on a dependent scalar chain, with an Int32 type for kernels that are about 34% faster), division named by rounding rule with a Result-returning total form, canonical sorted-key map iteration, an implicit Int-to-Float coercion marked on the IR, and reserved-word mangling that was not injective (lambda and lambda_ collided, so Python returned a different value than the other five tiers). We list the divergences found, which were not found by the specification but by execution, and the three ways the checks themselves were found to be switched off in CI. We are explicit about what is not closed: arbitrary-precision Integer is a proposal, WebAssembly still refuses Float, maps and non-integer float rendering, and the cost figures come from single measurements stated in prose.

Problem and claim

Cross-target agreement is usually argued from the emitters: if every backend accepts a construct, the construct is portable. revl's own errata say why that fails: "tests/test_cross_tier.py checks that every emitter accepts a construct, which catches a tier that refuses and cannot catch a tier that accepts and then means something else" (docs/contract-errata.md:271-274). The failures that mattered were silent: a value that differs, not a build that breaks.

The claim of this paper is a procedure and its history, not a theory. Cross-target divergence appears first in decisions nobody wrote down. Deciding each once, giving the backends the missing information through the IR (for example operands on arithmetic nodes, "widen": "Float" on coercion sites), and attaching a probe that executes on each tier converts a silent divergence into either agreement, a named refusal, or a failing test. The docs also give the cost of each decision where it was measured.

The scope is deliberately limited to what the tree shows. Several decisions are only partly landed, and some documentation is stale; both are noted below instead of smoothed over.

Contributions

  1. A decision write-up for five families, each with the measured pre-fix divergence table, the chosen meaning and the argument: string unit (docs/strings.md:99-135), integer width and overflow (docs/arithmetic.md:213-245), division and remainder conventions (docs/arithmetic.md:31-82), map iteration order (docs/collections.md:85-129), and identifier mangling (docs/v2.0-roadmap.md:1584).
  2. The pattern "frontend marks, backends emit": operands on bin nodes for / and %, and "widen": "Float" on coercion sites, both additive with no ir_version bump (docs/arithmetic.md:377-412; operands defined at :172 and :355).
  3. A counterexample to cross-tier equivalence of a different kind: non-injective keyword mangling, where Python silently returned a different value than the other five tiers. The roadmap notes that an injectivity assertion failed on the pristine self-host tree; this is not reproduced in the tree.
  4. Cost figures stated in the docs (single prose measurements, not reproduced in the tree): about 9% for 64-bit trapping Int on a scalar chain (0.98 to 1.07 ns/iter), and about 34% for an Int32 vectorisable loop.
  5. A baseline discipline for known divergences: pinned current behaviour that fails both when it worsens and when it starts passing (tests/test_cross_tier_execution.py:740-745, :930-940).
  6. An honest ledger of what escaped: probes sitting behind an environment variable nothing sets, a skipped TypeScript column, a doc that called a defective tier "already correct".

Evidence on main

Claimpath:line or testStatusNote
String unit is code points on all six tiersdocs/strings.md:99-104; tests/test_cross_tier_execution.py:1927-2010 (test_string_unit_is_code_points); tests/test_458_str_ordering.pyshippedExecuted on py, ts, go by default; rust, java, wasm only under REVL_CROSS_TIER_SLOW
Pre-fix unit divergence table (py 1, ts 2, java 2, wasm 4, go and rust rejected the literal)docs/strings.md:37-47shipped (measurement in the docs)The java column was read from the emitter, not executed ("no JDK here", docs/strings.md:6-7)
Unit choice argued from existing split behaviour and the syntax ruledocs/strings.md:106-132shipped
String ordering, split(""), index bounds also pinnedtests/test_458_str_ordering.py:1-40; docs/contract-errata.md:659-694; tests/test_str_index_bounds_cross_tier.pyshippedA third fault (java keys() tie-break, described at tests/test_458_str_ordering.py:40-46; the errata range cited above does not mention it) was found while building the boundary fixture
Int is 64-bit two's complement and overflow traps, on all six tiersdocs/arithmetic.md:213-245 table; :278-310shippedwasm traps with no message; "a property of the instruction set"
Cost of the trap about 9%docs/arithmetic.md:222-228 (0.98 to 1.07 ns/iter)shipped (prose number)Single dependent scalar chain; no harness in the tree reproduces it
Int32 ships; vectorisable loop about 34% fasterdocs/arithmetic.md:414-441shipped (type), unreproduced (number)docs/int32-proposal.md:3 still says "proposed, not implemented": stale
Unary minus and Int.MIN / -1 trap on every tierdocs/arithmetic.md:262-277; docs/contract-errata.md:596-620; tests/test_int_min_edges_cross_tier.py; test_negation_of_int_min_traps (tests/test_cross_tier_execution.py:1644), test_div_int_min_traps (:1698)shipped
TypeScript Int is BigInt, was f64docs/contract-errata.md:596-601; tests/test_cross_tier_execution.py:1553shippeddocs/arithmetic.md:620-623 still lists "TypeScript is still not 64-bit" un-struck under "Still open", which contradicts :278-283: stale bullet
Literal outside the range is refused by the checkerdocs/arithmetic.md:246-258; examples/rejections/t20_int_literal_range.rvlshippedInt.MIN has no literal spelling
Division has names; % is truncated; mod Euclidean; pairing law over 16 sign combinationsdocs/arithmetic.md:31-82; tests/test_cross_tier_execution.py:1395-1425 (PAIRING_LAW, test_division_remainder_pairing_law)shipped
Total division forms return Result[Int, Str]; Int.MIN / -1 answers Err("revl: Int overflow")docs/arithmetic.md:642-693; tests/test_cross_tier_execution.py:1309 (CHECKED_DIVISION), :1719shippedExecuted on py, ts, go, wasm by default; rust, java slow
Integer division by zero faults on all six tiers; literal zero divisor refuseddocs/arithmetic.md:591-616; examples/rejections/arith_zero_divisor.rvlshippedTypeScript used to return Infinity
Float is IEEE binary64; == not reflexive; < partialdocs/arithmetic.md:178-211shippedNot a divergence, a caveat the docs state
Int-to-Float coercion marked on the IR, explicit conversion per backenddocs/arithmetic.md:377-412; tests/test_cross_tier_execution.py:687 (test_widening_is_marked_in_the_ir)shippedBefore: Rust E0308, TypeScript wrong answer (3n === 3 false)
Map iteration: sorted by Unicode scalar, shipped on five tiers, wasm refuses mapsdocs/collections.md:85-129; tests/test_cross_tier_execution.py:214 (probe entry "map iteration is canonical order", not a test function); tests/test_458_str_ordering.py:285-290shippeddocs/collections.md:151-160 and docs/stdlib-2.0.md:271 still say the TCK case is "pending"; stale
TCK clause for map order existstck/spec.py:271,369-380 (c1_map_iteration_order)shippedWhether any adapter currently reports it live was not checked
Reserved-word mangling made injective on five tiersdocs/v2.0-roadmap.md:1584; backends/python/emit.py:440-470; backends/python/tests/test_reserved_word_idents.py:121-170; tests/test_cross_tier_execution.py:312-335shippedThe roadmap's cited line numbers (emit.py:278-289) are out of date; the rule now lives at emit.py:446
Injectivity ported to five self-host emitters; the roadmap notes an injectivity assertion failing on the pristine treedocs/v2.0-roadmap.md:1687shippedThe pristine-tree check is described in the roadmap; no script in the tree reproduces it
A residual namespaced-key collision is refused by nametests/test_selfhost_emit_rust.py:1379-1395shippedRust shown; same pattern in py/go/java tests
Known divergences are pinned, failing on drift either waytests/test_cross_tier_execution.py:740-745,757,930-940shipped
Integer (arbitrary precision)docs/integer-proposal.md:3 "proposed, not implemented"; docs/contract-errata.md:891-901designedWasm bignum "concentrated cost"
Probes were not running in CIdocs/contract-errata.md:276-300shipped (finding)REVL_CROSS_TIER_SLOW is set in .github/workflows/ci.yml:2292 now

Numbers that exist

Paper outline

  1. Introduction: the emoji-length table as the opening example; why accept-only checks pass.
  2. Method: measure, choose, mark in the IR, probe, document. The DIVERGENCES baseline discipline.
  3. Five decisions in turn: strings (unit and ordering and bounds), integers (width, trap, literal range, Int32, Int.MIN edges), division and remainder (names, pairing law, total forms), iteration order, mangling injectivity.
  4. Costs: the two measurements and what they do and do not show; the Wasm tier's refusals as a cost in coverage.
  5. How the checks failed: the env var nothing set, the skipped TypeScript column, the "already correct" Java claim.
  6. Related work and lessons.

Threats to validity and limits

Fact-check

Claims in this draft were checked against revl main at commit 45bec431f: 28 checked, 6 corrected.

Cite this draft

@misc{revlp50,
  author       = {Thomas Moussajee},
  title        = {Pinning the Semantics a Six-Target Compiler Silently Inherits: String Units, Sized and Trapping Integers, Total Division, Iteration Order and Injective Mangling},
  year         = {2026},
  howpublished = {\url{https://inso1337.github.io/revl/research/P50.html}},
  note         = {Research draft, not peer reviewed. Written against revl commit 45bec431f. Licensed CC BY 4.0.}
}

Text licensed CC BY 4.0, author Thomas Moussajee.

← All research drafts