nandi/rustnimpublic Fork 0
af6e50f
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.

Lower `bitflags!` directly, checked against the real crate

bitflags is the most-depended-on translatable crate in libcosmic's resolved
tree -- 79 of its 741 -- and therefore the highest-leverage target there. It
is also 26 macro_rules! definitions across five files, which is exactly what
the lowering cannot represent.

Expanding does not rescue it. RUSTC_BOOTSTRAP=1 with -Zunpretty=expanded on a
one-flag user gives 869 lines that still call bitflags::{Bits, Flag, Flags,
iter, parser} -- items defined by those same macros. The chain does not end
in code we could lower, so expansion buys nothing.

What the macro means is small and stable, so it is lowered directly:
src/macros.rs parses the invocation and the lowering emits a newtype over an
integer with its constants, set operations, operators and Debug. This is a
deliberate exception to rejecting macros whose expansion is unknown, and the
argument is that this expansion is known -- documented, stable, and now
pinned by a test rather than by a comment.

That test is the interesting part. `//@ extern: bitflags` makes the *oracle*
compile against the real crate while rustnim gets no such crate, so rustnim
has to reproduce bitflags' behaviour without it and the two stdouts are
compared byte for byte. It caught the two behaviours a reimplementation gets
wrong: `!x` is complemented and then masked to all(), so !(READ|WRITE) is
EXEC and not 0xFFFFFFFC; and from_bits returns None for any bit outside
all() where from_bits_truncate masks. Plus the Debug spelling, Perms(READ |
WRITE) and Perms(0x0).

Also here: dyn Fn/FnMut/FnOnce map to Nim proc types, the same as impl Fn,
which covers roughly a third of the dyn occurrences in the registry sample;
a method the input defines now wins over our model of the standard library,
as Rust resolves inherent methods; and static and method call arguments are
typed from the callee's signature rather than the receiver.

The version-drift risk is real and recorded in DESIGN.md: a future bitflags
could change what the macro generates and the shim would not know. Linking
the real crate in the test is what would catch that.

40 differential cases, all green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nandithebull committed 2026-09-18T22:32:24-07:00 Browse files
af6e50f parent: 8354895
modified Cargo.lock +7 -0
@@ -2,6 +2,12 @@
22 # It is not intended for manual editing.
33 version = 4
44
5+[[package]]
6+name = "bitflags"
7+version = "2.13.2"
8+source = "registry+https://github.com/rust-lang/crates.io-index"
9+checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
10+
511 [[package]]
612 name = "proc-macro2"
713 version = "1.0.107"
@@ -24,6 +30,7 @@ dependencies = [
2430 name = "rustnim"
2531 version = "0.1.0"
2632 dependencies = [
33+ "bitflags",
2734 "syn",
2835 ]
2936
@@ -2,6 +2,12 @@
2 # It is not intended for manual editing.2 # It is not intended for manual editing.
3 version = 43 version = 4
4 4
5+[[package]]
6+name = "bitflags"
7+version = "2.13.2"
8+source = "registry+https://github.com/rust-lang/crates.io-index"
9+checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
10+
5 [[package]]11 [[package]]
6 name = "proc-macro2"12 name = "proc-macro2"
7 version = "1.0.107"13 version = "1.0.107"
@@ -24,6 +30,7 @@ dependencies = [
24 name = "rustnim"30 name = "rustnim"
25 version = "0.1.0"31 version = "0.1.0"
26 dependencies = [32 dependencies = [
33+ "bitflags",
27 "syn",34 "syn",
28 ]35 ]
29 36
modified Cargo.toml +6 -0
@@ -5,3 +5,9 @@ edition = "2024"
55
66 [dependencies]
77 syn = { version = "3.0.5", features = ["full", "extra-traits", "visit"] }
8+
9+# Used only by the test corpus: a case carrying `//@ extern: bitflags` is
10+# compiled by the oracle against the real crate, so the `bitflags!` lowering is
11+# checked against it rather than against our reading of its documentation.
12+[dev-dependencies]
13+bitflags = "2"
@@ -5,3 +5,9 @@ edition = "2024"
5 5
6 [dependencies]6 [dependencies]
7 syn = { version = "3.0.5", features = ["full", "extra-traits", "visit"] }7 syn = { version = "3.0.5", features = ["full", "extra-traits", "visit"] }
8+
9+# Used only by the test corpus: a case carrying `//@ extern: bitflags` is
10+# compiled by the oracle against the real crate, so the `bitflags!` lowering is
11+# checked against it rather than against our reading of its documentation.
12+[dev-dependencies]
13+bitflags = "2"
modified DESIGN.md +40 -0
@@ -478,6 +478,44 @@ work built on `palette` (40,874 lines across 122 files, plus a proc-macro
478478 crate). `mode.rs` needs `cosmic-config` and its derive macro. Those are
479479 dependency walls, not language gaps.
480480
481+## `bitflags!`, and why it is lowered rather than expanded
482+
483+`bitflags` is the most-depended-on translatable crate in `libcosmic`'s
484+resolved tree — 79 of its 741 crates — so it is the highest-leverage target
485+there. It is also 26 `macro_rules!` definitions across five files, which is
486+the one thing the lowering cannot represent.
487+
488+Expanding 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,
491+parser::to_writer}` — items defined by those same macros. The chain does not
492+end in code we could lower.
493+
494+What the macro *means*, though, is small and stable: a newtype over an integer
495+with named constants and set operations. So `src/macros.rs` parses the
496+invocation and the lowering emits that directly. This is a deliberate
497+exception to "a macro whose expansion is not known is rejected", and the
498+argument is that the expansion *is* known here — it is documented, stable, and
499+now pinned by a test.
500+
501+`tests/cases/034-bitflags.rs` is that test, and it is unusual: `//@ extern:
502+bitflags` makes the **oracle** compile against the real crate while rustnim
503+gets no such crate. rustnim has to reproduce bitflags' behaviour without it,
504+and the outputs are compared byte for byte. Two behaviours it pins that a
505+reimplementation 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+
512+plus the `Debug` spelling, which is `Perms(READ | WRITE)` and `Perms(0x0)`.
513+
514+The risk this carries is version drift: a future `bitflags` could change what
515+the macro generates, and the shim would not know. The test is what would catch
516+it, which is why it links the real crate rather than a copy of its
517+documentation.
518+
481519 ## Proof of byte-identity for `base16ct`
482520
483521 [`PROOF.md`](PROOF.md) sets out what is actually established: exhaustive
@@ -523,6 +561,8 @@ Cases carry directives in leading `//@` comments:
523561 | `//@ skip: <reason>` | not run; reported as skipped |
524562 | `//@ args: <argv>` | passed to both binaries |
525563 | `//@ stdin: <line>` | fed to both binaries |
564+| `//@ cfg: feature=<name>` | passed to rustnim, and to rustc as `--cfg feature="<name>"` |
565+| `//@ extern: <crate>` | the **oracle** links this crate; rustnim does not get it |
526566
527567 `reject` cases are how the "fail loudly" rule is tested rather than merely
528568 stated: `900``904` pin the rejections of `i128`, an unmapped standard-library
@@ -478,6 +478,44 @@ work built on `palette` (40,874 lines across 122 files, plus a proc-macro
478 crate). `mode.rs` needs `cosmic-config` and its derive macro. Those are478 crate). `mode.rs` needs `cosmic-config` and its derive macro. Those are
479 dependency walls, not language gaps.479 dependency walls, not language gaps.
480 480
481+## `bitflags!`, and why it is lowered rather than expanded
482+
483+`bitflags` is the most-depended-on translatable crate in `libcosmic`'s
484+resolved tree — 79 of its 741 crates — so it is the highest-leverage target
485+there. It is also 26 `macro_rules!` definitions across five files, which is
486+the one thing the lowering cannot represent.
487+
488+Expanding 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,
491+parser::to_writer}` — items defined by those same macros. The chain does not
492+end in code we could lower.
493+
494+What the macro *means*, though, is small and stable: a newtype over an integer
495+with named constants and set operations. So `src/macros.rs` parses the
496+invocation and the lowering emits that directly. This is a deliberate
497+exception to "a macro whose expansion is not known is rejected", and the
498+argument is that the expansion *is* known here — it is documented, stable, and
499+now pinned by a test.
500+
501+`tests/cases/034-bitflags.rs` is that test, and it is unusual: `//@ extern:
502+bitflags` makes the **oracle** compile against the real crate while rustnim
503+gets no such crate. rustnim has to reproduce bitflags' behaviour without it,
504+and the outputs are compared byte for byte. Two behaviours it pins that a
505+reimplementation 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+
512+plus the `Debug` spelling, which is `Perms(READ | WRITE)` and `Perms(0x0)`.
513+
514+The risk this carries is version drift: a future `bitflags` could change what
515+the macro generates, and the shim would not know. The test is what would catch
516+it, which is why it links the real crate rather than a copy of its
517+documentation.
518+
481 ## Proof of byte-identity for `base16ct`519 ## Proof of byte-identity for `base16ct`
482 520
483 [`PROOF.md`](PROOF.md) sets out what is actually established: exhaustive521 [`PROOF.md`](PROOF.md) sets out what is actually established: exhaustive
@@ -523,6 +561,8 @@ Cases carry directives in leading `//@` comments:
523 | `//@ skip: <reason>` | not run; reported as skipped |561 | `//@ skip: <reason>` | not run; reported as skipped |
524 | `//@ args: <argv>` | passed to both binaries |562 | `//@ args: <argv>` | passed to both binaries |
525 | `//@ stdin: <line>` | fed to both binaries |563 | `//@ stdin: <line>` | fed to both binaries |
564+| `//@ cfg: feature=<name>` | passed to rustnim, and to rustc as `--cfg feature="<name>"` |
565+| `//@ extern: <crate>` | the **oracle** links this crate; rustnim does not get it |
526 566
527 `reject` cases are how the "fail loudly" rule is tested rather than merely567 `reject` cases are how the "fail loudly" rule is tested rather than merely
528 stated: `900``904` pin the rejections of `i128`, an unmapped standard-library568 stated: `900``904` pin the rejections of `i128`, an unmapped standard-library
modified README.md +9 -0
@@ -77,6 +77,15 @@ argument for longer inputs and 20,000 pseudorandom cases attacking it.
7777 `cargo test --test proof` runs it — 151,463 cases, 9.1 MB of output, compared
7878 byte for byte.
7979
80+## `bitflags!`
81+
82+`bitflags` is the most-depended-on translatable crate in libcosmic's tree (79
83+of 741), and it is 26 `macro_rules!` definitions. Expanding the macro does not
84+help — the expansion still calls into the crate's own macro-defined runtime.
85+So the macro is lowered directly, and checked against the real crate: the test
86+links bitflags for the *oracle only*, and rustnim has to reproduce its
87+behaviour without it, byte for byte.
88+
8089 ## Does it generalise?
8190
8291 `base16ct` is the crate this was built toward, so a second one was tried.
@@ -77,6 +77,15 @@ argument for longer inputs and 20,000 pseudorandom cases attacking it.
77 `cargo test --test proof` runs it — 151,463 cases, 9.1 MB of output, compared77 `cargo test --test proof` runs it — 151,463 cases, 9.1 MB of output, compared
78 byte for byte.78 byte for byte.
79 79
80+## `bitflags!`
81+
82+`bitflags` is the most-depended-on translatable crate in libcosmic's tree (79
83+of 741), and it is 26 `macro_rules!` definitions. Expanding the macro does not
84+help — the expansion still calls into the crate's own macro-defined runtime.
85+So the macro is lowered directly, and checked against the real crate: the test
86+links bitflags for the *oracle only*, and rustnim has to reproduce its
87+behaviour without it, byte for byte.
88+
80 ## Does it generalise?89 ## Does it generalise?
81 90
82 `base16ct` is the crate this was built toward, so a second one was tried.91 `base16ct` is the crate this was built toward, so a second one was tried.
modified src/lower.rs +260 -1
@@ -213,6 +213,10 @@ pub struct Lowerer {
213213 assoc: HashMap<(String, String), Nim>,
214214 /// `(type, name) -> (nim name, type)` for `const` items inside an `impl`.
215215 assoc_consts: HashMap<(String, String), (String, Nim)>,
216+ /// Types declared by a `bitflags!` invocation.
217+ bitflags: std::collections::HashSet<String>,
218+ /// `(type, flag) -> nim const name`.
219+ flag_consts: HashMap<(String, String), String>,
216220 /// `use` brings a name into scope from another module. Flattening loses
217221 /// the module structure, so the mapping is recorded and consulted when a
218222 /// bare call is resolved.
@@ -288,6 +292,8 @@ impl Lowerer {
288292 type_generics: HashMap::new(),
289293 assoc: HashMap::new(),
290294 assoc_consts: HashMap::new(),
295+ bitflags: std::collections::HashSet::new(),
296+ flag_consts: HashMap::new(),
291297 use_map: HashMap::new(),
292298 structs: HashMap::new(),
293299 enums: HashMap::new(),
@@ -554,6 +560,9 @@ impl Lowerer {
554560 }
555561 self.structs.insert(s.ident.to_string(), fields);
556562 }
563+ Item::Macro(m) if path_name(&m.mac.path) == "bitflags" => {
564+ self.collect_bitflags(&m.mac)?;
565+ }
557566 Item::Mod(m) if m.content.is_some() => {
558567 let items = m.content.as_ref().map(|(_, i)| i.clone()).unwrap_or_default();
559568 for i in &items {
@@ -1178,6 +1187,7 @@ impl Lowerer {
11781187 }
11791188 match item {
11801189 Item::Struct(_) | Item::Enum(_) | Item::Const(_) => self.item_inner(item),
1190+ Item::Macro(m) if path_name(&m.mac.path) == "bitflags" => self.item_inner(item),
11811191 Item::Mod(m) if m.content.is_some() => {
11821192 let items = m.content.as_ref().map(|(_, i)| i.clone()).unwrap_or_default();
11831193 for i in &items {
@@ -1197,6 +1207,9 @@ impl Lowerer {
11971207 if matches!(item, Item::Struct(_) | Item::Enum(_) | Item::Const(_)) {
11981208 return Ok(());
11991209 }
1210+ if matches!(item, Item::Macro(m) if path_name(&m.mac.path) == "bitflags") {
1211+ return Ok(()); // emitted with the types
1212+ }
12001213 self.item_inner(item)
12011214 }
12021215
@@ -1225,6 +1238,7 @@ impl Lowerer {
12251238 self.blank();
12261239 Ok(())
12271240 }
1241+ Item::Macro(m) if path_name(&m.mac.path) == "bitflags" => self.emit_bitflags(&m.mac),
12281242 Item::Type(_) => Ok(()), // expanded at every use site
12291243 Item::Trait(t) => {
12301244 // We do not model trait resolution, so a declaration generates
@@ -1416,6 +1430,193 @@ impl Lowerer {
14161430 }
14171431 }
14181432
1433+ /// Register a `bitflags!` type's operations so call sites resolve.
1434+ fn collect_bitflags(&mut self, mac: &syn::Macro) -> Result<(), String> {
1435+ let input: crate::macros::BitflagsInput = mac
1436+ .parse_body()
1437+ .map_err(|e| format!("`bitflags!`: {e}"))?;
1438+ for def in &input.0 {
1439+ let name = def.name.to_string();
1440+ let repr = self.map_ty(&def.repr)?;
1441+ if !repr.is_integer() {
1442+ return Err(format!("`bitflags! {name}` needs an integer representation"));
1443+ }
1444+ let me = Nim::Named(name.clone(), vec![]);
1445+ let b = Nim::Prim("bool".into());
1446+ self.structs
1447+ .insert(name.clone(), vec![("bitsField".into(), repr.clone())]);
1448+ self.type_generics.insert(name.clone(), Vec::new());
1449+
1450+ let mut m = |n: &str, params: Vec<Nim>, ret: Nim, nim: String| {
1451+ self.methods.insert(
1452+ (name.clone(), n.to_string()),
1453+ Sig { params, ret, generics: Vec::new() },
1454+ );
1455+ self.statics.insert((name.clone(), n.to_string()), nim);
1456+ };
1457+ let s1 = vec![me.clone()];
1458+ let s2 = vec![me.clone(), me.clone()];
1459+ let vs2 = vec![Nim::Var(Box::new(me.clone())), me.clone()];
1460+ m("bits", s1.clone(), repr.clone(), format!("{name}_bits"));
1461+ m("is_empty", s1.clone(), b.clone(), format!("{name}_is_empty"));
1462+ m("is_all", s1.clone(), b.clone(), format!("{name}_is_all"));
1463+ m("contains", s2.clone(), b.clone(), format!("{name}_contains"));
1464+ m("intersects", s2.clone(), b.clone(), format!("{name}_intersects"));
1465+ for (rust, nim) in [
1466+ ("union", "union"),
1467+ ("intersection", "intersection"),
1468+ ("difference", "difference"),
1469+ ("symmetric_difference", "symmetric_difference"),
1470+ ] {
1471+ m(rust, s2.clone(), me.clone(), format!("{name}_{nim}"));
1472+ }
1473+ for n in ["insert", "remove", "toggle"] {
1474+ m(n, vs2.clone(), Nim::Unit, format!("{name}_{n}"));
1475+ }
1476+ m(
1477+ "set",
1478+ vec![Nim::Var(Box::new(me.clone())), me.clone(), b.clone()],
1479+ Nim::Unit,
1480+ format!("{name}_set"),
1481+ );
1482+ m("empty", vec![], me.clone(), format!("{name}_empty"));
1483+ m("all", vec![], me.clone(), format!("{name}_all"));
1484+ m(
1485+ "from_bits",
1486+ vec![repr.clone()],
1487+ Nim::Named("Option".into(), vec![me.clone()]),
1488+ format!("{name}_from_bits"),
1489+ );
1490+ m(
1491+ "from_bits_truncate",
1492+ vec![repr.clone()],
1493+ me.clone(),
1494+ format!("{name}_from_bits_truncate"),
1495+ );
1496+ m("complement", s1.clone(), me.clone(), format!("{name}_complement"));
1497+
1498+ // The operator forms, routed through the same dispatch that a
1499+ // hand-written `impl BitOr` would use.
1500+ for (op, trait_name, method) in [
1501+ ("|", "BitOr", "bitor"),
1502+ ("&", "BitAnd", "bitand"),
1503+ ("^", "BitXor", "bitxor"),
1504+ ("-", "Sub", "sub"),
1505+ ("not", "Not", "not"),
1506+ ] {
1507+ self.op_impls.insert((name.clone(), op.to_string()), ());
1508+ let params = if op == "not" { s1.clone() } else { s2.clone() };
1509+ self.methods.insert(
1510+ (name.clone(), method.to_string()),
1511+ Sig { params, ret: me.clone(), generics: Vec::new() },
1512+ );
1513+ let _ = trait_name;
1514+ }
1515+ self.bitflags.insert(name);
1516+ }
1517+ Ok(())
1518+ }
1519+
1520+ /// Emit the Nim for a `bitflags!` type. See `src/macros.rs` for why this
1521+ /// is lowered directly rather than by expanding the macro.
1522+ fn emit_bitflags(&mut self, mac: &syn::Macro) -> Result<(), String> {
1523+ let input: crate::macros::BitflagsInput = mac
1524+ .parse_body()
1525+ .map_err(|e| format!("`bitflags!`: {e}"))?;
1526+ for def in &input.0 {
1527+ let name = def.name.to_string();
1528+ let repr = self.map_ty(&def.repr)?;
1529+ let r = repr.render();
1530+
1531+ self.line(&format!("type {name}* = object"));
1532+ self.line(&format!(" bitsField*: {r}"));
1533+ self.blank();
1534+ self.line(&format!("proc {name}_bits*(x: {name}): {r} = x.bitsField"));
1535+
1536+ // The constants. A flag's value may name earlier flags, as
1537+ // `const ALL = Self::READ.bits() | ..` does, so they are emitted
1538+ // in order and each is in scope for the next.
1539+ self.push_scope();
1540+ self.bind_static_type(&name, &repr);
1541+ for (fname, value) in &def.flags {
1542+ let v = self.expr_at(value, Some(&repr))?;
1543+ self.line(&format!(
1544+ "const {}{}* = {}(bitsField: {})",
1545+ name, fname, name, v.code
1546+ ));
1547+ self.flag_consts
1548+ .insert((name.clone(), fname.to_string()), format!("{name}{fname}"));
1549+ }
1550+ self.pop_scope();
1551+
1552+ let all: Vec<String> = def
1553+ .flags
1554+ .iter()
1555+ .map(|(f, _)| format!("{name}{f}.bitsField"))
1556+ .collect();
1557+ let all_bits = if all.is_empty() {
1558+ format!("{}(0)", r)
1559+ } else {
1560+ all.join(" or ")
1561+ };
1562+ self.blank();
1563+ self.line(&format!("const {name}AllBits: {r} = {all_bits}"));
1564+ self.blank();
1565+
1566+ for l in [
1567+ format!("proc {name}_empty*(): {name} = {name}(bitsField: {r}(0))"),
1568+ format!("proc {name}_all*(): {name} = {name}(bitsField: {name}AllBits)"),
1569+ format!("proc {name}_is_empty*(x: {name}): bool = x.bitsField == {r}(0)"),
1570+ format!("proc {name}_is_all*(x: {name}): bool = (x.bitsField and {name}AllBits) == {name}AllBits"),
1571+ format!("proc {name}_contains*(a, b: {name}): bool = (a.bitsField and b.bitsField) == b.bitsField"),
1572+ format!("proc {name}_intersects*(a, b: {name}): bool = (a.bitsField and b.bitsField) != {r}(0)"),
1573+ format!("proc {name}_union*(a, b: {name}): {name} = {name}(bitsField: a.bitsField or b.bitsField)"),
1574+ format!("proc {name}_intersection*(a, b: {name}): {name} = {name}(bitsField: a.bitsField and b.bitsField)"),
1575+ format!("proc {name}_difference*(a, b: {name}): {name} = {name}(bitsField: a.bitsField and (not b.bitsField))"),
1576+ format!("proc {name}_symmetric_difference*(a, b: {name}): {name} = {name}(bitsField: a.bitsField xor b.bitsField)"),
1577+ // `!x` complements and then masks to `all()`, which is what
1578+ // bitflags does and not what a plain `not` would give.
1579+ format!("proc {name}_complement*(x: {name}): {name} = {name}(bitsField: (not x.bitsField) and {name}AllBits)"),
1580+ format!("proc {name}_from_bits_truncate*(b: {r}): {name} = {name}(bitsField: b and {name}AllBits)"),
1581+ format!("proc {name}_from_bits*(b: {r}): Option[{name}] ="),
1582+ format!(" if (b and (not {name}AllBits)) != {r}(0): rsNone[{name}]() else: rsSome({name}(bitsField: b))"),
1583+ format!("proc {name}_insert*(x: var {name}, o: {name}) = x.bitsField = x.bitsField or o.bitsField"),
1584+ format!("proc {name}_remove*(x: var {name}, o: {name}) = x.bitsField = x.bitsField and (not o.bitsField)"),
1585+ format!("proc {name}_toggle*(x: var {name}, o: {name}) = x.bitsField = x.bitsField xor o.bitsField"),
1586+ format!("proc {name}_set*(x: var {name}, o: {name}, on: bool) ="),
1587+ format!(" if on: {name}_insert(x, o) else: {name}_remove(x, o)"),
1588+ format!("proc rsBitOr_{name}_bitor*(a, b: {name}): {name} = {name}_union(a, b)"),
1589+ format!("proc rsBitAnd_{name}_bitand*(a, b: {name}): {name} = {name}_intersection(a, b)"),
1590+ format!("proc rsBitXor_{name}_bitxor*(a, b: {name}): {name} = {name}_symmetric_difference(a, b)"),
1591+ format!("proc rsSub_{name}_sub*(a, b: {name}): {name} = {name}_difference(a, b)"),
1592+ format!("proc rsNot_{name}_not*(a: {name}): {name} = {name}_complement(a)"),
1593+ ] {
1594+ self.line(&l);
1595+ }
1596+
1597+ // Debug prints the set flag names, or `0x0` when empty -- again
1598+ // matching the crate rather than a guess.
1599+ self.line(&format!("proc rsDebug*(x: {name}): string ="));
1600+ self.line(&format!(" result = \"{name}(\""));
1601+ self.line(" var first = true");
1602+ for (fname, _) in &def.flags {
1603+ self.line(&format!(
1604+ " if (x.bitsField and {name}{f}.bitsField) == {name}{f}.bitsField and {name}{f}.bitsField != {r}(0):",
1605+ f = fname
1606+ ));
1607+ self.line(" if not first: result.add(\" | \")");
1608+ self.line(&format!(" result.add(\"{fname}\")"));
1609+ self.line(" first = false");
1610+ }
1611+ self.line(" if first: result.add(\"0x0\")");
1612+ self.line(" result.add(\")\")");
1613+ self.blank();
1614+ }
1615+ Ok(())
1616+ }
1617+
1618+ fn bind_static_type(&mut self, _name: &str, _repr: &Nim) {}
1619+
14191620 fn emit_enum(&mut self, def: &EnumDef) {
14201621 let name = ident(&def.name);
14211622 let g = Self::gen_list(
@@ -2860,6 +3061,18 @@ impl Lowerer {
28603061 if name == "None" {
28613062 return Ok(Val::new(self.none_of(expect), expect.cloned()));
28623063 }
3064+ // `Perms::READ`: a constant of a `bitflags!` type.
3065+ if let Some(q) = p.path.segments.iter().rev().nth(1).map(|s| s.ident.to_string()) {
3066+ let q = if q == "Self" {
3067+ self.self_ty.as_ref().map(type_name).unwrap_or(q)
3068+ } else {
3069+ q
3070+ };
3071+ if let Some(c) = self.flag_consts.get(&(q.clone(), name.clone())) {
3072+ return Ok(Val::new(c.clone(), Some(Nim::Named(q, vec![]))));
3073+ }
3074+ }
3075+
28633076 // `Grid::BORDER`: a `const` declared inside an `impl`.
28643077 if let Some(q) = p.path.segments.iter().rev().nth(1).map(|s| s.ident.to_string()) {
28653078 let q = if q == "Self" {
@@ -3313,7 +3526,12 @@ impl Lowerer {
33133526 // Rust's `!` is logical on bool and bitwise-complement on integers.
33143527 // Nim spells those `not` and `not` as well, so one mapping covers
33153528 // both — but only because Nim overloads `not` the same way.
3316- UnOp::Not(_) => Ok(Val::new(format!("(not {})", v.code), v.ty)),
3529+ UnOp::Not(_) => {
3530+ if let Some(f) = self.op_proc(&v.ty, "not") {
3531+ return Ok(Val::new(format!("{}({})", f, v.code), v.ty));
3532+ }
3533+ Ok(Val::new(format!("(not {})", v.code), v.ty))
3534+ }
33173535 UnOp::Deref(_) => Ok(v),
33183536 _ => Err("unsupported unary operator".into()),
33193537 }
@@ -3933,6 +4151,19 @@ impl Lowerer {
39334151 q
39344152 };
39354153 if let Some(sig) = self.methods.get(&(q.clone(), name.clone())) {
4154+ // Re-lower the arguments with the declared parameter types, so
4155+ // a literal takes the width the signature asks for.
4156+ let declared = sig.params.clone();
4157+ let mut args = args.clone();
4158+ let mut codes = codes.clone();
4159+ for (i, a) in c.args.iter().enumerate() {
4160+ if let Some(want) = declared.get(i) {
4161+ let want = want.clone().unvar();
4162+ args[i] = self.expr_at(a, Some(&want))?;
4163+ codes[i] = args[i].code.clone();
4164+ }
4165+ }
4166+ let sig = &self.methods[&(q.clone(), name.clone())];
39364167 let arg_tys: Vec<Option<Nim>> = args.iter().map(|a| a.ty.clone()).collect();
39374168 let ret = Self::instantiate(sig, &arg_tys);
39384169 let nim = self
@@ -4086,6 +4317,34 @@ impl Lowerer {
40864317 let a0 = args.first().map(|a| a.code.clone());
40874318 let rt = recv.ty.clone();
40884319
4320+ // A method the input defines wins over our model of the standard
4321+ // library: `is_empty` on a `bitflags!` type is that type's, not the
4322+ // sequence one. Rust resolves inherent methods the same way.
4323+ if let Some(t) = &rt {
4324+ let key = (type_name(t), name.clone());
4325+ if self.methods.contains_key(&key) {
4326+ let declared = self.methods[&key].params.clone();
4327+ let skip = usize::from(declared.len() == m.args.len() + 1);
4328+ for (i, a) in m.args.iter().enumerate() {
4329+ if let Some(want) = declared.get(i + skip) {
4330+ let want = want.clone().unvar();
4331+ args[i] = self.expr_at(a, Some(&want))?;
4332+ }
4333+ }
4334+ let mut arg_tys: Vec<Option<Nim>> = vec![rt.clone()];
4335+ arg_tys.extend(args.iter().map(|a| a.ty.clone()));
4336+ let ret = Self::instantiate(&self.methods[&key], &arg_tys);
4337+ let nim = self
4338+ .statics
4339+ .get(&key)
4340+ .cloned()
4341+ .unwrap_or_else(|| ident(&name));
4342+ let mut all = vec![recv.code.clone()];
4343+ all.extend(args.iter().map(|a| a.code.clone()));
4344+ return Ok(Val::new(format!("{}({})", nim, all.join(", ")), Some(ret)));
4345+ }
4346+ }
4347+
40894348 let (code, ty) = match name.as_str() {
40904349 // Rust's `len()` is `usize`; Nim's is `int`. The conversion is
40914350 // explicit so that a `usize` binding type-checks on the Nim side.
@@ -213,6 +213,10 @@ pub struct Lowerer {
213 assoc: HashMap<(String, String), Nim>,213 assoc: HashMap<(String, String), Nim>,
214 /// `(type, name) -> (nim name, type)` for `const` items inside an `impl`.214 /// `(type, name) -> (nim name, type)` for `const` items inside an `impl`.
215 assoc_consts: HashMap<(String, String), (String, Nim)>,215 assoc_consts: HashMap<(String, String), (String, Nim)>,
216+ /// Types declared by a `bitflags!` invocation.
217+ bitflags: std::collections::HashSet<String>,
218+ /// `(type, flag) -> nim const name`.
219+ flag_consts: HashMap<(String, String), String>,
216 /// `use` brings a name into scope from another module. Flattening loses220 /// `use` brings a name into scope from another module. Flattening loses
217 /// the module structure, so the mapping is recorded and consulted when a221 /// the module structure, so the mapping is recorded and consulted when a
218 /// bare call is resolved.222 /// bare call is resolved.
@@ -288,6 +292,8 @@ impl Lowerer {
288 type_generics: HashMap::new(),292 type_generics: HashMap::new(),
289 assoc: HashMap::new(),293 assoc: HashMap::new(),
290 assoc_consts: HashMap::new(),294 assoc_consts: HashMap::new(),
295+ bitflags: std::collections::HashSet::new(),
296+ flag_consts: HashMap::new(),
291 use_map: HashMap::new(),297 use_map: HashMap::new(),
292 structs: HashMap::new(),298 structs: HashMap::new(),
293 enums: HashMap::new(),299 enums: HashMap::new(),
@@ -554,6 +560,9 @@ impl Lowerer {
554 }560 }
555 self.structs.insert(s.ident.to_string(), fields);561 self.structs.insert(s.ident.to_string(), fields);
556 }562 }
563+ Item::Macro(m) if path_name(&m.mac.path) == "bitflags" => {
564+ self.collect_bitflags(&m.mac)?;
565+ }
557 Item::Mod(m) if m.content.is_some() => {566 Item::Mod(m) if m.content.is_some() => {
558 let items = m.content.as_ref().map(|(_, i)| i.clone()).unwrap_or_default();567 let items = m.content.as_ref().map(|(_, i)| i.clone()).unwrap_or_default();
559 for i in &items {568 for i in &items {
@@ -1178,6 +1187,7 @@ impl Lowerer {
1178 }1187 }
1179 match item {1188 match item {
1180 Item::Struct(_) | Item::Enum(_) | Item::Const(_) => self.item_inner(item),1189 Item::Struct(_) | Item::Enum(_) | Item::Const(_) => self.item_inner(item),
1190+ Item::Macro(m) if path_name(&m.mac.path) == "bitflags" => self.item_inner(item),
1181 Item::Mod(m) if m.content.is_some() => {1191 Item::Mod(m) if m.content.is_some() => {
1182 let items = m.content.as_ref().map(|(_, i)| i.clone()).unwrap_or_default();1192 let items = m.content.as_ref().map(|(_, i)| i.clone()).unwrap_or_default();
1183 for i in &items {1193 for i in &items {
@@ -1197,6 +1207,9 @@ impl Lowerer {
1197 if matches!(item, Item::Struct(_) | Item::Enum(_) | Item::Const(_)) {1207 if matches!(item, Item::Struct(_) | Item::Enum(_) | Item::Const(_)) {
1198 return Ok(());1208 return Ok(());
1199 }1209 }
1210+ if matches!(item, Item::Macro(m) if path_name(&m.mac.path) == "bitflags") {
1211+ return Ok(()); // emitted with the types
1212+ }
1200 self.item_inner(item)1213 self.item_inner(item)
1201 }1214 }
1202 1215
@@ -1225,6 +1238,7 @@ impl Lowerer {
1225 self.blank();1238 self.blank();
1226 Ok(())1239 Ok(())
1227 }1240 }
1241+ Item::Macro(m) if path_name(&m.mac.path) == "bitflags" => self.emit_bitflags(&m.mac),
1228 Item::Type(_) => Ok(()), // expanded at every use site1242 Item::Type(_) => Ok(()), // expanded at every use site
1229 Item::Trait(t) => {1243 Item::Trait(t) => {
1230 // We do not model trait resolution, so a declaration generates1244 // We do not model trait resolution, so a declaration generates
@@ -1416,6 +1430,193 @@ impl Lowerer {
1416 }1430 }
1417 }1431 }
1418 1432
1433+ /// Register a `bitflags!` type's operations so call sites resolve.
1434+ fn collect_bitflags(&mut self, mac: &syn::Macro) -> Result<(), String> {
1435+ let input: crate::macros::BitflagsInput = mac
1436+ .parse_body()
1437+ .map_err(|e| format!("`bitflags!`: {e}"))?;
1438+ for def in &input.0 {
1439+ let name = def.name.to_string();
1440+ let repr = self.map_ty(&def.repr)?;
1441+ if !repr.is_integer() {
1442+ return Err(format!("`bitflags! {name}` needs an integer representation"));
1443+ }
1444+ let me = Nim::Named(name.clone(), vec![]);
1445+ let b = Nim::Prim("bool".into());
1446+ self.structs
1447+ .insert(name.clone(), vec![("bitsField".into(), repr.clone())]);
1448+ self.type_generics.insert(name.clone(), Vec::new());
1449+
1450+ let mut m = |n: &str, params: Vec<Nim>, ret: Nim, nim: String| {
1451+ self.methods.insert(
1452+ (name.clone(), n.to_string()),
1453+ Sig { params, ret, generics: Vec::new() },
1454+ );
1455+ self.statics.insert((name.clone(), n.to_string()), nim);
1456+ };
1457+ let s1 = vec![me.clone()];
1458+ let s2 = vec![me.clone(), me.clone()];
1459+ let vs2 = vec![Nim::Var(Box::new(me.clone())), me.clone()];
1460+ m("bits", s1.clone(), repr.clone(), format!("{name}_bits"));
1461+ m("is_empty", s1.clone(), b.clone(), format!("{name}_is_empty"));
1462+ m("is_all", s1.clone(), b.clone(), format!("{name}_is_all"));
1463+ m("contains", s2.clone(), b.clone(), format!("{name}_contains"));
1464+ m("intersects", s2.clone(), b.clone(), format!("{name}_intersects"));
1465+ for (rust, nim) in [
1466+ ("union", "union"),
1467+ ("intersection", "intersection"),
1468+ ("difference", "difference"),
1469+ ("symmetric_difference", "symmetric_difference"),
1470+ ] {
1471+ m(rust, s2.clone(), me.clone(), format!("{name}_{nim}"));
1472+ }
1473+ for n in ["insert", "remove", "toggle"] {
1474+ m(n, vs2.clone(), Nim::Unit, format!("{name}_{n}"));
1475+ }
1476+ m(
1477+ "set",
1478+ vec![Nim::Var(Box::new(me.clone())), me.clone(), b.clone()],
1479+ Nim::Unit,
1480+ format!("{name}_set"),
1481+ );
1482+ m("empty", vec![], me.clone(), format!("{name}_empty"));
1483+ m("all", vec![], me.clone(), format!("{name}_all"));
1484+ m(
1485+ "from_bits",
1486+ vec![repr.clone()],
1487+ Nim::Named("Option".into(), vec![me.clone()]),
1488+ format!("{name}_from_bits"),
1489+ );
1490+ m(
1491+ "from_bits_truncate",
1492+ vec![repr.clone()],
1493+ me.clone(),
1494+ format!("{name}_from_bits_truncate"),
1495+ );
1496+ m("complement", s1.clone(), me.clone(), format!("{name}_complement"));
1497+
1498+ // The operator forms, routed through the same dispatch that a
1499+ // hand-written `impl BitOr` would use.
1500+ for (op, trait_name, method) in [
1501+ ("|", "BitOr", "bitor"),
1502+ ("&", "BitAnd", "bitand"),
1503+ ("^", "BitXor", "bitxor"),
1504+ ("-", "Sub", "sub"),
1505+ ("not", "Not", "not"),
1506+ ] {
1507+ self.op_impls.insert((name.clone(), op.to_string()), ());
1508+ let params = if op == "not" { s1.clone() } else { s2.clone() };
1509+ self.methods.insert(
1510+ (name.clone(), method.to_string()),
1511+ Sig { params, ret: me.clone(), generics: Vec::new() },
1512+ );
1513+ let _ = trait_name;
1514+ }
1515+ self.bitflags.insert(name);
1516+ }
1517+ Ok(())
1518+ }
1519+
1520+ /// Emit the Nim for a `bitflags!` type. See `src/macros.rs` for why this
1521+ /// is lowered directly rather than by expanding the macro.
1522+ fn emit_bitflags(&mut self, mac: &syn::Macro) -> Result<(), String> {
1523+ let input: crate::macros::BitflagsInput = mac
1524+ .parse_body()
1525+ .map_err(|e| format!("`bitflags!`: {e}"))?;
1526+ for def in &input.0 {
1527+ let name = def.name.to_string();
1528+ let repr = self.map_ty(&def.repr)?;
1529+ let r = repr.render();
1530+
1531+ self.line(&format!("type {name}* = object"));
1532+ self.line(&format!(" bitsField*: {r}"));
1533+ self.blank();
1534+ self.line(&format!("proc {name}_bits*(x: {name}): {r} = x.bitsField"));
1535+
1536+ // The constants. A flag's value may name earlier flags, as
1537+ // `const ALL = Self::READ.bits() | ..` does, so they are emitted
1538+ // in order and each is in scope for the next.
1539+ self.push_scope();
1540+ self.bind_static_type(&name, &repr);
1541+ for (fname, value) in &def.flags {
1542+ let v = self.expr_at(value, Some(&repr))?;
1543+ self.line(&format!(
1544+ "const {}{}* = {}(bitsField: {})",
1545+ name, fname, name, v.code
1546+ ));
1547+ self.flag_consts
1548+ .insert((name.clone(), fname.to_string()), format!("{name}{fname}"));
1549+ }
1550+ self.pop_scope();
1551+
1552+ let all: Vec<String> = def
1553+ .flags
1554+ .iter()
1555+ .map(|(f, _)| format!("{name}{f}.bitsField"))
1556+ .collect();
1557+ let all_bits = if all.is_empty() {
1558+ format!("{}(0)", r)
1559+ } else {
1560+ all.join(" or ")
1561+ };
1562+ self.blank();
1563+ self.line(&format!("const {name}AllBits: {r} = {all_bits}"));
1564+ self.blank();
1565+
1566+ for l in [
1567+ format!("proc {name}_empty*(): {name} = {name}(bitsField: {r}(0))"),
1568+ format!("proc {name}_all*(): {name} = {name}(bitsField: {name}AllBits)"),
1569+ format!("proc {name}_is_empty*(x: {name}): bool = x.bitsField == {r}(0)"),
1570+ format!("proc {name}_is_all*(x: {name}): bool = (x.bitsField and {name}AllBits) == {name}AllBits"),
1571+ format!("proc {name}_contains*(a, b: {name}): bool = (a.bitsField and b.bitsField) == b.bitsField"),
1572+ format!("proc {name}_intersects*(a, b: {name}): bool = (a.bitsField and b.bitsField) != {r}(0)"),
1573+ format!("proc {name}_union*(a, b: {name}): {name} = {name}(bitsField: a.bitsField or b.bitsField)"),
1574+ format!("proc {name}_intersection*(a, b: {name}): {name} = {name}(bitsField: a.bitsField and b.bitsField)"),
1575+ format!("proc {name}_difference*(a, b: {name}): {name} = {name}(bitsField: a.bitsField and (not b.bitsField))"),
1576+ format!("proc {name}_symmetric_difference*(a, b: {name}): {name} = {name}(bitsField: a.bitsField xor b.bitsField)"),
1577+ // `!x` complements and then masks to `all()`, which is what
1578+ // bitflags does and not what a plain `not` would give.
1579+ format!("proc {name}_complement*(x: {name}): {name} = {name}(bitsField: (not x.bitsField) and {name}AllBits)"),
1580+ format!("proc {name}_from_bits_truncate*(b: {r}): {name} = {name}(bitsField: b and {name}AllBits)"),
1581+ format!("proc {name}_from_bits*(b: {r}): Option[{name}] ="),
1582+ format!(" if (b and (not {name}AllBits)) != {r}(0): rsNone[{name}]() else: rsSome({name}(bitsField: b))"),
1583+ format!("proc {name}_insert*(x: var {name}, o: {name}) = x.bitsField = x.bitsField or o.bitsField"),
1584+ format!("proc {name}_remove*(x: var {name}, o: {name}) = x.bitsField = x.bitsField and (not o.bitsField)"),
1585+ format!("proc {name}_toggle*(x: var {name}, o: {name}) = x.bitsField = x.bitsField xor o.bitsField"),
1586+ format!("proc {name}_set*(x: var {name}, o: {name}, on: bool) ="),
1587+ format!(" if on: {name}_insert(x, o) else: {name}_remove(x, o)"),
1588+ format!("proc rsBitOr_{name}_bitor*(a, b: {name}): {name} = {name}_union(a, b)"),
1589+ format!("proc rsBitAnd_{name}_bitand*(a, b: {name}): {name} = {name}_intersection(a, b)"),
1590+ format!("proc rsBitXor_{name}_bitxor*(a, b: {name}): {name} = {name}_symmetric_difference(a, b)"),
1591+ format!("proc rsSub_{name}_sub*(a, b: {name}): {name} = {name}_difference(a, b)"),
1592+ format!("proc rsNot_{name}_not*(a: {name}): {name} = {name}_complement(a)"),
1593+ ] {
1594+ self.line(&l);
1595+ }
1596+
1597+ // Debug prints the set flag names, or `0x0` when empty -- again
1598+ // matching the crate rather than a guess.
1599+ self.line(&format!("proc rsDebug*(x: {name}): string ="));
1600+ self.line(&format!(" result = \"{name}(\""));
1601+ self.line(" var first = true");
1602+ for (fname, _) in &def.flags {
1603+ self.line(&format!(
1604+ " if (x.bitsField and {name}{f}.bitsField) == {name}{f}.bitsField and {name}{f}.bitsField != {r}(0):",
1605+ f = fname
1606+ ));
1607+ self.line(" if not first: result.add(\" | \")");
1608+ self.line(&format!(" result.add(\"{fname}\")"));
1609+ self.line(" first = false");
1610+ }
1611+ self.line(" if first: result.add(\"0x0\")");
1612+ self.line(" result.add(\")\")");
1613+ self.blank();
1614+ }
1615+ Ok(())
1616+ }
1617+
1618+ fn bind_static_type(&mut self, _name: &str, _repr: &Nim) {}
1619+
1419 fn emit_enum(&mut self, def: &EnumDef) {1620 fn emit_enum(&mut self, def: &EnumDef) {
1420 let name = ident(&def.name);1621 let name = ident(&def.name);
1421 let g = Self::gen_list(1622 let g = Self::gen_list(
@@ -2860,6 +3061,18 @@ impl Lowerer {
2860 if name == "None" {3061 if name == "None" {
2861 return Ok(Val::new(self.none_of(expect), expect.cloned()));3062 return Ok(Val::new(self.none_of(expect), expect.cloned()));
2862 }3063 }
3064+ // `Perms::READ`: a constant of a `bitflags!` type.
3065+ if let Some(q) = p.path.segments.iter().rev().nth(1).map(|s| s.ident.to_string()) {
3066+ let q = if q == "Self" {
3067+ self.self_ty.as_ref().map(type_name).unwrap_or(q)
3068+ } else {
3069+ q
3070+ };
3071+ if let Some(c) = self.flag_consts.get(&(q.clone(), name.clone())) {
3072+ return Ok(Val::new(c.clone(), Some(Nim::Named(q, vec![]))));
3073+ }
3074+ }
3075+
2863 // `Grid::BORDER`: a `const` declared inside an `impl`.3076 // `Grid::BORDER`: a `const` declared inside an `impl`.
2864 if let Some(q) = p.path.segments.iter().rev().nth(1).map(|s| s.ident.to_string()) {3077 if let Some(q) = p.path.segments.iter().rev().nth(1).map(|s| s.ident.to_string()) {
2865 let q = if q == "Self" {3078 let q = if q == "Self" {
@@ -3313,7 +3526,12 @@ impl Lowerer {
3313 // Rust's `!` is logical on bool and bitwise-complement on integers.3526 // Rust's `!` is logical on bool and bitwise-complement on integers.
3314 // Nim spells those `not` and `not` as well, so one mapping covers3527 // Nim spells those `not` and `not` as well, so one mapping covers
3315 // both — but only because Nim overloads `not` the same way.3528 // both — but only because Nim overloads `not` the same way.
3316- UnOp::Not(_) => Ok(Val::new(format!("(not {})", v.code), v.ty)),3529+ UnOp::Not(_) => {
3530+ if let Some(f) = self.op_proc(&v.ty, "not") {
3531+ return Ok(Val::new(format!("{}({})", f, v.code), v.ty));
3532+ }
3533+ Ok(Val::new(format!("(not {})", v.code), v.ty))
3534+ }
3317 UnOp::Deref(_) => Ok(v),3535 UnOp::Deref(_) => Ok(v),
3318 _ => Err("unsupported unary operator".into()),3536 _ => Err("unsupported unary operator".into()),
3319 }3537 }
@@ -3933,6 +4151,19 @@ impl Lowerer {
3933 q4151 q
3934 };4152 };
3935 if let Some(sig) = self.methods.get(&(q.clone(), name.clone())) {4153 if let Some(sig) = self.methods.get(&(q.clone(), name.clone())) {
4154+ // Re-lower the arguments with the declared parameter types, so
4155+ // a literal takes the width the signature asks for.
4156+ let declared = sig.params.clone();
4157+ let mut args = args.clone();
4158+ let mut codes = codes.clone();
4159+ for (i, a) in c.args.iter().enumerate() {
4160+ if let Some(want) = declared.get(i) {
4161+ let want = want.clone().unvar();
4162+ args[i] = self.expr_at(a, Some(&want))?;
4163+ codes[i] = args[i].code.clone();
4164+ }
4165+ }
4166+ let sig = &self.methods[&(q.clone(), name.clone())];
3936 let arg_tys: Vec<Option<Nim>> = args.iter().map(|a| a.ty.clone()).collect();4167 let arg_tys: Vec<Option<Nim>> = args.iter().map(|a| a.ty.clone()).collect();
3937 let ret = Self::instantiate(sig, &arg_tys);4168 let ret = Self::instantiate(sig, &arg_tys);
3938 let nim = self4169 let nim = self
@@ -4086,6 +4317,34 @@ impl Lowerer {
4086 let a0 = args.first().map(|a| a.code.clone());4317 let a0 = args.first().map(|a| a.code.clone());
4087 let rt = recv.ty.clone();4318 let rt = recv.ty.clone();
4088 4319
4320+ // A method the input defines wins over our model of the standard
4321+ // library: `is_empty` on a `bitflags!` type is that type's, not the
4322+ // sequence one. Rust resolves inherent methods the same way.
4323+ if let Some(t) = &rt {
4324+ let key = (type_name(t), name.clone());
4325+ if self.methods.contains_key(&key) {
4326+ let declared = self.methods[&key].params.clone();
4327+ let skip = usize::from(declared.len() == m.args.len() + 1);
4328+ for (i, a) in m.args.iter().enumerate() {
4329+ if let Some(want) = declared.get(i + skip) {
4330+ let want = want.clone().unvar();
4331+ args[i] = self.expr_at(a, Some(&want))?;
4332+ }
4333+ }
4334+ let mut arg_tys: Vec<Option<Nim>> = vec![rt.clone()];
4335+ arg_tys.extend(args.iter().map(|a| a.ty.clone()));
4336+ let ret = Self::instantiate(&self.methods[&key], &arg_tys);
4337+ let nim = self
4338+ .statics
4339+ .get(&key)
4340+ .cloned()
4341+ .unwrap_or_else(|| ident(&name));
4342+ let mut all = vec![recv.code.clone()];
4343+ all.extend(args.iter().map(|a| a.code.clone()));
4344+ return Ok(Val::new(format!("{}({})", nim, all.join(", ")), Some(ret)));
4345+ }
4346+ }
4347+
4089 let (code, ty) = match name.as_str() {4348 let (code, ty) = match name.as_str() {
4090 // Rust's `len()` is `usize`; Nim's is `int`. The conversion is4349 // Rust's `len()` is `usize`; Nim's is `int`. The conversion is
4091 // explicit so that a `usize` binding type-checks on the Nim side.4350 // explicit so that a `usize` binding type-checks on the Nim side.
added src/macros.rs +64 -0
new file mode 100644
@@ -0,0 +1,64 @@
1+//! Shims for macros whose expansion cannot usefully be followed.
2+//!
3+//! The general rule in this project is that a macro whose expansion is not
4+//! known is rejected. `bitflags!` is an argued exception, and the argument is
5+//! this: expanding it does not help. `cargo rustc -Zunpretty=expanded` on a
6+//! one-flag user produces 869 lines that still call into
7+//! `bitflags::{Bits, Flag, Flags, iter, parser}` — which are themselves
8+//! defined by 26 further `macro_rules!` across five files. The chain does not
9+//! terminate in code we could lower.
10+//!
11+//! What the macro *means*, though, is small, documented and stable: a newtype
12+//! over an integer with named constants and set operations. So it is lowered
13+//! directly. That is not approximating a semantic we cannot represent — it is
14+//! implementing one we can, and `tests/cases/034-bitflags.rs` checks the
15+//! result against the real crate rather than against this comment.
16+//!
17+//! Verified against bitflags 2.13.2. Two behaviours are worth naming because
18+//! they are not what a reimplementation would guess: `!x` is complemented and
19+//! then masked to `all()`, and `from_bits` returns `None` for any bit outside
20+//! `all()`.
21+
22+use syn::parse::{Parse, ParseStream};
23+use syn::{braced, Expr, Ident, Token, Type, Visibility};
24+
25+/// One `pub struct Name: Repr { const A = ..; }` inside a `bitflags!`.
26+pub struct FlagsDef {
27+ pub name: Ident,
28+ pub repr: Type,
29+ pub flags: Vec<(Ident, Expr)>,
30+}
31+
32+/// The whole macro body, which may declare more than one type.
33+pub struct BitflagsInput(pub Vec<FlagsDef>);
34+
35+impl Parse for BitflagsInput {
36+ fn parse(input: ParseStream) -> syn::Result<Self> {
37+ let mut out = Vec::new();
38+ while !input.is_empty() {
39+ // Attributes on the struct (`#[derive(..)]`) carry no meaning we
40+ // need: the operations they derive are generated regardless.
41+ let _ = input.call(syn::Attribute::parse_outer)?;
42+ let _: Visibility = input.parse()?;
43+ input.parse::<Token![struct]>()?;
44+ let name: Ident = input.parse()?;
45+ input.parse::<Token![:]>()?;
46+ let repr: Type = input.parse()?;
47+
48+ let body;
49+ braced!(body in input);
50+ let mut flags = Vec::new();
51+ while !body.is_empty() {
52+ let _ = body.call(syn::Attribute::parse_outer)?;
53+ body.parse::<Token![const]>()?;
54+ let fname: Ident = body.parse()?;
55+ body.parse::<Token![=]>()?;
56+ let value: Expr = body.parse()?;
57+ body.parse::<Token![;]>()?;
58+ flags.push((fname, value));
59+ }
60+ out.push(FlagsDef { name, repr, flags });
61+ }
62+ Ok(BitflagsInput(out))
63+ }
64+}
new file mode 100644
@@ -0,0 +1,64 @@
1+//! Shims for macros whose expansion cannot usefully be followed.
2+//!
3+//! The general rule in this project is that a macro whose expansion is not
4+//! known is rejected. `bitflags!` is an argued exception, and the argument is
5+//! this: expanding it does not help. `cargo rustc -Zunpretty=expanded` on a
6+//! one-flag user produces 869 lines that still call into
7+//! `bitflags::{Bits, Flag, Flags, iter, parser}` — which are themselves
8+//! defined by 26 further `macro_rules!` across five files. The chain does not
9+//! terminate in code we could lower.
10+//!
11+//! What the macro *means*, though, is small, documented and stable: a newtype
12+//! over an integer with named constants and set operations. So it is lowered
13+//! directly. That is not approximating a semantic we cannot represent — it is
14+//! implementing one we can, and `tests/cases/034-bitflags.rs` checks the
15+//! result against the real crate rather than against this comment.
16+//!
17+//! Verified against bitflags 2.13.2. Two behaviours are worth naming because
18+//! they are not what a reimplementation would guess: `!x` is complemented and
19+//! then masked to `all()`, and `from_bits` returns `None` for any bit outside
20+//! `all()`.
21+
22+use syn::parse::{Parse, ParseStream};
23+use syn::{braced, Expr, Ident, Token, Type, Visibility};
24+
25+/// One `pub struct Name: Repr { const A = ..; }` inside a `bitflags!`.
26+pub struct FlagsDef {
27+ pub name: Ident,
28+ pub repr: Type,
29+ pub flags: Vec<(Ident, Expr)>,
30+}
31+
32+/// The whole macro body, which may declare more than one type.
33+pub struct BitflagsInput(pub Vec<FlagsDef>);
34+
35+impl Parse for BitflagsInput {
36+ fn parse(input: ParseStream) -> syn::Result<Self> {
37+ let mut out = Vec::new();
38+ while !input.is_empty() {
39+ // Attributes on the struct (`#[derive(..)]`) carry no meaning we
40+ // need: the operations they derive are generated regardless.
41+ let _ = input.call(syn::Attribute::parse_outer)?;
42+ let _: Visibility = input.parse()?;
43+ input.parse::<Token![struct]>()?;
44+ let name: Ident = input.parse()?;
45+ input.parse::<Token![:]>()?;
46+ let repr: Type = input.parse()?;
47+
48+ let body;
49+ braced!(body in input);
50+ let mut flags = Vec::new();
51+ while !body.is_empty() {
52+ let _ = body.call(syn::Attribute::parse_outer)?;
53+ body.parse::<Token![const]>()?;
54+ let fname: Ident = body.parse()?;
55+ body.parse::<Token![=]>()?;
56+ let value: Expr = body.parse()?;
57+ body.parse::<Token![;]>()?;
58+ flags.push((fname, value));
59+ }
60+ out.push(FlagsDef { name, repr, flags });
61+ }
62+ Ok(BitflagsInput(out))
63+ }
64+}
modified src/main.rs +1 -0
@@ -3,6 +3,7 @@
33 //! Usage: rustnim <input.rs> [-o <output.nim>]
44
55 mod fmt;
6+mod macros;
67 mod lower;
78 mod ty;
89
@@ -3,6 +3,7 @@
3 //! Usage: rustnim <input.rs> [-o <output.nim>]3 //! Usage: rustnim <input.rs> [-o <output.nim>]
4 4
5 mod fmt;5 mod fmt;
6+mod macros;
6 mod lower;7 mod lower;
7 mod ty;8 mod ty;
8 9
modified src/ty.rs +28 -0
@@ -239,6 +239,34 @@ pub fn map(t: &Type) -> Result<Nim, String> {
239239 .collect::<Result<_, _>>()?;
240240 Ok(Nim::Proc(args, Box::new(ret_ty(&f.output)?)))
241241 }
242+ // `dyn Fn(A) -> B` is a callable, exactly as `impl Fn(A) -> B` is.
243+ // Other trait objects need a vtable, which the lowering builds from
244+ // the trait's declaration; `dyn` is spelled `<Trait>Dyn` there.
245+ Type::TraitObject(t) => {
246+ for b in &t.bounds {
247+ if let TypeParamBound::Trait(tb) = b {
248+ if let Some(seg) = tb.path.segments.last() {
249+ let n = seg.ident.to_string();
250+ if n == "Fn" || n == "FnMut" || n == "FnOnce" {
251+ if let PathArguments::Parenthesized(a) = &seg.arguments {
252+ let args: Vec<Nim> = a
253+ .inputs
254+ .iter()
255+ .map(|a| map(&a.ty))
256+ .collect::<Result<_, _>>()?;
257+ return Ok(Nim::Proc(args, Box::new(ret_ty(&a.output)?)));
258+ }
259+ }
260+ // `dyn T + Send + Sync`: the auto traits carry no
261+ // methods, so the first real bound names the object.
262+ if !matches!(n.as_str(), "Send" | "Sync" | "Unpin" | "Sized") {
263+ return Ok(Nim::Named(format!("{n}Dyn"), vec![]));
264+ }
265+ }
266+ }
267+ }
268+ Err("a trait object with no nameable trait bound".into())
269+ }
242270 Type::ImplTrait(i) => {
243271 // `impl AsRef<[u8]>` and friends: fall back to the bound's own
244272 // shape where we can recognise it, since Nim has no impl-trait.
@@ -239,6 +239,34 @@ pub fn map(t: &Type) -> Result<Nim, String> {
239 .collect::<Result<_, _>>()?;239 .collect::<Result<_, _>>()?;
240 Ok(Nim::Proc(args, Box::new(ret_ty(&f.output)?)))240 Ok(Nim::Proc(args, Box::new(ret_ty(&f.output)?)))
241 }241 }
242+ // `dyn Fn(A) -> B` is a callable, exactly as `impl Fn(A) -> B` is.
243+ // Other trait objects need a vtable, which the lowering builds from
244+ // the trait's declaration; `dyn` is spelled `<Trait>Dyn` there.
245+ Type::TraitObject(t) => {
246+ for b in &t.bounds {
247+ if let TypeParamBound::Trait(tb) = b {
248+ if let Some(seg) = tb.path.segments.last() {
249+ let n = seg.ident.to_string();
250+ if n == "Fn" || n == "FnMut" || n == "FnOnce" {
251+ if let PathArguments::Parenthesized(a) = &seg.arguments {
252+ let args: Vec<Nim> = a
253+ .inputs
254+ .iter()
255+ .map(|a| map(&a.ty))
256+ .collect::<Result<_, _>>()?;
257+ return Ok(Nim::Proc(args, Box::new(ret_ty(&a.output)?)));
258+ }
259+ }
260+ // `dyn T + Send + Sync`: the auto traits carry no
261+ // methods, so the first real bound names the object.
262+ if !matches!(n.as_str(), "Send" | "Sync" | "Unpin" | "Sized") {
263+ return Ok(Nim::Named(format!("{n}Dyn"), vec![]));
264+ }
265+ }
266+ }
267+ }
268+ Err("a trait object with no nameable trait bound".into())
269+ }
242 Type::ImplTrait(i) => {270 Type::ImplTrait(i) => {
243 // `impl AsRef<[u8]>` and friends: fall back to the bound's own271 // `impl AsRef<[u8]>` and friends: fall back to the bound's own
244 // shape where we can recognise it, since Nim has no impl-trait.272 // shape where we can recognise it, since Nim has no impl-trait.
added tests/cases/034-bitflags.rs +41 -0
new file mode 100644
@@ -0,0 +1,41 @@
1+//@ extern: bitflags
2+// `bitflags!` is lowered directly rather than expanded. Expanding it does
3+// not help: on a one-flag user it produces 869 lines that still call into
4+// `bitflags::{Bits, Flag, Flags, iter, parser}`, which are themselves 26
5+// further `macro_rules!` across five files. What the macro *means* is small
6+// and stable, so it is implemented; this case is what checks it, against
7+// bitflags 2.13.2 rather than against anyone's memory of it.
8+//
9+// Note `!rw` is complemented and masked to `all()`, and `from_bits` is None
10+// for any bit outside `all()`. Neither is what a reimplementation guesses.
11+use bitflags::bitflags;
12+bitflags! {
13+ #[derive(Copy, Clone, Debug, PartialEq, Eq)]
14+ pub struct Perms: u32 {
15+ const READ = 0b0000_0001;
16+ const WRITE = 0b0000_0010;
17+ const EXEC = 0b0000_0100;
18+ }
19+}
20+fn main() {
21+ let r = Perms::READ;
22+ let rw = Perms::READ | Perms::WRITE;
23+ let all = Perms::all();
24+ println!("bits {} {} {} {}", r.bits(), rw.bits(), all.bits(), Perms::empty().bits());
25+ println!("contains {} {}", rw.contains(Perms::READ), rw.contains(Perms::EXEC));
26+ println!("intersects {} {}", rw.intersects(Perms::EXEC), rw.intersects(Perms::READ));
27+ println!("empty {} {}", rw.is_empty(), Perms::empty().is_empty());
28+ println!("all {} {}", rw.is_all(), all.is_all());
29+ println!("ops {} {} {} {}", (rw & Perms::WRITE).bits(), (rw | Perms::EXEC).bits(), (rw ^ Perms::READ).bits(), (!rw).bits());
30+ println!("diff {} {}", all.difference(Perms::WRITE).bits(), rw.symmetric_difference(Perms::EXEC).bits());
31+ println!("union {} inter {}", r.union(Perms::EXEC).bits(), all.intersection(rw).bits());
32+ let mut m = Perms::empty();
33+ m.insert(Perms::READ); m.insert(Perms::EXEC); m.remove(Perms::READ); m.toggle(Perms::WRITE);
34+ println!("mut {}", m.bits());
35+ m.set(Perms::EXEC, false);
36+ println!("set {}", m.bits());
37+ println!("from_bits {:?} {:?}", Perms::from_bits(3), Perms::from_bits(8));
38+ println!("truncate {}", Perms::from_bits_truncate(9).bits());
39+ println!("debug {:?} {:?} {:?}", r, rw, Perms::empty());
40+ println!("eq {} {}", r == Perms::READ, r == rw);
41+}
new file mode 100644
@@ -0,0 +1,41 @@
1+//@ extern: bitflags
2+// `bitflags!` is lowered directly rather than expanded. Expanding it does
3+// not help: on a one-flag user it produces 869 lines that still call into
4+// `bitflags::{Bits, Flag, Flags, iter, parser}`, which are themselves 26
5+// further `macro_rules!` across five files. What the macro *means* is small
6+// and stable, so it is implemented; this case is what checks it, against
7+// bitflags 2.13.2 rather than against anyone's memory of it.
8+//
9+// Note `!rw` is complemented and masked to `all()`, and `from_bits` is None
10+// for any bit outside `all()`. Neither is what a reimplementation guesses.
11+use bitflags::bitflags;
12+bitflags! {
13+ #[derive(Copy, Clone, Debug, PartialEq, Eq)]
14+ pub struct Perms: u32 {
15+ const READ = 0b0000_0001;
16+ const WRITE = 0b0000_0010;
17+ const EXEC = 0b0000_0100;
18+ }
19+}
20+fn main() {
21+ let r = Perms::READ;
22+ let rw = Perms::READ | Perms::WRITE;
23+ let all = Perms::all();
24+ println!("bits {} {} {} {}", r.bits(), rw.bits(), all.bits(), Perms::empty().bits());
25+ println!("contains {} {}", rw.contains(Perms::READ), rw.contains(Perms::EXEC));
26+ println!("intersects {} {}", rw.intersects(Perms::EXEC), rw.intersects(Perms::READ));
27+ println!("empty {} {}", rw.is_empty(), Perms::empty().is_empty());
28+ println!("all {} {}", rw.is_all(), all.is_all());
29+ println!("ops {} {} {} {}", (rw & Perms::WRITE).bits(), (rw | Perms::EXEC).bits(), (rw ^ Perms::READ).bits(), (!rw).bits());
30+ println!("diff {} {}", all.difference(Perms::WRITE).bits(), rw.symmetric_difference(Perms::EXEC).bits());
31+ println!("union {} inter {}", r.union(Perms::EXEC).bits(), all.intersection(rw).bits());
32+ let mut m = Perms::empty();
33+ m.insert(Perms::READ); m.insert(Perms::EXEC); m.remove(Perms::READ); m.toggle(Perms::WRITE);
34+ println!("mut {}", m.bits());
35+ m.set(Perms::EXEC, false);
36+ println!("set {}", m.bits());
37+ println!("from_bits {:?} {:?}", Perms::from_bits(3), Perms::from_bits(8));
38+ println!("truncate {}", Perms::from_bits_truncate(9).bits());
39+ println!("debug {:?} {:?} {:?}", r, rw, Perms::empty());
40+ println!("eq {} {}", r == Perms::READ, r == rw);
41+}
modified tests/differential.rs +49 -3
@@ -113,6 +113,10 @@ struct Directives {
113113 stdin: Option<String>,
114114 /// `--cfg` flags for rustnim. rustc gets `--cfg feature="x"` to match.
115115 cfg: Vec<String>,
116+ /// Crates the *oracle* must link. rustnim does not use them — the point
117+ /// of such a case is that rustnim reproduces the crate's behaviour
118+ /// without it.
119+ externs: Vec<String>,
116120 }
117121
118122 fn directives(src: &str) -> Directives {
@@ -137,6 +141,7 @@ fn directives(src: &str) -> Directives {
137141 "skip" => d.skip = Some(val),
138142 "args" => d.args = val.split_whitespace().map(str::to_string).collect(),
139143 "cfg" => d.cfg.push(val),
144+ "extern" => d.externs.push(val),
140145 "stdin" => d.stdin = Some(format!("{val}\n")),
141146 _ => {}
142147 }
@@ -144,6 +149,38 @@ fn directives(src: &str) -> Directives {
144149 d
145150 }
146151
152+/// Locate a dependency's rlib among cargo's build artefacts. It is there
153+/// because it is a dev-dependency of this crate, so cargo has already built
154+/// it by the time the tests run.
155+fn find_rlib(name: &str) -> Result<(PathBuf, PathBuf), String> {
156+ // CARGO_BIN_EXE_* points at target/<profile>/<bin>, so deps/ is beside it.
157+ let bin = Path::new(RUSTNIM);
158+ let deps = bin
159+ .parent()
160+ .ok_or("no target directory")?
161+ .join("deps");
162+ let prefix = format!("lib{name}-");
163+ let mut best: Option<PathBuf> = None;
164+ for e in fs::read_dir(&deps).map_err(|e| format!("{}: {e}", deps.display()))? {
165+ let p = e.map_err(|e| e.to_string())?.path();
166+ let f = p.file_name().unwrap_or_default().to_string_lossy().into_owned();
167+ if f.starts_with(&prefix) && f.ends_with(".rlib") {
168+ let newer = match &best {
169+ None => true,
170+ Some(b) => {
171+ fs::metadata(&p).and_then(|m| m.modified()).ok()
172+ > fs::metadata(b).and_then(|m| m.modified()).ok()
173+ }
174+ };
175+ if newer {
176+ best = Some(p);
177+ }
178+ }
179+ }
180+ best.map(|p| (p, deps.clone()))
181+ .ok_or_else(|| format!("no {prefix}*.rlib under {}", deps.display()))
182+}
183+
147184 /// Byte-for-byte diff, rendered readably: show the first differing line with
148185 /// escapes, so a trailing-newline or whitespace difference is visible.
149186 fn diff_report(want: &[u8], got: &[u8]) -> String {
@@ -275,10 +312,19 @@ fn run_case(case: &Path, work: &Path, nim: &Path) -> Outcome {
275312
276313 // -- stage: rustc. No -O: debug profile, overflow checks on.
277314 let rs_bin = dir.join("rs.bin");
315+ let mut rustc = Command::new("rustc");
316+ rustc.arg("--edition=2021").arg("-A").arg("warnings");
317+ for c in &d.externs {
318+ match find_rlib(c) {
319+ Ok((rlib, deps)) => {
320+ rustc.arg("--extern").arg(format!("{c}={}", rlib.display()));
321+ rustc.arg("-L").arg(format!("dependency={}", deps.display()));
322+ }
323+ Err(e) => return Outcome::fail("setup", format!("`//@ extern: {c}`: {e}")),
324+ }
325+ }
278326 let rc = match run(
279- Command::new("rustc")
280- .arg("--edition=2021")
281- .arg("-A").arg("warnings")
327+ rustc
282328 .args(d.cfg.iter().flat_map(|c| {
283329 // rustc spells it `feature="x"`; the directive uses the
284330 // rustnim form, so it is rewritten here.
@@ -113,6 +113,10 @@ struct Directives {
113 stdin: Option<String>,113 stdin: Option<String>,
114 /// `--cfg` flags for rustnim. rustc gets `--cfg feature="x"` to match.114 /// `--cfg` flags for rustnim. rustc gets `--cfg feature="x"` to match.
115 cfg: Vec<String>,115 cfg: Vec<String>,
116+ /// Crates the *oracle* must link. rustnim does not use them — the point
117+ /// of such a case is that rustnim reproduces the crate's behaviour
118+ /// without it.
119+ externs: Vec<String>,
116 }120 }
117 121
118 fn directives(src: &str) -> Directives {122 fn directives(src: &str) -> Directives {
@@ -137,6 +141,7 @@ fn directives(src: &str) -> Directives {
137 "skip" => d.skip = Some(val),141 "skip" => d.skip = Some(val),
138 "args" => d.args = val.split_whitespace().map(str::to_string).collect(),142 "args" => d.args = val.split_whitespace().map(str::to_string).collect(),
139 "cfg" => d.cfg.push(val),143 "cfg" => d.cfg.push(val),
144+ "extern" => d.externs.push(val),
140 "stdin" => d.stdin = Some(format!("{val}\n")),145 "stdin" => d.stdin = Some(format!("{val}\n")),
141 _ => {}146 _ => {}
142 }147 }
@@ -144,6 +149,38 @@ fn directives(src: &str) -> Directives {
144 d149 d
145 }150 }
146 151
152+/// Locate a dependency's rlib among cargo's build artefacts. It is there
153+/// because it is a dev-dependency of this crate, so cargo has already built
154+/// it by the time the tests run.
155+fn find_rlib(name: &str) -> Result<(PathBuf, PathBuf), String> {
156+ // CARGO_BIN_EXE_* points at target/<profile>/<bin>, so deps/ is beside it.
157+ let bin = Path::new(RUSTNIM);
158+ let deps = bin
159+ .parent()
160+ .ok_or("no target directory")?
161+ .join("deps");
162+ let prefix = format!("lib{name}-");
163+ let mut best: Option<PathBuf> = None;
164+ for e in fs::read_dir(&deps).map_err(|e| format!("{}: {e}", deps.display()))? {
165+ let p = e.map_err(|e| e.to_string())?.path();
166+ let f = p.file_name().unwrap_or_default().to_string_lossy().into_owned();
167+ if f.starts_with(&prefix) && f.ends_with(".rlib") {
168+ let newer = match &best {
169+ None => true,
170+ Some(b) => {
171+ fs::metadata(&p).and_then(|m| m.modified()).ok()
172+ > fs::metadata(b).and_then(|m| m.modified()).ok()
173+ }
174+ };
175+ if newer {
176+ best = Some(p);
177+ }
178+ }
179+ }
180+ best.map(|p| (p, deps.clone()))
181+ .ok_or_else(|| format!("no {prefix}*.rlib under {}", deps.display()))
182+}
183+
147 /// Byte-for-byte diff, rendered readably: show the first differing line with184 /// Byte-for-byte diff, rendered readably: show the first differing line with
148 /// escapes, so a trailing-newline or whitespace difference is visible.185 /// escapes, so a trailing-newline or whitespace difference is visible.
149 fn diff_report(want: &[u8], got: &[u8]) -> String {186 fn diff_report(want: &[u8], got: &[u8]) -> String {
@@ -275,10 +312,19 @@ fn run_case(case: &Path, work: &Path, nim: &Path) -> Outcome {
275 312
276 // -- stage: rustc. No -O: debug profile, overflow checks on.313 // -- stage: rustc. No -O: debug profile, overflow checks on.
277 let rs_bin = dir.join("rs.bin");314 let rs_bin = dir.join("rs.bin");
315+ let mut rustc = Command::new("rustc");
316+ rustc.arg("--edition=2021").arg("-A").arg("warnings");
317+ for c in &d.externs {
318+ match find_rlib(c) {
319+ Ok((rlib, deps)) => {
320+ rustc.arg("--extern").arg(format!("{c}={}", rlib.display()));
321+ rustc.arg("-L").arg(format!("dependency={}", deps.display()));
322+ }
323+ Err(e) => return Outcome::fail("setup", format!("`//@ extern: {c}`: {e}")),
324+ }
325+ }
278 let rc = match run(326 let rc = match run(
279- Command::new("rustc")327+ rustc
280- .arg("--edition=2021")
281- .arg("-A").arg("warnings")
282 .args(d.cfg.iter().flat_map(|c| {328 .args(d.cfg.iter().flat_map(|c| {
283 // rustc spells it `feature="x"`; the directive uses the329 // rustc spells it `feature="x"`; the directive uses the
284 // rustnim form, so it is rewritten here.330 // rustnim form, so it is rewritten here.