Phase 1: describe the Apple ABI once, and check the four copies agree #8

Merged
rleibman merged 1 commit from phase/1-shim-generator into plan/phase-0-spikes 2026-09-29 07:05:20 -07:00
Owner

Closes the code half of #7. See docs/13-phases.md for the phase table.

What measuring changed

Phase 1 was scoped as "generate the Swift shim". Measuring the two shims before writing a generator disproved that premise, and the measurement is the most useful thing here:

per shim AppKit UIKit
exported functions 34 35
signature lines (generatable) 80 82
body lines (hand-written) 211 212

Of the 34 functions in both shims, 7 have byte-identical bodies, 13 differ only in a type or property name, and 14 are genuinely different code. NSImageView.imageScaling is not UIView.contentMode; placeholderString is not placeholder; AppKit's coordinate system is flipped. A generator emitting bodies would emit guesses, and the ~5 200-line saving docs/09 projected was never available.

What is available, and is worth more

Every boundary function is declared four times — C header, two @_cdecl signatures, one Scala extern. At 34 functions that is 136 declarations kept in step by hand, and nothing checks them: Swift compiles against its own signature, Scala Native against its own extern, and the linker matches by name only. An Int32 against an Int64 links cleanly, passes CI, and reads a garbage register at runtime — in a callback, on a phone. That failure had no detector anywhere in this repository.

What this adds

tools/shim-gen — plain JVM, aggregated into root, no Scala Native and no Xcode, so it runs on the machine where the mistake gets written rather than only on the one that suffers it.

Abi.scala the source of truth: 34 functions in a type vocabulary with no struct in it
Parse.scala reads declarations back out of C, Swift and Scala
Emit.scala emits the C header, the Scala externs, and Swift signature scaffolding with fatalError bodies — never a guessed body
ConsistencySuite the four artefacts agree, and Abi describes them
GenerateSuite generating from Abi reproduces exactly what is checked in

10 tests, compared as declarations rather than as text: a byte diff would fail on a comment reflow and say nothing about whether the ABI was preserved.

Exit criterion

Target Measured
Hand-maintained declarations per function 4 → 1 4 → 1 (1 Abi entry; the 2 Swift bodies stay hand-written)
Generated header vs checked-in same declarations identical; only two long declarations wrap at a different column
Generated externs vs checked-in same declarations identical at machine representation
Check runs without Xcode yes yes — JVM only
Tests — 108 pass (10 new)

Two exceptions, encoded rather than papered over

  • sui_set_root_view is exported by the UIKit shim and declared in neither the header nor Shim.scala — and that is correct. On iOS the Swift side owns @main, because a UIScene delegate cannot live in Scala Native's static archive, so it hands its root view in. Scala never calls it. Abi.swiftOnly records this so it is not reported as drift.
  • The Scala comparison runs at machine representation, not at the named type. Scala Native binds sui_handle and const uint8_t * to the same Ptr[Byte]; demanding it tell them apart would be a false positive forever. Every difference that costs a wrong register still fails the check.

Deliberately not done

The generated files are not adopted. GenerateSuite proves the swap changes no declaration, but it should be made on a machine that can compile the Swift and rerun the 23-check self-test. That half of #7 stays open.

Also in this branch

docs/13-phases.md phase 1 row and §13.3 rewritten against the measurements rather than the original assumption; docs/12 and the decision log updated in the same PR, per rule 5.

🤖 Generated with Claude Code

Closes the code half of #7. See `docs/13-phases.md` for the phase table. ## What measuring changed Phase 1 was scoped as "generate the Swift shim". Measuring the two shims *before* writing a generator disproved that premise, and the measurement is the most useful thing here: | per shim | AppKit | UIKit | |---|---|---| | exported functions | 34 | 35 | | signature lines (generatable) | 80 | 82 | | body lines (hand-written) | 211 | 212 | Of the 34 functions in both shims, **7** have byte-identical bodies, 13 differ only in a type or property name, and **14 are genuinely different code**. `NSImageView.imageScaling` is not `UIView.contentMode`; `placeholderString` is not `placeholder`; AppKit's coordinate system is flipped. A generator emitting bodies would emit guesses, and the ~5 200-line saving `docs/09` projected was never available. ## What is available, and is worth more Every boundary function is declared **four** times — C header, two `@_cdecl` signatures, one Scala `extern`. At 34 functions that is **136 declarations** kept in step by hand, and **nothing checks them**: Swift compiles against its own signature, Scala Native against its own `extern`, and the linker matches **by name only**. An `Int32` against an `Int64` links cleanly, passes CI, and reads a garbage register at runtime — in a callback, on a phone. That failure had no detector anywhere in this repository. ## What this adds `tools/shim-gen` — plain JVM, aggregated into root, **no Scala Native and no Xcode**, so it runs on the machine where the mistake gets written rather than only on the one that suffers it. | | | |---|---| | `Abi.scala` | the source of truth: 34 functions in a type vocabulary with no struct in it | | `Parse.scala` | reads declarations back out of C, Swift and Scala | | `Emit.scala` | emits the C header, the Scala externs, and Swift signature scaffolding with `fatalError` bodies — never a guessed body | | `ConsistencySuite` | the four artefacts agree, and `Abi` describes them | | `GenerateSuite` | generating from `Abi` reproduces exactly what is checked in | 10 tests, compared as *declarations* rather than as text: a byte diff would fail on a comment reflow and say nothing about whether the ABI was preserved. ## Exit criterion | | Target | Measured | |---|---|---| | Hand-maintained declarations per function | 4 → 1 | **4 → 1** (1 `Abi` entry; the 2 Swift *bodies* stay hand-written) | | Generated header vs checked-in | same declarations | **identical**; only two long declarations wrap at a different column | | Generated externs vs checked-in | same declarations | **identical** at machine representation | | Check runs without Xcode | yes | **yes** — JVM only | | Tests | — | **108 pass** (10 new) | ## Two exceptions, encoded rather than papered over - **`sui_set_root_view` is exported by the UIKit shim and declared in neither the header nor `Shim.scala` — and that is correct.** On iOS the Swift side owns `@main`, because a `UIScene` delegate cannot live in Scala Native's static archive, so it hands its root view *in*. Scala never calls it. `Abi.swiftOnly` records this so it is not reported as drift. - **The Scala comparison runs at machine representation, not at the named type.** Scala Native binds `sui_handle` and `const uint8_t *` to the same `Ptr[Byte]`; demanding it tell them apart would be a false positive forever. Every difference that costs a wrong register still fails the check. ## Deliberately not done **The generated files are not adopted.** `GenerateSuite` proves the swap changes no declaration, but it should be made on a machine that can compile the Swift and rerun the 23-check self-test. That half of #7 stays open. ## Also in this branch `docs/13-phases.md` phase 1 row and §13.3 rewritten against the measurements rather than the original assumption; `docs/12` and the decision log updated in the same PR, per rule 5. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Phase 1 was scoped as "generate the Swift shim". Measuring the two shims
before writing any generator disproved that, and the measurement is the most
useful thing in this commit:

  per shim   AppKit  UIKit
  functions      34     35
  signatures     80     82   generatable
  bodies        211    212   hand-written

Of the 34 functions in both shims, 7 have byte-identical bodies, 13 differ
only in a type or property name, and 14 are genuinely different code.
NSImageView.imageScaling is not UIView.contentMode; placeholderString is not
placeholder; AppKit's coordinate system is flipped. A generator emitting
bodies would emit guesses, and the ~5 200-line saving docs/09 projected was
never on the table.

What is on the table is worth more. Every boundary function is declared four
times - C header, two @_cdecl signatures, one Scala extern - which is 136
declarations at current size, kept in step by hand, with nothing checking
them. Swift compiles against its own signature, Scala Native against its own
extern, and the linker matches by name only: an Int32 against an Int64 links
cleanly, passes CI, and reads a garbage register at runtime in a callback on
a phone. That failure had no detector anywhere in this repository.

tools/shim-gen is that detector, plus the description it checks against:

- Abi.scala   34 functions in a type vocabulary with no struct in it
- Parse.scala reads declarations back out of C, Swift and Scala
- Emit.scala  emits the C header, the Scala externs, and Swift signature
              scaffolding with fatalError bodies - never a guessed body
- 10 tests    the four artefacts agree; generating from Abi reproduces
              exactly what is checked in, compared as declarations rather
              than as text

Plain JVM, aggregated into root, no Scala Native and no Xcode. It runs on the
machine where the mistake gets written, not only on the one that suffers it.

Two exceptions are encoded rather than papered over. sui_set_root_view is
exported by the UIKit shim and declared in neither the header nor Shim.scala,
and that is correct - on iOS the Swift side owns @main because a UIScene
delegate cannot live in Scala Native's static archive, so it hands its root
view in. And the Scala comparison runs at machine representation, not at the
named type, because Scala Native binds sui_handle and const uint8_t * to the
same Ptr[Byte]; demanding it tell them apart would be a false positive
forever.

The generated files are NOT adopted here. GenerateSuite proves the swap
changes no declaration, but it should be made where the Swift can be compiled
and the 23-check self-test rerun. Forgejo #7.

108 tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
rleibman merged commit b149103771 into plan/phase-0-spikes 2026-09-29 07:05:20 -07:00
rleibman deleted branch phase/1-shim-generator 2026-09-29 07:05:20 -07:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
rleibman/scala-ui!8
No description provided.