nandi/rustnimpublic Fork 0
4e4d09dcdd22d17ba510de5639fc3a952ac73f6e
Commits
Clone
git clone https://git.rickub.com/nandi/rustnim.git
git clone ssh://git@rickub.com/nandi/rustnim.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

DESIGN.md · 851 lines · 41.3 KBmarkdown Blame HistoryRaw
Scaffold rustnim: Rust->Nim transpiler, type mapping 1a218c2 nandi 9h ago1# rustnim — a Rust → Nim transpiler
2
3## Status
4
Add display.rs and the alloc half: all of base16ct now goes through afb2a6e nandithebull 7h ago5**Milestone 1 is reached: all of `base16ct` goes through.** Every one of its
6source files transpiles byte-for-byte as published, `alloc` half included, and
7its decode and encode output is byte-identical to rustc's. 33 differential
8cases, 29 behavioural and 4 rejections, plus 6 unit/integration tests. All
9green. Run `cargo test`.
Add trait impls and slice iterators; base16ct's decoder now goes through ae9f986 nandithebull 8h ago10
11Passing today: functions, `impl` methods, trait impls (formatting traits and
12`From`), structs, enums (C-like and data-carrying), `Option`/`Result` with
Add closures and unsafe; base16ct's lower.rs and upper.rs go through 0a375d8 nandithebull 8h ago13`?`, closures, `unsafe`, slice iterators (`iter`/`iter_mut`/`enumerate`/`zip`/`chunks_exact`/
Add trait impls and slice iterators; base16ct's decoder now goes through ae9f986 nandithebull 8h ago14`chunks_exact_mut`/`windows`), borrowed slices as values and return types,
15`let`/`let mut`, the full integer
Add enums, Option/Result, `?`, and the machinery base16ct needs around them b0ccd80 nandithebull 8h ago16and float operator set at exact widths, `as` casts, `if`/`while`/`loop`/`for`,
17`match` including patterns that bind, `Vec`/slices/arrays, type aliases
18(including generic ones), function-typed parameters (`impl Fn(A) -> B`),
19multi-file input, `#[cfg]` evaluation, and `println!`/`format!` with `{}`,
20`{:?}`, `{:x}`, `{:b}`, positional and inline-named arguments, and
Add the differential test runner, and a lowering to measure with it 8ac32af nandithebull 9h ago21zero/space padding.
Scaffold rustnim: Rust->Nim transpiler, type mapping 1a218c2 nandi 9h ago22
23## Why this exists
24
25We tried [tarekwasfy01/Code-Transpiler](https://github.com/tarekwasfy01/Code-Transpiler),
26which advertises `rust` as a source language, on the `base16ct` crate. It emits
27empty files and exits 0. The full investigation is in [`findings/`](findings/)
28and is published at
29https://rickub.com/nandi/code-transpiler-rust-frontend-findings
30
31The decisive finding, and the reason this is a new project rather than a patch:
32its Universal AST cannot represent Rust. `defaultSemanticTypeContract()` in
33`internal/backend/semantic_program.go:85` is hardcoded to
34
35```
36numeric: binary64, integer_width: unknown, truth: r_compatible,
37ownership: unknown, index_base: 1
38```
39
40and `semantic_document.go:1014` *validates* that every contract equals exactly
41that, while `typed_operation.go:46` rejects any value model that is not
42`tagged_dynamic_binary64`. There is no integer width and no ownership in the
43model at all. Code like `base16ct`'s constant-time decoder —
44
45```rust
46ret += (((0x2fi16 - byte) & (byte - 0x3a)) >> 8) & (byte - 47);
47```
48
49— depends on exact 16-bit signed wrapping and arithmetic shift. Lowering that
50into a 1-indexed dynamic float64 model produces silently wrong answers. So the
51first rule of this project is the one that codebase broke:
52
53> **Never approximate a semantic you cannot represent. Fail loudly instead.**
54
55`src/ty.rs` already does this: `i128`/`u128` are rejected with a reason rather
56than widened or truncated.
57
58## Architecture
59
60```
61Rust source ──syn──> syn AST ──lower──> Nim source ──nim c──> binary
62```
63
64**The frontend is `syn`, deliberately.** Hand-rolling a Rust grammar is how the
65other project went wrong; a correct parser is not the interesting part of this
66problem. The interesting part is the lowering, which is where all the work goes.
67
68Planned modules:
69
70| file | role | state |
71|---|---|---|
72| `src/ty.rs` | Rust type → Nim type, exact widths, explicit rejections | written |
Add the differential test runner, and a lowering to measure with it 8ac32af nandithebull 9h ago73| `src/lower.rs` | items, statements, expressions → Nim | written |
74| `src/fmt.rs` | `println!`/`format!` format-string handling | written |
75| `src/prelude.nim` | `Option`/`Result`/panic/`Display`/`Debug` runtime | written |
76| `src/main.rs` | CLI: `rustnim <in.rs> -o <out.nim>` | written |
77| `tests/differential.rs` | the runner described below | written |
78
Add enums, Option/Result, `?`, and the machinery base16ct needs around them b0ccd80 nandithebull 8h ago79### Enums, `Option` and `Result`
80
81A C-like enum becomes a plain Nim `enum`, which compares, orders and
82`case`-checks the way Rust's does. A data-carrying enum becomes a Nim object
83variant — a discriminant enum plus one branch per variant — which is the same
84shape the prelude already uses for `Option` and `Result`. Nim requires the
85branches of a variant object to have distinct field names, so each payload
86field is prefixed with its variant.
87
88`match` takes one of two forms. Arms that neither bind nor destructure become
89a Nim `case`, which is exhaustiveness-checked the way Rust's is. Arms that do
90bind become an `if`/`elif` chain with the bindings emitted as `let`s, because
91Nim's `case` cannot destructure. The chain always ends in an arm that panics:
92Rust proved it unreachable, but Nim cannot see that, and leaving the chain
93open would silently fall through instead.
94
95`Ok`, `Err` and `Some` are emitted with their full type arguments
96(`rsOk[T, E](v)`), because Nim cannot infer `E` from an `Ok(v)` alone. That is
97why the expected type has to reach a `match` arm as well as a `let`.
98
99`?` expands to statements — a temporary, a discriminant test, and an early
100`return` — which are emitted ahead of the line being built. Rust inserts a
101`From::from` on the error there; we accept only the case where the two error
102types already agree, rather than assume a conversion is the identity. `?` in a
103`while` condition is rejected: the early return would run once before the
104loop rather than on each iteration.
105
Add trait impls and slice iterators; base16ct's decoder now goes through ae9f986 nandithebull 8h ago106### Trait impls
107
108A `Display` impl becomes `proc rsDisplay(self: T): string`. Rust's `Formatter`
109is a sink and the observable result of `{}` is exactly the bytes written into
Add display.rs and the alloc half: all of base16ct now goes through afb2a6e nandithebull 7h ago110it, so a write through the formatter **appends** to that string — a `fmt` body
111may write repeatedly, and `UpperHex` writes once per byte in a loop. A body
112that does anything else with the formatter — padding, precision,
113`debug_struct` — is rejected, because those change the output and this model
114does not carry them. `Debug`, `LowerHex`, `UpperHex`, `Binary` and `Octal`
115work the same way.
116
117Writing into a string cannot fail, so `?` on a formatter write is a no-op. `?`
118on anything else inside a `fmt` body *can* fail, and `format!` panics when a
119formatting impl returns an error — so that is what the error branch does, with
120std's own message.
121
122`{:x}` on an integer formats its two's-complement bit pattern; on any other
123type it calls that type's own `LowerHex` impl. Those are different operations,
124so a radix format on an argument of unknown type is rejected rather than
125guessed.
Add trait impls and slice iterators; base16ct's decoder now goes through ae9f986 nandithebull 8h ago126
127`impl From<A> for B` becomes a conversion proc that `.into()` resolves
128through. A marker trait with no items generates nothing: we do not model trait
129resolution anywhere, so there is nothing for it to affect; a use that actually
130needed the trait (a `dyn`, a bound) is rejected where it appears. Any other
131trait impl is rejected.
132
133Methods are keyed by `(receiver type, name)`, not by name alone — two types
134may define the same method, and Nim tells them apart by overload resolution on
135the first parameter.
136
137`fmt::Error` is *not* the same type as a crate's own `Error`. Collapsing a
138qualified path to its last segment merged them, which was a real soundness
139bug; `core::fmt`'s types are now recognised by their qualified name.
140
141### Slice iterators are resolved to one index loop
142
143Rust's slice iterators are lazy and compose. Nim's `for` is over one sequence,
144so a chain of adaptors is resolved into a small IR and emitted as a single
145index loop in which **each binding is an lvalue into the original container**.
146That is what makes `*d = v` through `iter_mut()` write back to the caller's
147slice instead of to a copy, and what lets `chunks_exact(2)` hand out a window
148that indexes straight into the source with an offset.
149
150Only adaptors with an exact index-loop equivalent are accepted. `map`,
151`filter` and `take_while` are rejected rather than partially honoured:
152silently dropping an adaptor would change which elements the loop visits.
153
154`zip` stops at the shorter side, as Rust's does — that is a test, not an
155assumption (`tests/cases/023`).
156
157### Borrowed slices are views, not copies
158
159`&[T]` is a borrow. Nim's experimental view types model exactly that,
160including returning one from a proc: writing through the returned view is
161visible in the original buffer. That was probed against Nim 2.2.4 before being
162relied on, because copying into a `seq` would print the right bytes while
163silently changing aliasing.
164
Add display.rs and the alloc half: all of base16ct now goes through afb2a6e nandithebull 7h ago165Nim does allow a view inside an object and inside an object *field* — both
166probed, both preserving aliasing — so `Result<&[u8], E>` and
167`HexDisplay<'a>(&'a [u8])` both work. (An earlier version of this document
168claimed otherwise; that was wrong.)
169
170Two real constraints remain. Nim will not let a `let` borrow out of a local,
171so `.unwrap()`/`.expect()` on a `Result` holding a view is expanded inline and
172the binding becomes an alias — a view is a reference, so there is nothing to
173materialise, and the substituted expression is a plain field access that
174re-evaluates nothing. And `s.get(a..b)` is an `Option` of a view whose
175*validity* is what matters: the view and its condition travel together through
176`ok_or` until a `?` or `unwrap` resolves them into a bounds check plus a
177binding. Keeping such an `Option` in a variable is rejected with a message
178saying so.
179
180A `let` binding a borrow keeps the view rather than copying into a `seq`:
181`let res = encode(..)?` names the caller's buffer, and copying would print the
182right bytes while silently breaking the aliasing.
Add trait impls and slice iterators; base16ct's decoder now goes through ae9f986 nandithebull 8h ago183
Add closures and unsafe; base16ct's lower.rs and upper.rs go through 0a375d8 nandithebull 8h ago184### Closures and `unsafe`
185
186`unsafe` is a permission marker, not a semantic change: it does not alter what
187the enclosed operations mean. So the block is transparent, and every operation
188inside still goes through the ordinary lowering and is still rejected if it has
189no faithful mapping. `unsafe fn` lowers like any other proc.
190
191A closure becomes a Nim anonymous proc. Nim's closures capture by reference, as
192Rust's non-`move` closures do; a `move` closure captures by value, which is a
193different thing, so it is rejected rather than lowered to the same construct.
194`impl Fn(A) -> B` is left at Nim's default calling convention, which accepts
195both a plain top-level proc and a capturing closure — as Rust's `impl Fn` does.
196
197`.map`/`.and_then` over an `Option`/`Result` are expanded inline with the
198closure's parameter aliased to the payload, rather than handed to a generic
199proc. That keeps the whole thing an expression and keeps a view a view.
200
201`&str` is a borrowed view of someone else's bytes, so it maps to
202`openArray[char]`, not to an owned `string`. Nim accepts a `string` argument
203for an `openArray[char]` parameter, so a literal still passes straight through.
204`from_utf8_unchecked` reinterprets a byte view as a character view over the
205same memory — no copy, no validation, and writes through the original are
206visible, as in Rust.
207
208### Modules
209
210Rust keeps `lower::decode` and `mixed::decode` apart by module; flattening into
211one Nim module would merge them — they are *different functions*. So the first
212input is the crate root and each later one is a module named by its file stem,
213items are emitted as `<module>_<name>`, and a call resolves through an explicit
214qualifier, then the current module, then what `use` brought into scope, then
215the root.
216
Add generics; transpile the part of cosmic-theme that is reachable ff34e1b nandithebull 6h ago217### Generics
218
219Rust type parameters become Nim's. Nim instantiates a generic structurally at
220the call site much as Rust does, so `fn f<T>(x: T) -> T` has a direct target in
221`proc f[T](x: T): T` and no monomorphisation pass is needed.
222
223**Trait bounds and `where` clauses are dropped.** That is sound in the
224direction that matters: an operation the bound permitted either exists for the
225instantiated type or is a compile error at that instantiation site. Dropping a
226bound cannot make an accepted program mean something different — it only makes
227rustnim accept some programs rustc would reject, which does not matter when
228the input is known-good Rust. (Where it *would* matter is bound-directed
229method selection, e.g. blanket impls choosing between candidates. We do not
230model trait resolution at all, so such a program is rejected elsewhere.)
231
232Const generic parameters have no Nim equivalent and are still rejected.
233
234Two things need more than a rename. Nim cannot infer an object's generic
235parameters from a constructor's field values, so `Pair { a: 1, b: 2 }` is
236emitted as `Pair[int32](...)` using the expected type — and a generic enum's
237unit variant (`Holder::Empty`) likewise. And a binding's annotation cannot
238name a parameter Nim is still inferring, so call sites run a small unifier:
239the callee's declared parameter types are matched against the actual argument
240types to bind `T`, and the result is substituted into the return type.
241
Add trait impls and slice iterators; base16ct's decoder now goes through ae9f986 nandithebull 8h ago242### Declaration order
243
244Rust has no declaration-before-use rule and Nim does, so every proc is
245forward-declared between the type definitions and the bodies. Reordering the
246input instead would not handle mutual recursion.
247
Add the differential test runner, and a lowering to measure with it 8ac32af nandithebull 9h ago248### Type propagation is load-bearing
249
250Rust infers an unsuffixed integer literal's type from context and falls back
251to `i32`; Nim falls back to 64-bit `int`. So `lower.rs` threads an *expected
252type* down through every expression — into `let` annotations, call arguments,
253`match` patterns, compound assignments and both operands of a binary — and
254annotates every binding it emits. Without that, `let x: u8 = 200; x + 100`
255means two different things in the two languages. With it, a width the lowering
256gets wrong becomes a Nim compile error (a loud failure, reported by the
257runner) rather than a wrong answer.
Scaffold rustnim: Rust->Nim transpiler, type mapping 1a218c2 nandi 9h ago258
259## Mapping decisions made so far
260
261- **Integers**: exact width. `i32``int32`, `usize``uint`, etc. `i128`/`u128`
262 rejected.
263- **Indexing**: both 0-based. Direct.
264- **`&T`** → plain value. **`&mut T`** → `var T` parameter.
265- **`&[T]`** → `openArray[T]` in parameter position, `seq[T]` when owned.
266 `Nim::owned()` performs that conversion.
267- **Ownership/borrowck**: ignored. Nim is GC'd; for safe Rust this is sound.
268- **`Option`/`Result`** → object variants in the prelude.
269- **`match`** → Nim `case` where the arms are simple, `if`/`elif` when arms have
270 guards or bindings.
271- **Rust's expression-orientation** maps well: Nim `if`/`case` are expressions
272 too, and a proc's trailing expression is its return value.
273
Settle signed-shr and unsigned-wrap semantics against both compilers 87c9cc8 nandi 9h ago274### Settled empirically (Nim 2.2.4 vs rustc 1.98.1, both run)
275
2761. **Nim's `shr` on a signed integer is arithmetic**, matching Rust.
277 `int16(-256) shr 8` = `-1` in Nim; `(-256i16) >> 8` = `-1` in Rust.
278 `base16ct`'s decoder depends on this, so it maps directly with no helper.
2792. **Nim's fixed-width unsigned arithmetic wraps silently**, matching Rust's
280 `wrapping_*`. `uint8(200) + 100` = `44` in Nim; `200u8.wrapping_add(100)`
281 = `44` in Rust. So `wrapping_add` on an unsigned type is just `+`.
282
Add the differential test runner, and a lowering to measure with it 8ac32af nandithebull 9h ago2833. **We model rustc's debug profile.** Rust debug builds panic on signed
284 integer overflow; Nim's default build raises `OverflowDefect` on it. Those
285 are the matching pair, so the runner invokes `rustc` without `-O` and `nim
286 c` with its defaults, and `tests/cases/016` pins the behaviour. A Rust
287 panic exits 101 where a Nim Defect exits 1, so every generated module ends
288 with a handler that maps one to the other — otherwise the runner's
289 exit-status comparison would be vacuous. `wrapping_*` is therefore an
290 explicit operation on both sides: unsigned maps to the bare operator (item
291 2), signed is routed through the unsigned view of the same width.
2924. **`char` round-trips.** Rust `char` → Nim `Rune`, confirmed for ASCII and
293 non-ASCII scalars in both `{}` and `{:?}`, and across `as u32`
294 (`tests/cases/014`).
Prove byte-identity for base16ct by enumerating whole input domains 99b8376 nandithebull 7h ago2955. **Nim's integer conversion `T(x)` truncates; it does not range-check.**
296 `uint8(511'u16)` is `255`, `uint8(300'i32)` is `44`, `uint8(-1'i32)` is
297 `255` — the same answers as `cast[uint8]`. An earlier version of this
298 document asserted that `T(x)` range-checks, and used that to justify
299 `cast`. The conclusion stands — `cast` is the clearer spelling of
300 "truncate" — but the stated reason was wrong, and it had been assumed
301 rather than probed. Found by sabotaging the cast lowering and watching the
302 exhaustive proof *not* fail, which is what a sabotage test is for.
Add the differential test runner, and a lowering to measure with it 8ac32af nandithebull 9h ago303
Settle signed-shr and unsigned-wrap semantics against both compilers 87c9cc8 nandi 9h ago304### Still open
305
Add generics; transpile the part of cosmic-theme that is reachable ff34e1b nandithebull 6h ago3066. Const generic parameters, `move` closures, closure bodies with statements,
Lower `extern "C"` blocks to Nim importc; do not transpile libc 7db7991 nandithebull 5h ago307 trait objects and `macro_rules!` definitions are rejected with a reason. A `trait` declaration lowers to nothing — trait resolution
Add associated types and consts, and test whether the blocker stack bottoms out 8354895 nandithebull 6h ago308 is not modelled — but one giving a method a *default body* is rejected,
309 since that body is code with no impl to be emitted into.
Add trait impls and slice iterators; base16ct's decoder now goes through ae9f986 nandithebull 8h ago310 Lifetime parameters are *not* a rejection: they carry no runtime meaning
311 and Nim is GC'd, so `fn encode<'a>(..)` lowers fine.
Add generics; transpile the part of cosmic-theme that is reachable ff34e1b nandithebull 6h ago3127. `saturating_*` and `checked_*` are implemented, detecting overflow on the
313 unsigned view of the same width rather than with a range check that would
314 itself trap. `wrapping_*`, `overflowing_*` and `strict_*` are not all
315 covered: only add, sub and mul have the saturating and checked forms.
Prove byte-identity for base16ct by enumerating whole input domains 99b8376 nandithebull 7h ago3168. Float formatting matches Rust for ordinary values and for `inf`/`NaN`, but
Add the differential test runner, and a lowering to measure with it 8ac32af nandithebull 9h ago317 the exponent-form thresholds have only been checked at `1e21`.
Prove byte-identity for base16ct by enumerating whole input domains 99b8376 nandithebull 7h ago3189. Functions are scoped by module now, but *types* are still global: two
Add closures and unsafe; base16ct's lower.rs and upper.rs go through 0a375d8 nandithebull 8h ago319 modules declaring the same type name would collide. Relatedly, a crate's
320 own `type Result<T>` is told apart from the builtin `Result<T, E>` by
321 arity, which is not how Rust resolves it.
Evaluate host cfg predicates, and measure what that actually buys 0e6c394 nandithebull 6h ago32210. Host `#[cfg]` predicates — `unix`, `windows`, `target_os`, `target_arch`,
Lower the `log` facade, and add explicit enum discriminants 12c0a01 nandithebull 5h ago323 `target_family`, `target_pointer_width`, `target_endian`,
324 `target_has_atomic` — are evaluated
Evaluate host cfg predicates, and measure what that actually buys 0e6c394 nandithebull 6h ago325 against the machine, since the generated Nim is compiled for it. That makes
326 the output host-shaped: a crate branching on platform has had that branch
327 decided at transpile time. `doc`/`doctest`/`miri` are false. A custom or
328 build-script `cfg` (`crossbeam_loom`, `target_has_atomic`) has no value we
329 could know and is rejected.
Transpile a second crate, adler2, to test whether any of this generalises 5fdd5be nandithebull 7h ago33011. Associated types (`impl Iterator { type Item = .. }`) and `mod`
331 directories (`specialized/mod.rs`) are not implemented.
33212. `String::from_utf8_unchecked` copies, because Nim's `string` is an owned
Add display.rs and the alloc half: all of base16ct now goes through afb2a6e nandithebull 7h ago333 value. Rust's consumes the `Vec` without copying. Observably the same from
334 the caller, but it is a copy where Rust has none.
Scaffold rustnim: Rust->Nim transpiler, type mapping 1a218c2 nandi 9h ago335
Transpile a second crate, adler2, to test whether any of this generalises 5fdd5be nandithebull 7h ago336## A second crate: does this generalise, or is it fitted to `base16ct`?
337
338`base16ct` is the crate this was built toward, so passing it proves less than
339it looks. `adler2` 2.0.1 was picked as a deliberately different shape —
340a stateful struct with methods, operator-overload trait impls, a hand-unrolled
341four-lane inner loop — and it now works: `tests/cases/029-adler2-crate/`
342transpiles `algo.rs` byte-for-byte as published, with `lib.rs`'s items and a
343driver, and its checksums are byte-identical to rustc's across every single
344byte, every length to 600 (crossing the 4-byte unrolling boundary and the
3455552-chunk path), and 144 incremental-write splits.
346
347It needed real work, which is the honest part of the answer. Ten features:
348trait impls generalised beyond formatting and `From` (any trait's methods
349become procs on the type, with the operator traits wired into `+=`/`+`
350dispatch), `Self`, `Type::method()` static calls, `u32::from` between
351primitives, tuple-destructuring `let`, `split_at`, iterators bound to
352variables and `.remainder()`, `[0; 4]` as an array rather than a `seq`, and
353the bare `#[cfg]` flags.
354
355It also caught a **regression I had introduced**: the three-phase emission
356added for forward declarations was silently dropping `const` items declared
357*inside* a function body. `base16ct` has none, so 33 passing cases said
358nothing about it.
359
360### What the other crates did
361
362Run without fixing anything, to see where the wall is rather than to move it:
363
364| crate | outcome |
365|---|---|
366| `adler2` 2.0.1 | **works**, byte-identical |
367| `siphasher` 1.0.1 | rejected: `u128` |
368| `rustc-hash` 2.1.1 | rejected: `u128` |
369| `hex` 0.4.3 | rejected: `impl Iterator` needs an associated type |
370| `crc32fast` 1.5.0 | rejected: directory modules (`specialized/mod.rs`), then SIMD intrinsics |
371
372Two of the five stop at `u128`, which is the founding rule doing its job
373rather than a gap: they are told they cannot be translated instead of being
374handed a silently truncated hasher. The other two are honest missing
375features — associated types, and `mod` directories.
376
Evaluate host cfg predicates, and measure what that actually buys 0e6c394 nandithebull 6h ago377## How far off is a crate like `libcosmic`?
378
379Measured, not guessed. Running rustnim over `libcosmic`'s own `src/`:
380
381```
3820 of 164 files produce any translation
38350,633 lines, 112 direct dependencies
384```
385
386with 66 generic-parameter blockers, 20 trait objects, 51 `async` uses, 235
387`where` clauses, 73 associated types and 1,104 lifetime annotations. Those are
388not features the crate happens to use; they are its architecture. `libcosmic`
389is a north star, not a next step.
390
391### The blocker survey, and what it says about roadmaps
392
393400 crates from the local registry (under 4,000 lines each), all their module
394files passed together, first blocker recorded:
395
Add associated types and consts, and test whether the blocker stack bottoms out 8354895 nandithebull 6h ago396| blocker | start | +host-`cfg` | +generics | +assoc types |
397|---|---|---|---|---|
398| unevaluable `#[cfg]` | 124 | 34 | 34 | 34 |
399| generic type parameter | 69 | 91 | **1** | 1 |
400| associated types in an `impl` | 54 | 63 | 87 | **2** |
401| unsupported type | 20 | 29 | 60 | 71 |
402| trait object | 17 | 25 | 30 | 35 |
403| macro definition | 21 | 24 | 25 | 32 |
404| raw pointer | 12 | 22 | 24 | 28 |
405| **crates fully transpiled** | **2** | **2** | **2** | **3** |
Add generics; transpile the part of cosmic-theme that is reachable ff34e1b nandithebull 6h ago406
407This has now happened twice. Evaluating the host `#[cfg]` predicates cleared
40890 of 124 blockers and moved the fully-working count by zero. Generics then
409cleared 90 of 91 and moved it by zero again. Every crate each unblocked simply
410hit its next blocker.
411
412That is the shape of the problem: blockers are **deep, not wide**. A frequency
413ranking of *first* blockers is not a roadmap — it says which feature is most
414often first, not which one finishes a crate. `base16ct`, `adler2` and
415`cosmic-theme`'s spacing model work because their whole stack was ground
416through, one blocker at a time.
Evaluate host cfg predicates, and measure what that actually buys 0e6c394 nandithebull 6h ago417
Add associated types and consts, and test whether the blocker stack bottoms out 8354895 nandithebull 6h ago418### Does the stack have a bottom?
419
420If the stacks are drawn from one shared finite pool of language features, then
421clearing the pool clears every stack at once, and the flat counts above are
422just what progress looks like before a step change. That is a real possibility
423and it is testable, so it was tested. Sampling every *file* of the 400 crates
424rather than only each crate's first blocker — 1,766 blocker observations:
425
426| | count | bounded? |
427|---|---|---|
428| distinct normalised blocker kinds | **63** | **yes — this is the language-feature pool** |
429| distinct unsupported std methods | 80 | no — this is `std`'s surface |
430| distinct unknown functions | 75 | no — these are calls into dependencies |
431| distinct unsupported macros | 13 | no |
432
433So the answer is *both*, split by class:
434
435- **Language features do bottom out.** 63 distinct kinds, and each one cleared
436 is cleared for every crate forever. This part is finite and the step-change
437 intuition is correct for it. Associated types produced the first net gain
438 (2 → 3), which is what that dynamic looks like starting.
439- **The API surface does not.** 80 distinct `std` methods appeared in 1,766
440 samples of *small* crates; `std` has thousands of items, and each needs a
441 verified Nim equivalent rather than a guess. That is enumerable but it has
442 no bottom you reach by clearing features.
443- **Dependencies are not a pool at all.** 315 of the 400 crates depend on
444 other crates, which have to be transpiled too, recursively, until the graph
445 ends at `libc`, proc macros or SIMD intrinsics — which it does, and those do
446 not translate.
447
448And among the 85 dependency-free crates, 1 of 84 currently transpiles in full.
449So even with dependencies removed from the picture, features alone are not the
450only remaining gate.
451
Evaluate host cfg predicates, and measure what that actually buys 0e6c394 nandithebull 6h ago452So the ranking above is not a roadmap — it says which feature is most often
453*first*, which is not the same as which feature finishes a crate. The only
454honest way to add a crate is to pick it and clear its stack, as was done
455twice.
456
Add associated types and consts, and test whether the blocker stack bottoms out 8354895 nandithebull 6h ago457Generics and associated types are now done. By frequency the next are
458unsupported types (71), trait objects (35) and `macro_rules!` definitions (32)
459— but see above before treating that as a plan.
Add generics; transpile the part of cosmic-theme that is reachable ff34e1b nandithebull 6h ago460
461### `cosmic-theme`: what was reachable
462
463`tests/cases/032-cosmic-theme-spacing/` transpiles `corner.rs`, `spacing.rs`
464and `layout.rs` from `cosmic-theme` 1.0.0 — the spacing scale, corner radii
465and density model a COSMIC-native UI needs to match the desktop — with output
466byte-identical to rustc's, including the `Density`/`Spacing` and
467`Roundness`/`CornerRadii` round trips.
468
469Those files are the crate's own, with one mechanical change recorded here: the
470`use serde::{Deserialize, Serialize}` line and the `Serialize, Deserialize`
471entries in two `derive` lists were removed, because the oracle is plain
472`rustc` with no dependencies available. Nothing else was touched; rustnim
473ignores both anyway.
474
475The rest of `cosmic-theme``theme.rs` (1,830 lines), `color.rs`,
476`cosmic_palette.rs`, `derivation.rs`, `steps.rs`, `composite.rs` — is colour
477work built on `palette` (40,874 lines across 122 files, plus a proc-macro
478crate). `mode.rs` needs `cosmic-config` and its derive macro. Those are
479dependency walls, not language gaps.
Evaluate host cfg predicates, and measure what that actually buys 0e6c394 nandithebull 6h ago480
Lower `bitflags!` directly, checked against the real crate af6e50f nandithebull 5h ago481## `bitflags!`, and why it is lowered rather than expanded
482
483`bitflags` is the most-depended-on translatable crate in `libcosmic`'s
484resolved tree — 79 of its 741 crates — so it is the highest-leverage target
485there. It is also 26 `macro_rules!` definitions across five files, which is
486the one thing the lowering cannot represent.
487
488Expanding the macro does not rescue this. `RUSTC_BOOTSTRAP=1 cargo rustc --
489-Zunpretty=expanded` on a single-flag user produces 869 lines that still call
490`bitflags::{Bits, Flag, Flags, iter::Iter, iter::IterNames, parser::from_str,
491parser::to_writer}` — items defined by those same macros. The chain does not
492end in code we could lower.
493
494What the macro *means*, though, is small and stable: a newtype over an integer
495with named constants and set operations. So `src/macros.rs` parses the
496invocation and the lowering emits that directly. This is a deliberate
497exception to "a macro whose expansion is not known is rejected", and the
498argument is that the expansion *is* known here — it is documented, stable, and
499now pinned by a test.
500
501`tests/cases/034-bitflags.rs` is that test, and it is unusual: `//@ extern:
502bitflags` makes the **oracle** compile against the real crate while rustnim
503gets no such crate. rustnim has to reproduce bitflags' behaviour without it,
504and the outputs are compared byte for byte. Two behaviours it pins that a
505reimplementation would get wrong:
506
507- `!x` is complemented and then **masked to `all()`**`!(READ|WRITE)` is
508 `EXEC`, not `0xFFFFFFFC`.
509- `from_bits` returns `None` for any bit outside `all()`;
510 `from_bits_truncate` masks instead.
511
512plus the `Debug` spelling, which is `Perms(READ | WRITE)` and `Perms(0x0)`.
513
514The risk this carries is version drift: a future `bitflags` could change what
515the macro generates, and the shim would not know. The test is what would catch
516it, which is why it links the real crate rather than a copy of its
517documentation.
518
Lower the `log` facade, and add explicit enum discriminants 12c0a01 nandithebull 5h ago519## `log`, on the same argument
520
521`log` is second by dependents in `libcosmic`'s tree (61 of 741), and it has
522the same shape as `bitflags`: `src/macros.rs` is 20 `macro_rules!`, and that
523is what the dependents use. `src/lib.rs` is the facade — `Level`,
524`LevelFilter`, the `Log` trait behind a `&'static dyn`, atomics and
525`set_logger`.
526
527So the macros are lowered directly, against behaviour pinned from the real
528crate by `tests/cases/035-log.rs` (again `//@ extern: log`, so the oracle
529links it and rustnim does not). What that test pins:
530
531- `max_level()` starts at `Off`.
532- `log_enabled!` is **false even after `set_max_level`** when no logger is
533 installed, because the facade consults the logger as well as the level.
534- `Display` for a level is upper-case (`WARN`), `Debug` is not (`Warn`).
535- `Level::Error as usize` is 1 through `Trace` as 5; `LevelFilter::Off` is 0.
536
537A record's arguments are not evaluated when the level is disabled, so the
538lowering emits `if rsLogEnabled(l): rsLog(l, ..)` rather than computing the
539message first.
540
541**The boundary is deliberate: rustnim models log's *emitting* side, not its
542installing side.** A transpiled library's `info!` calls work and, with no
543logger, do nothing — which is exactly Rust's behaviour. Installing a logger
544is an application's job and is done from Nim:
545
546```nim
547rsLogSetLogger(proc (level: RsLogLevel, target, msg: string) =
548 echo "[", rsDisplay(level), "] ", msg)
549rsLogMaxLevel = int(rsLvlTrace)
550```
551
552Modelling `impl Log` instead would mean reproducing `Record` and `Metadata`,
553which is more shim surface for something a Nim application would not write in
554Rust anyway.
555
556The facade's types are emitted as `RsLogLevel` and `RsLogFilter`, not `Level`
557and `LevelFilter`. The first attempt used Rust's names and broke six existing
558cases, because a crate's own `Error` type and an enum field named `Error`
559cannot coexist in one Nim module.
560
Lower `extern "C"` blocks to Nim importc; do not transpile libc 7db7991 nandithebull 5h ago561## `libc`: not transpiled, and should not be
562
563`libc` is third by dependents in `libcosmic`'s tree (60 of 741), but it is not
564a crate to translate. Measured:
565
566```
567129,594 lines across 387 files
568 54,544 `pub const`
569 7,660 `pub fn`, nearly all inside `extern "C"` blocks -- declarations
570 1,926 `pub type`
571 121 actual function bodies in the entire crate
572```
573
574There is essentially no code in it. It is a set of declarations binding to the
575platform's C library, and **Nim reaches those same symbols natively** — the
576symbols are the same objects, not two implementations of one idea.
577
578So what was built is the general capability instead: an `extern "C"` block
579lowers to Nim `importc` declarations. Both are statements *about* a symbol
580someone else defines, and both are bound by the C ABI, so the two declarations
581describe one symbol rather than one being a translation of the other. Raw
582pointers map too (`*mut T` to `ptr T`), which cleared all 38 raw-pointer
583blockers in the survey.
584
585### `const` matters at the C level even though it does not at the Rust one
586
587Rust emits no C prototype; Nim emits a real one. So declaring `strlen` as
588taking `*const u8` produces:
589
590```
591error: conflicting types for 'strlen'; have 'NU(char *)'
592note: previous declaration with type 'size_t(const char *)'
593```
594
595which is the C compiler catching a declaration that does not match the symbol
596— a loud failure, and a better outcome than Rust's silence. To make honest
597declarations expressible, `*const T` maps to a generated const-qualified
598alias:
599
600```nim
601type RsConstPtrcchar* {.importc: "const char *", nodecl.} = distinct pointer
602proc strlen*(s: RsConstPtrcchar): uint {.importc: "strlen", cdecl.}
603```
604
605one per element type actually used. Nim's generic form (`importc: "const $1*"`)
606was tried first and does not work in 2.2.4. A `*const T` whose C spelling we
607do not know is rejected rather than declared without the `const`.
608
609`tests/cases/036-extern-c.rs` calls `abs`, `labs`, `strlen` and `atoi` through
610this path, byte-identical to rustc.
611
Expand `macro_rules!` rather than translating it to a Nim template 04eb29f nandithebull 5h ago612## `macro_rules!`: expanded, not translated
613
614Nim has `template` and `macro`, so a shape-level correspondence with
615`macro_rules!` exists. Across 1,833 definitions in a 400-crate sample:
616
617| shape | share | Nim equivalent |
618|---|---|---|
619| single rule, no repetition | 36% | a `template` |
620| multiple rules | 12% | a `macro` dispatching on shape |
621| `$(..)` repetition | 28% | `varargs` in a `macro` |
622| `:tt` token-tree munching | 22% | an interpreter; not mechanical |
623
624So ~76% has a translatable shape. **We expand instead, and the reason is the
625type-directed lowering.** A Nim template body is *untyped*: substituted first,
626type-checked after. This lowering needs a type at nearly every step — to
627choose `div` over `/`, to size a `cast`, to pick an integer literal's width.
628Translating a macro body would mean lowering Rust with no type information,
629which is exactly the guessing the project refuses. Expanding at the call site
630yields ordinary Rust in a context where the types are known, so it lowers like
631anything else. Same applicability, faithful output.
632
Expand multi-rule and recursive macro_rules d459281 nandithebull 4h ago633`src/mrules.rs` handles any number of rules, with `$(..)` repetition.
634Measured against the 1,833 definitions in the sample:
Expand `$(..)` repetition in macro_rules 3d4a7d2 nandithebull 4h ago635
Expand multi-rule and recursive macro_rules d459281 nandithebull 4h ago636| | count | |
637|---|---|---|
638| **expandable** | **1,369** | **74%** |
639| nested `$(..)` inside `$(..)` | 44 | 2% |
640| `:tt` token-tree munching | 420 | 22% |
641
642Rules are tried top to bottom and the first whose matcher matches wins, which
643is Rust's rule and is also what makes a recursive macro terminate — a base
644case sits above the recursive rule. Recursion falls out for free: an expansion
645containing another macro call is lowered like any expression, so it expands
646again, with a 64-level limit so a non-terminating macro is an error rather
647than a hang.
Expand `$(..)` repetition in macro_rules 3d4a7d2 nandithebull 4h ago648
649A definition it cannot handle is recorded *with its reason*, so a call site
650says "`foo!` cannot be expanded: nested `$(..)` repetition is not implemented
651yet" rather than "unknown macro". Captured fragments are parenthesised on
652substitution, so `square!(2 + 3)` is 25 and not 11.
653
Expand multi-rule and recursive macro_rules d459281 nandithebull 4h ago654Three things here are easy to get wrong, all caught by `tests/cases/038` and
655`039` rather than by reasoning:
Expand `$(..)` repetition in macro_rules 3d4a7d2 nandithebull 4h ago656
657- `>` closes `=>` and `->` as well as a generic argument list, so the nesting
658 depth used to find a fragment's end must not go negative. Without that,
659 `$($a:expr => $b:expr),*` swallows the whole invocation.
660- A repetition's separator may be *inside* the pattern rather than between
661 iterations — `$first:expr $(, $rest:expr)*` has no separator, its comma
662 leads each iteration. A trailing fragment therefore stops at the separator
663 when there is one and at whatever starts the next iteration when there is
664 not.
Expand multi-rule and recursive macro_rules d459281 nandithebull 4h ago665- **A fragment never spans a top-level `,` or `;`.** Without that, a greedy
666 `$x:expr` swallows the whole invocation and `describe!(3, 4)` matches the
667 *one-argument* rule, silently taking the wrong branch. A comma inside
668 brackets is within a `Group` token and so is not at this level at all,
669 which is why the rule is safe.
Expand `$(..)` repetition in macro_rules 3d4a7d2 nandithebull 4h ago670
671A block expression with statements (`{{ let mut m = ..; m }}`, which is how
672these macros are usually written) now lowers to Nim's `block:` expression
673rather than being hoisted, so the same macro expanded at two call sites does
674not collide in one scope.
675
676**Expansion moved the survey more than everything before it combined: 3 → 18
677of 400 crates accepted.** A `macro_rules!` used to be a hard stop at item
678level, failing a whole crate on sight.
679
Lower argument-position `impl Trait`; it was a generic parameter all along 4e4d09d nandithebull 4h ago680Repetition, multi-rule and argument-position `impl Trait` then each moved it
681by **zero** — 18 before, 18 after, 12 compilable throughout — for the reason every previous feature did:
Expand multi-rule and recursive macro_rules d459281 nandithebull 4h ago682the crates they unblocked hit their next blocker. That is now eight features
683running. The capabilities are real and tested; the crate count is gated by
684something else, and the remaining blockers say what: unsupported types (86)
685and calls into dependencies (49), which is the unbounded tail rather than the
686finite feature pool.
Expand `macro_rules!` rather than translating it to a Nim template 04eb29f nandithebull 5h ago687
688### A stricter number
689
690"rustnim exits 0" is not "the output is real". Of those 18, **12 produce Nim
691that the Nim compiler accepts**:
692
693```
694adler2 arrayref cfg_aliases ×3 cfg-if ×2 ctor-lite darling ×4
695```
696
697Compiling is still not behaving: only the cases in `tests/cases/` are checked
698against rustc for identical output. Three numbers, in increasing strength —
699accepted 18, compiles 12, behaviourally verified only the corpus.
700
701## `serde`: assessed, not attempted
702
703`serde` is 49 dependents in `libcosmic`'s tree and the most generic crate
704looked at here: 17,237 lines, 369 `impl<`, 922 `where` clauses, 849 uses of
705`'de`, 324 associated types. `serde_derive` is another 8,975 lines of *proc
706macro* — a program that runs at compile time, so it can be expanded (as
707`bitflags!` was, with `RUSTC_BOOTSTRAP=1`) but never translated.
708
709The derive's expansion is small and clean — thirteen lines for a two-field
710struct. But it is **generic over a `Serializer`**, so unlike `bitflags!`
711(self-contained) and `log` (a facade with a defined no-op default), it has no
712observable behaviour at all until a format crate supplies one. A shim would
713therefore have to pick a format and implement *that*, which is a narrower and
714much larger commitment than either previous shim.
715
716`serde_json`'s exact output was pinned for whenever that is attempted:
717declaration order, no whitespace, `null` for `None`, and a float keeps the
718`.0` that `Display` drops — `{"x":-3,"ratio":2.0,"maybe":null}`.
719
Lower argument-position `impl Trait`; it was a generic parameter all along 4e4d09d nandithebull 4h ago720## What "unsupported type" was hiding
721
722The survey's largest remaining category was 86 crates blocked on "unsupported
723types". Opening it: **every one was the same message**, `unsupported
724\`impl Trait\` type` — one construct, not a category. The label was doing no
725work and the message named neither the trait nor the position.
726
727Counting `impl Trait` in the sample splits it decisively:
728
729| | count | |
730|---|---|---|
731| argument position, `x: impl T` | 2,268 | **is a generic parameter** |
732| return position, `-> impl T` | 690 | an opaque type |
733
734Argument position *is* Rust's own desugaring of a generic: `fn f(x: impl T)`
735is `fn f<A: T>(x: A)`. Bounds are already dropped (see Generics), so what is
736left is a fresh parameter, and Nim instantiates it structurally at the call
737site exactly as Rust does. That is now what happens — 86 blockers down to 59.
738
739Return position is genuinely different: the caller cannot name the type, and
740Nim has no equivalent. It is still rejected, but the message now says which
741trait and that argument position would have been fine.
742
743### Trait methods are named by trait, not by type
744
745Making that work required a change with its own reasoning. A trait method's
746proc was `rs<Trait>_<Type>_<method>`, which cannot be called on a generic
747receiver — there is no type to put in the name yet. It is now
748`rs<Trait>_<method>`, so every impl of one trait method shares a name and Nim
749overloads on the first parameter, which is how the call resolves at
750instantiation. Two traits declaring the same method still cannot collide,
751because the trait is in the name.
752
753The exception is a trait method with **no receiver**. `Default::default()`
754takes none, so two impls would differ only in return type, which Nim cannot
755resolve — `overloaded 'rsDefault_default' leads to ambiguous calls`. Those
756keep the type in the name. The `bitflags!` shim's operators were renamed to
757match, since they are trait methods with a receiver.
758
Prove byte-identity for base16ct by enumerating whole input domains 99b8376 nandithebull 7h ago759## Proof of byte-identity for `base16ct`
760
761[`PROOF.md`](PROOF.md) sets out what is actually established: exhaustive
762agreement over every two-byte decode input (65,536), every two-byte encode
763input (65,536), every single byte through `encode_str` and `HexDisplay`, and
764every length to 128 — plus a compositional argument extending those to inputs
765of any length, and 20,000 pseudorandom multi-chunk cases attacking the one
766step in that argument that is inspection rather than enumeration. Run it with
767`cargo test --test proof`. It is explicit about the difference between the
768exhaustive parts and the sampled ones.
769
Scaffold rustnim: Rust->Nim transpiler, type mapping 1a218c2 nandi 9h ago770## Testing: differential, not golden
771
772The bar is **behavioural equivalence with rustc**, not that the output looks
773plausible. For each case in `tests/cases/`:
774
775```
776rustc case.rs && ./case > expected
777rustnim case.rs -o case.nim && nim c -r case.nim > actual
778diff expected actual
779```
780
781A case only counts as passing when both binaries build *and* produce identical
Add the differential test runner, and a lowering to measure with it 8ac32af nandithebull 9h ago782stdout *and* exit with the same status.
783
784`tests/differential.rs` implements this, and checks each stage separately so a
785failure says where it went wrong: `rustnim`, `rustc`, `nim`, or `diff`. Three
786guards exist specifically because of how the other transpiler failed:
787
788- `rustnim` exiting 0 while writing **no output file** is a failure.
789- `rustnim` exiting 0 while writing an **empty output file** is a failure.
790- An **empty corpus** is a failure, so the runner cannot pass by finding
791 nothing to do.
792
793All three have been verified by deliberately breaking the transpiler and
794confirming the runner goes red.
795
796Cases carry directives in leading `//@` comments:
797
798| directive | meaning |
799|---|---|
800| `//@ reject: <substring>` | `rustnim` must *fail*, with this in its message |
801| `//@ skip: <reason>` | not run; reported as skipped |
802| `//@ args: <argv>` | passed to both binaries |
803| `//@ stdin: <line>` | fed to both binaries |
Lower `bitflags!` directly, checked against the real crate af6e50f nandithebull 5h ago804| `//@ cfg: feature=<name>` | passed to rustnim, and to rustc as `--cfg feature="<name>"` |
805| `//@ extern: <crate>` | the **oracle** links this crate; rustnim does not get it |
Add the differential test runner, and a lowering to measure with it 8ac32af nandithebull 9h ago806
807`reject` cases are how the "fail loudly" rule is tested rather than merely
808stated: `900`–`904` pin the rejections of `i128`, an unmapped standard-library
809method, a float→int cast, an unimplemented format spec, and a closure.
810
811Run one case with `RUSTNIM_CASE=005 cargo test --test differential --
812--nocapture`. Nim is found at `.nim-toolchain/bin/nim` in the repository root
813or any parent, or via `RUSTNIM_NIM`.
Scaffold rustnim: Rust->Nim transpiler, type mapping 1a218c2 nandi 9h ago814
815## Toolchain
816
817- `rustc` / `cargo` 1.98.1 — system.
818- Nim 2.2.4 — vendored at `.nim-toolchain/` (gitignored; downloaded from
819 nim-lang.org, not installed system-wide). Binary: `.nim-toolchain/bin/nim`.
820
821## Milestone 1
822
823Transpile `base16ct` 1.0.0 — the crate the other transpiler failed on — and
Add enums, Option/Result, `?`, and the machinery base16ct needs around them b0ccd80 nandithebull 8h ago824have its decoder produce byte-identical output to the Rust original.
825
Add display.rs and the alloc half: all of base16ct now goes through afb2a6e nandithebull 7h ago826**Reached.** `tests/cases/026-base16ct-crate/` transpiles **every source file
827of base16ct 1.0.0** — `error.rs`, `lower.rs`, `upper.rs`, `mixed.rs` and
828`display.rs`, each byte-for-byte as published on crates.io, verified with
829`cmp` rather than by eye — together with `lib.rs`'s `decoded_len`,
830`encoded_len` and `decode_inner` verbatim. The `alloc` half is on, via
831`--cfg feature=alloc`. Output is byte-identical to rustc's:
Add enums, Option/Result, `?`, and the machinery base16ct needs around them b0ccd80 nandithebull 8h ago832
Add trait impls and slice iterators; base16ct's decoder now goes through ae9f986 nandithebull 8h ago833```
Add closures and unsafe; base16ct's lower.rs and upper.rs go through 0a375d8 nandithebull 8h ago834lower ok abcd1234 len=4 decode: lower, upper, mixed
835upper-rej err InvalidEncoding ... upper correctly rejects lowercase
836oddlen err InvalidLength / invalid Base16 length <- Debug and Display
837encode ok 6162636431323334 len=8 encode, both cases
838encode_str ok abcd1234 len=8 closure over unsafe, borrowed &str
Add display.rs and the alloc half: all of base16ct now goes through afb2a6e nandithebull 7h ago839Ok([171, 205, 18, 52]) decode_vec \
840abcd1234 encode_string > the alloc half
841ABCD1234 abcd1234 HexDisplay {:X} {:x}
Add trait impls and slice iterators; base16ct's decoder now goes through ae9f986 nandithebull 8h ago842```
Add enums, Option/Result, `?`, and the machinery base16ct needs around them b0ccd80 nandithebull 8h ago843
Add display.rs and the alloc half: all of base16ct now goes through afb2a6e nandithebull 7h ago844Everything lowers as written: `dst.get_mut(..decoded_len(src)?)`,
845`src.chunks_exact(2).zip(dst.iter_mut())`, `*dst = byte as u8`, the returned
846`&'a [u8]` view into the caller's buffer, `encode(src, dst).map(|r| unsafe {
847core::str::from_utf8_unchecked(r) })`, and `HexDisplay`'s `UpperHex` impl
848writing once per byte into the formatter.
Add trait impls and slice iterators; base16ct's decoder now goes through ae9f986 nandithebull 8h ago849
Add display.rs and the alloc half: all of base16ct now goes through afb2a6e nandithebull 7h ago850This is the crate whose six files the transpiler in `findings/` emitted empty
851output for, while exiting 0.