feat(spec): one TypeScript utility, seven native languages, with the generated code committed - #580
hyanmandian wants to merge 13 commits into
Conversation
Adds `spec/`, a prototype of describing a utility once as language neutral data and emitting it to every language Brazilian Utils publishes. Nothing under `src/` changes and nothing here ships in the npm package. `spec/EXPLORATION.md` is the write-up: what it proves, what it does not cover, why transpiling TypeScript and a shared native core were rejected, and the suggested order of work. What is in the prototype: - `spec/utilities/*.json` specify `isValidCpf`, `isValidPis` and `formatCpf` as pipelines over explicit charsets and check digit algorithms. A utility has named profiles, and each package declares which one it implements today, so generated code reproduces current behaviour instead of replacing it. - `spec/codegen/interpret.ts` is the reference implementation; the six emitters under `spec/codegen/targets/` turn the same specs into idiomatic TypeScript, Python, Go, Ruby, Rust and Java. - `spec/vectors/conformance.json` holds 743 seeded inputs with the answer every profile gives, and the conformance drivers check each language against it. Results, all reproducible with `bash spec/conformance/run-all.sh`: - the spec agrees with the package this repository ships on 11,928 checks, hostile inputs included; - every generated target passes its vectors; - the spec agrees with the published Python, Ruby, Go and Rust packages on 743/743 corpus inputs each, with two documented deviations in Python. Findings worth acting on, recorded in the specs and the write-up: the six packages already give four different answers for a masked CPF; `brutils` accepts a CPF written in Arabic-Indic digits because it gates on `str.isdigit()`; PIS diverges the other way, with four packages accepting `"00000000000"` and rejecting masks; and regex shorthands (`\d`, `\s`, Ruby's line anchors, Java's pre-lexing `\uXXXX`) do not survive a port, which is why the IR carries code points instead of regexes. Config: `vite.config.ts` gains a lint override for `spec/**` (console output, running other toolchains from PATH, asserting the shape of its own JSON) and keeps the generated vectors out of the formatter. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UX1gTGeMTyoXQyr1qUoQKd
Answers the follow up to the first exploration: could the logic live in one binary core — Rust, C, wasm — with every package binding to it instead of carrying generated source? `spec/BINDINGS-INVESTIGATION.md` is the write-up; `spec/bench` is the harness behind every number in it. Generated source wins. One validator, one corpus of 1000 inputs, the same profile in every arm: - generated source runs at 0.36x (Java), 0.43x (Go), 0.55x (Ruby), 0.56x (Python) and 0.66x (Node) of the idiomatic handwritten code it replaces; - UniFFI, the tool most often recommended for this, costs 9064 ns per call in Python — 3.7x slower than simply implementing the validator in Python, and 17x slower than the same shared library over hand written ctypes; - the boundary, not the work, is the cost: 104 ns per call in Go over wazero, 521 ns in Ruby, 472 ns over ctypes in Python, and 30059 ns over wasmtime-py, against a validator whose body takes about 100 ns; - batching one call over 1000 inputs makes a shared core fast everywhere (0.12x), which is the one workload where it earns its keep; - for the npm package a wasm core costs 9291 bytes against 509 for the generated `isValidCpf`, with no tree shaking and asynchronous startup. Two emitter rules make the source level result hold, and both are in this commit. Before them the Python and Ruby emitters walked strings code point by code point and were 2.6x and 4.1x slower than handwritten: - `guard-shape` plus `sanitize` collapse into one anchored regex with capture groups, with the character classes written out code point by code point so the semantics stay the spec's; - check digits are read as bytes and the verification is unrolled. Python went 6485 to 1366 ns/op and Ruby 15051 to 2020, with conformance unchanged at 2229/2229 for both and the JavaScript differential still at 11928/11928. The handwritten arms also disagree with the spec on the same corpus (Python 333, Ruby 329, Go 334 valid, against 337), each language's own `strip` being a different set of whitespace. Correctness is the reason to generate; the performance is what makes it affordable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UX1gTGeMTyoXQyr1qUoQKd
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Advanced Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Comment |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #580 +/- ##
=========================================
Coverage 100.00% 100.00%
=========================================
Files 183 183
Lines 2069 2069
Branches 612 612
=========================================
Hits 2069 2069
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Tree-shaking report✅ No bundle size impact. All 155 exports are the same size as on the base branch (full import 648.9 KB, gzip 166.2 KB). All exports (155)
How this is measuredEvery export is imported alone into an esbuild consumer bundle (minified, tree-shaken) built from the head and from the base of this pull request; the sizes are the resulting bundles, gzip is their gzipped size. 🔴 marks a regression: a pre-existing export that grew more than 20% and more than 256 B, or the bundle importing every pre-existing export growing more than 5%. 🟡 is growth under the threshold, 🟢 a decrease, ⚪ no change, 🆕 an export that does not exist on the base (never a regression), 🗑️ an export that was removed. An intentional increase is accepted with the |
Adds `spec/bridge`, a compiler that takes a utility written once in a portable subset of TypeScript and emits idiomatic native code for TypeScript, Python, Go, Rust, Ruby, Java and C#. Nothing per language is written by hand: each target is an emitter plus a small runtime, both of which are utility agnostic. Covered so far: `isValidCnpj`, `formatCnpj` and `getMunicipalities`. How the hard parts are pinned: - Regular expressions are compiled at build time into explicit code point classes and a backtrack free matcher, so `\s` means the same 25 code points everywhere instead of 6 in Go and a Unicode property in Python. - Locale collation is resolved at build time too: `data/build.ts` bakes the pt-BR order the JavaScript package returns into the dataset, because no two of the seven targets ship the same collator and four ship none at all. - The boundary quirks the JavaScript package has (`String(value)` on a number, a truthy non boolean option) are a small portable std every target implements. Parity is measured, not asserted: - `conformance/verify-typescript.sh` drops the generated TypeScript into `src/` and runs the package's own vitest suites against it: 4 files, 157 tests pass, the `getMunicipalities` type level tests included. - `conformance/run-all.sh` replays 5,730 CNPJ expectations and an 11,176 line municipality dump, both recorded from the shipped package, through all seven targets. All seven match; Go, Rust, Java and C# skip the 18 cases where a `string | number` union has no static form. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UX1gTGeMTyoXQyr1qUoQKd
Adds `getAddressInfoByCep` to `spec/bridge`, which completes the POC: the four utilities it now covers use, between them, every hard thing a Brazilian Utils function does — regular expressions, a 5,571 row dataset with a pt-BR collation order, HTTP with retries, JSON, an error hierarchy, and three providers raced against each other. The source stays straight-line, synchronous TypeScript. Three things are now the emitter's job rather than the author's: - `async` is inferred. The compiler works out which functions wait on the network and colours the call graph per target: TypeScript and C# get async/await and a Promise/Task return, while Go, Rust, Ruby, Java and Python stay blocking. - Raising is translated. `throw new GetAddressInfoByCepNotFoundError(…)` becomes a real exception class in five targets, `(T, error)` threaded through every call site in Go, and `Result<T, runtime::Error>` in Rust. The declared error hierarchy survives in all seven. - Concurrency is one primitive. `startAll` becomes promises, Tasks, goroutines with a channel, threads with an mpsc channel, virtual threads, or threads with a queue, depending on the target. Parity, all recorded from the package this repository ships: - The package's own vitest suites pass against the generated TypeScript: 4 files, 157 tests, the type level assertions and the CEP error class hierarchy included. - `conformance/run-all.sh` replays the CNPJ vectors, an 11,176 line municipality dump and 110 CEP scenarios through all seven targets. The CEP scenarios are served over real HTTP by the same table that recorded them, so Go, Rust, Java, C#, Ruby and Python exercise their own HTTP clients, JSON readers and schedulers rather than a stub. - All seven match. The only skips are inputs with no form in a statically typed language — a `string | number` union, and a `providers: null` that Python and Ruby cannot tell from an absent one. Both are listed in the README. `spec/bridge/README.md` documents the accepted subset, the three things that would otherwise differ between hosts, and every known gap. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UX1gTGeMTyoXQyr1qUoQKd
990cdfc to
a17cc47
Compare
…t uses The bindings investigation concluded that generated source beat every binding. That conclusion came from the wrong measurements: it compared generated source against `ctypes`, Fiddle and a wasm runtime, and never against the mechanism each ecosystem actually ships native code with. Those differ by three orders of magnitude, so the conclusion was wrong for four of the seven ecosystems. Measured properly, per call, against a validator body of 47 ns: | ecosystem | best | mechanism | boundary | | --------- | ----------- | ----------------------------- | -------- | | C# | 76 ns | P/Invoke, SuppressGCTransition| ~1 ns | | Python | 65 ns | CPython extension module | 27 ns | | Ruby | 109 ns | C extension | 65 ns | | Java | 127 ns | Panama FFM, trivial downcall | 62 ns | | Go | 119 ns | cgo | 58 ns | Against each language's own generated source that is 21× for Python, 18× for Ruby, 3.9× for Java, and 4.3× against handwritten for C#, which had no arm at all before. The old numbers stand for what they measured: ctypes is 433 ns of boundary, Fiddle 1,479 ns, wasmtime-py 30 µs. Three details decide which half a binding lands in, and the harness shows each: a `static final` MethodHandle and a global arena in Java (84 ns otherwise), a real extension module rather than an FFI shim in Python and Ruby, and `[SuppressGCTransition]` in C# — the one runtime where the naive version is already fine. What does not change is JavaScript: the package is tree shakeable and a binary core is not, so npm keeps generated source whatever the others do. Go keeps it too, because cgo costs cross compilation, static binaries and CGO_ENABLED=0 for a 74 ns gain. So the answer is a hybrid, and `spec/bridge` now emits both halves from the one source: the Rust target also produces `#[no_mangle] extern "C"` wrappers, a C header, and a crate that builds as `cdylib` and `staticlib`. The CNPJ vectors replay through that ABI from C as an eighth conformance arm — 5,782 of 5,782, the same 18 skips as the other statically typed targets — which is the same surface a CPython extension, a Ruby C extension, a NuGet package over P/Invoke and a JAR over Panama would each call. Shapes the ABI refuses rather than guesses at, and says so in its own output: a function that raises needs an out parameter, one that waits on the network needs a callback, one that answers a list needs an iterator. `getMunicipalities` and `getAddressInfoByCep` stay generated-source only for now. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UX1gTGeMTyoXQyr1qUoQKd
The JSON-per-utility prototype under `spec/codegen` stopped scaling at the third utility and `spec/bridge` replaced it, so the prototype, the schema and the utility files it read, and the conformance runners built on it are removed. `spec/bench` goes with it: the harness existed to answer whether a binary core beats generated source, it answered, and the answer is written down in `spec/BINDINGS-INVESTIGATION.md`. Both are one `git checkout` away in this branch's history, which the report now says. What is left under `spec/` is the engine and the research that shaped it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UX1gTGeMTyoXQyr1qUoQKd
…anguages `spec/bridge` reads a utility written once, in a documented subset of TypeScript, and emits idiomatic native code for TypeScript, Python, Go, Rust, Ruby, Java and C#, plus a C ABI over the Rust crate for the ecosystems that would rather bind to a shared core than carry generated source. Three things would otherwise differ between the targets, and each is resolved at build time rather than left to the host: regular expressions are compiled into explicit code point classes and a backtrack-free matcher, collation is resolved once against the JavaScript package's own comparator and baked into the dataset, and JavaScript's boundary coercions live in a small portable standard library every target implements natively. Three more are the emitter's job rather than the author's: async colouring, raising, and the one concurrency primitive the subset has. A utility is one file exporting one function, the way `src/` is one directory per utility. What two of them share goes under `source/_internals/`, and `compiler/link.ts` inlines those helpers into whichever utility imports them and prunes whatever it does not reach, so every target still emits one self-contained unit. That is what keeps the JavaScript output tree shakeable and the C symbol table flat. Parity is measured rather than asserted, and nothing about it is hand-written per utility: a utility says which arguments to replay, the expectation comes from calling the package this repository ships, and the program that replays it in each language is generated from the compiled signature. A row also records which targets can be handed the call at all, so a number where a signature says `string` is reported as not expressible rather than quietly dropped. The engine lands on its own. Each utility is a change of its own on top of it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UX1gTGeMTyoXQyr1qUoQKd
The first utility through the compiler, and the one that exercises the parts a validator
needs: two compiled regular expressions, a check digit pass over both CNPJ versions, and the
`typeof` guard that makes a number invalid in JavaScript.
The CNPJ rules that `formatCnpj` will also need — the two shapes a CNPJ can be written in,
the weight vectors, the check digits — go under `source/_internals/`, written once. The
compiler inlines them into this utility and prunes the rest, so the emitted module carries
what it uses and nothing else.
The corpus is every string literal in the package's own CNPJ test files, plus generated
CNPJs, masked variants, one-digit mutations and the numbers a JavaScript caller can pass
where a string is declared, under all three option sets. The generator is seeded, so the
table is the same on every run.
typescript 1725/1725 python 1725/1725 ruby 1725/1725
go 1707/1707 rust 1707/1707 java 1707/1707
csharp 1707/1707 cabi 1707/1707
The eighteen the compiled targets do not run are the numeric inputs: they declare the
parameter as a string, so `isValidCnpj(12345678000195)` is a question that cannot be asked
there. The row records that rather than dropping it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UX1gTGeMTyoXQyr1qUoQKd
The second utility, and the one that shows what sharing costs. It reads the same two
character classes `isValidCnpj` reads, out of the same `source/_internals/cnpj.ts`, and adds
a mask helper of its own under `source/_internals/mask.ts` — written once, for every
`format*` utility that comes after it.
What each target ends up with is still one self-contained module: the generated
`format-cnpj` carries the mask helper and the two classes it uses, and not a line of the
check digit code, because the compiler prunes what the utility cannot reach. That is the
property the npm package needs, and it is why the helpers are inlined rather than emitted as
a shared module per target.
Three options that interact — `pad`, `version`, `obfuscate` — over the same corpus, so all
seven combinations are replayed against every input.
typescript 4025/4025 python 4025/4025 ruby 4025/4025
go 3983/3983 rust 3983/3983 java 3983/3983
csharp 3983/3983 cabi 3983/3983
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UX1gTGeMTyoXQyr1qUoQKd
The utility that decides whether the engine is real. It reaches the network, parses JSON,
retries a transient failure, races three providers and raises one of four errors depending
on how they fail — and none of that is written per target.
The source is straight-line and synchronous. The compiler works out that the call graph
waits on the network and colours it: TypeScript and C# come out `async`, with a `Promise`
and a `Task`; Go, Rust, Ruby, Java and Python stay blocking. `throw` becomes a real
exception class in five targets, `(T, error)` in Go and `Result<T, runtime::Error>` in Rust,
with the declared hierarchy intact in all seven. `startAll` becomes promises, `Task`s,
goroutines and a channel, threads and an `mpsc`, virtual threads, and threads with a queue.
The recorder carries both sides of the same scenario table: recording stubs `fetch` the way
the JavaScript suite does, and replaying serves the same answers over real HTTP, so each
target runs through its own client. Fourteen scenarios cover a provider that answers, one
that misses, one that is down, a body that is not an object, fields that are not strings, a
masked CEP, and the flags that disagree with the body.
typescript 110/110 python 109/109 ruby 109/109
go 103/103 rust 103/103 java 103/103 csharp 103/103
Python and Ruby cannot be handed `{ providers: null }`, because `None` and `nil` are also
what an omitted option looks like; the five compiled targets cannot be handed a numeric CEP
or a `providers` that is not a list at all. The C ABI does not carry this one: a function
that raises and waits needs more than pointers and integers, and it is refused rather than
guessed at.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UX1gTGeMTyoXQyr1qUoQKd
The last of the four, and the one that is about data rather than logic: 5,571 municipalities
per state, in the order `localeCompare(…, "pt-BR")` puts them.
That order is the whole difficulty. Go and Rust ship no collator, Ruby compares bytes, and
Python, Java and C# each resolve their own ICU or libc table, so sorting at run time would
produce seven different answers. `data/get-municipalities.ts` resolves both orders once —
the per state one and the combined one — against the JavaScript package's own comparator,
and bakes them into the table every emitter materialises natively. Nothing sorts at run
time.
Every municipality of every key is replayed, not a sample, so one name out of place in one
target fails the check. The keys include an unknown state, a lower case one, the empty
string and three inherited `Object` property names, because each of those has an answer of
its own.
typescript 34/34 python 34/34 ruby 34/34 go 34/34
rust 34/34 java 34/34 csharp 34/34
The C ABI does not carry this one: a function that answers a list needs an iterator rather
than a buffer, and it is refused rather than guessed at.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UX1gTGeMTyoXQyr1qUoQKd
403fb0d to
4de5b16
Compare
The red
|
…e read `spec/bridge/out` was ignored, which meant the central claim — that the compiler emits code a Go or Rust maintainer would accept — could only be checked by running it. The output is now committed: 64 files, seven targets and the C ABI, every one of them headed `DO NOT EDIT` and rewritten by `node compiler/cli.ts`. What the targets' own toolchains build from it stays ignored, since those are binaries and caches that `conformance/run-all.sh` recreates: `target/`, `classes/`, `obj/`, `bin/`, `__pycache__/`, the Rust lockfile and the compiled C driver. The generated tree is excluded from the linter for the same reason it was already excluded from the formatter: linting emitted code is linting the emitters at one remove, and the emitters are linted directly. `README.md` gains a short index of which files are worth opening, because seven copies of the baked municipality table are 85% of the bytes and none of the interest. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UX1gTGeMTyoXQyr1qUoQKd
An emitter joins its sections with a blank line between them, so a module that reaches none of the constants, or none of the errors, came out with a run of up to five empty lines where they would have been. Harmless to a compiler and distracting to a reader, which matters now that the output is committed to be read. `cli.ts` collapses any run of three or more newlines as it writes, and leaves `.json`, `.toml` and `go.mod` alone. Every target spells a string literal with escapes rather than a real newline, so a run of blank lines in an emitted file is always layout and never content. 195 blank lines removed across the 66 generated files, nothing added, and all seven targets plus the C ABI still match the JavaScript package. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UX1gTGeMTyoXQyr1qUoQKd
A separate branch, pull request #580, explored the same problem with a different answer: a compiler to seven targets whose output leans on a per-language runtime shipped beside it. That branch is closing, and one part of it is worth more than the code — the measurements that answer a question this engine never addressed, which is why generate source at all instead of shipping one binary core and binding to it. Two findings decide it, and only one is about speed. A tree-shakeable package cannot take a binary core: one utility as generated source was 509 bytes that disappear when unused, against 9,291 indivisible and asynchronous ones. Go's cost for cgo is cross compilation, static binaries and `CGO_ENABLED=0`, not its 58 ns. Neither is a preference between acceptable options. The finding that does not decide it is kept too, because it is the one that makes the position honest: for Python, Ruby, C# and Java a binding really is cheaper than generated source, and that document's first version got it wrong by measuring the bindings a script reaches for rather than the ones a package ships. So the answer is scoped — JavaScript and Go must have generated source on grounds unrelated to speed, and the other four are a choice this engine makes for readability and the absence of a runtime rather than for throughput. ADR 0012 records that, including what it costs: four emitters to maintain and each language's semantics encoded four times. The document carries a header saying the harness behind its numbers was retired with the branch, so they should be reproduced before anything rests on them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013i7T7KEWrhJFNZ6qaVqJWf
|
Closing this in favour of #587, which takes the same problem with a different answer — but one part of this branch was worth more than the code, and has been carried over rather than lost with it.
Both headers say the harness was retired with this branch, so those numbers should be reproduced before anything load-bearing rests on them. What is not carried over is the compiler, and the reason is visible in one line of its own output. The seven-target reach is the real loss. Java, C# and Ruby are not in #587, and the per-language semantics behind them were genuine work. Generated by Claude Code |
What does this PR do?
Answers, with running code, the question: can we implement each utility once and ship it in every language, without changing what any published package does today?
Yes.
spec/bridgeis a compiler. You write a utility once, in ordinary TypeScript, and it emits idiomatic native code for TypeScript, Python, Go, Rust, Ruby, Java and C#, plus a C ABI over the Rust crate. No bindings, no embedded VM, no FFI, and nothing hand-written per language.The generated output is committed, so the central claim can be read rather than taken on trust.
Nothing here ships in the npm package.
src/is untouched — the build, the bundle and the public API are exactly what they were, which the tree-shaking report confirms: "No bundle size impact. All 155 exports are the same size as on the base branch."Four utilities, one per capability
Chosen so that between them they use everything that is hard about doing this at all:
isValidCnpjformatCnpjisValidCnpj, written once and pruned per utilitygetAddressInfoByCepgetMunicipalities946 lines of source in, 53,794 lines of generated code out, across 66 files.
What is shared, and what is not
The utility's implementation, not the published package. Every ecosystem keeps writing its own DX by hand — its naming, its option objects, its types, its docs — over whichever core it gets.
That decides the shape of the output. Each source file holds exactly one utility and each target emits exactly one self-contained unit, because the JavaScript output has to stay tree shakeable and a C symbol table has to stay flat.
src/here is one directory per utility with the shared parts under_internals/, andsource/is laid out the same way:inlinesplices the helpers a module imports into the module, so the compiler sees one file and every target emits one unit that references nothing else.prunethen drops whatever that splicing brought in and the module does not reach.You can read the result directly:
out/typescript/_bridge/format-cnpj.tsis 103 lines — the utility, the mask helper it shares, the two character classes it uses, and not one line ofisValidCnpj's check digits.out/typescript/_bridge/is-valid-cnpj.tsis the mirror image.The three things that would otherwise differ
\sis 25 code points in JavaScript, 6 in Go and Ruby, a Unicode property in Python and Rust;^/$are line anchors in Ruby; Java and C# expand\uXXXXbefore the regex engine sees it. Patterns are compiled at build time into explicit code point ranges and a backtrack-free matcher every runtime implements identically. The compiler refuses any pattern whose consecutive classes overlap, which is what makes the greedy scan provably correct.localeCompare(…, "pt-BR")order is resolved once at build time against the JavaScript package's own comparator and baked into the dataset.formatCnpj(12_345_678)coerces,{ obfuscate: 1 }is truthy,getAddressInfoByCep("01310-100")strips the mask. These are the contract, not accidents, so they live in a small portable std each target implements natively.What the author never writes
The source is straight-line, synchronous TypeScript. Everything below is the emitter's, and the committed output is where to check it:
async/Promiseasync/Task(T, error)Result<T, runtime::Error>TasksmpscParity is measured, not asserted — and the harness is generated too
Adding a utility is two files: the utility, and a list of the arguments to replay. Nothing else changes. The expectation comes from calling the package this repository ships; the table's columns come from the compiled signature; the program that replays them in each of the eight targets is generated from the same signature. There is no per-utility driver to write and none to review.
bash spec/bridge/conformance/run-all.sh— every target against a recording of the shipped package:"n/e" is not expressible: a call a JavaScript caller can make that has no form in that language — a number where the signature says
string,nullwhere it says a list of names. Each row records which targets can be handed it, so nothing is skipped for being inconvenient. The C ABI carries only the two utilities whose shapes fit pointers and integers; it refuses the other two and says why in the generated file.bash spec/bridge/conformance/verify-typescript.sh— the generated TypeScript dropped intosrc/, running the package's own suite unmodified:Including the type-level assertions:
expectTypeOf(getMunicipalities).parameter(0)has to beStateCode | undefined,getAddressInfoByCephas to resolve toAddressInfo, and the four CEP error classes have to extend one another.Reading the generated output
Shortest first. Seven copies of the baked municipality table are 85% of the committed bytes and none of the interest, so skip those until last:
out/typescript/_bridge/format-cnpj.tsout/rust/include/*.hout/go/get_address_info_by_cep/(T, error)threaded through every call site, goroutines and a channelout/rust/src/get_address_info_by_cep.rsResult<T, runtime::Error>, with anmpscchannelout/csharp/GetAddressInfoByCepUtility.csasyncall the way down, from source that never saysasyncout/is reproducible: a cleanrun-all.shleaves it byte for byte as committed. Build artifacts (target/,classes/,obj/,bin/) stay ignored, and the tree is excluded from the linter and formatter, because linting emitted code is linting the emitters at one remove.The other route, measured
spec/BINDINGS-INVESTIGATION.mdmeasures one compiled core plus a hand-written binding per ecosystem, with the binding written the way a package writes it rather than the way a script does: ~1 ns in C#, 27 ns in Python, ~60 ns in Go, Java and Ruby, against a validator body of 47 ns. In Python and Ruby that is 21× and 18× faster than generated source.JavaScript cannot take a binary core — the package is tree shakeable and a wasm module is not — so npm gets generated source whatever the other ecosystems choose. That is why both surfaces come out of one compiler.
What the subset cannot express yet
A survey of the 140 utilities this package ships says four capabilities are missing, and each is a decision rather than a detail:
isBusinessDay,getHolidays,addBusinessDays,getBoletoInfogenerate*utilitiesIntlformatCurrency,clampPrecision,convertCurrencyToWordsnormalizeremoveAccents,sanitizeToAsciiEverything else in the survey is reachable with what is here. The rest of the honest edges are in the README.
Checklist
npm test) — not applicable: no change tosrc/. The new code has its own suites (spec/bridge/conformance/run-all.sh,spec/bridge/conformance/verify-typescript.sh) andnpm teststill passes.npm run checkpasses locally (format, lint, types).npm run build:llmsif I toucheddocs/utilities.md— not applicable,docs/untouched.Additional context
Changes outside
spec/:vite.config.tsgains a lint override forspec/bridge/**/*.tsand keepsspec/bridge/outout of both the formatter and the linter.spec/bridgeis a compiler: its emitters are one wide switch over the IR per language and their product is source code, so a handful of rules that keep library code readable work against them. Each one is disabled with the reason inline. Same pattern as the existingscripts/**andspec/**overrides.CONTRIBUTING.mdgains one paragraph saying whatspec/is and that nothing undersrc/depends on it.Reproducing the bridge needs the toolchains it checks:
node,python3,ruby,go,cargo,javacanddotnet. It does not run in CI today — worth wiring up only if the approach is adopted.This PR previously proposed the four utilities as a stack of four follow-ups (#581–#584). They are closed and collapsed into this one.
🤖 Generated with Claude Code
https://claude.ai/code/session_01UX1gTGeMTyoXQyr1qUoQKd