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>af6e50f parent: 8354895 modified
Cargo.lock +7 -0 | @@ -2,6 +2,12 @@ | ||
| 2 | 2 | # It is not intended for manual editing. |
| 3 | 3 | 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 | 11 | [[package]] |
| 6 | 12 | name = "proc-macro2" |
| 7 | 13 | version = "1.0.107" |
| @@ -24,6 +30,7 @@ dependencies = [ | ||
| 24 | 30 | name = "rustnim" |
| 25 | 31 | version = "0.1.0" |
| 26 | 32 | dependencies = [ |
| 33 | + "bitflags", | |
| 27 | 34 | "syn", |
| 28 | 35 | ] |
| 29 | 36 | |
| @@ -2,6 +2,12 @@ | |||
| 2 | # It is not intended for manual editing. | 2 | # It is not intended for manual editing. |
| 3 | version = 4 | 3 | 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" | ||
| 5 | 5 | |
| 6 | 6 | [dependencies] |
| 7 | 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" | |
| @@ -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 | ||
| 478 | 478 | crate). `mode.rs` needs `cosmic-config` and its derive macro. Those are |
| 479 | 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 | 519 | ## Proof of byte-identity for `base16ct` |
| 482 | 520 | |
| 483 | 521 | [`PROOF.md`](PROOF.md) sets out what is actually established: exhaustive |
| @@ -523,6 +561,8 @@ Cases carry directives in leading `//@` comments: | ||
| 523 | 561 | | `//@ skip: <reason>` | not run; reported as skipped | |
| 524 | 562 | | `//@ args: <argv>` | passed to both binaries | |
| 525 | 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 | 567 | `reject` cases are how the "fail loudly" rule is tested rather than merely |
| 528 | 568 | 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 are | 478 | 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: exhaustive | 521 | [`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 merely | 567 | `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-library | 568 | 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. | ||
| 77 | 77 | `cargo test --test proof` runs it — 151,463 cases, 9.1 MB of output, compared |
| 78 | 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 | 89 | ## Does it generalise? |
| 81 | 90 | |
| 82 | 91 | `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, compared | 77 | `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 { | ||
| 213 | 213 | assoc: HashMap<(String, String), Nim>, |
| 214 | 214 | /// `(type, name) -> (nim name, type)` for `const` items inside an `impl`. |
| 215 | 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 | 220 | /// `use` brings a name into scope from another module. Flattening loses |
| 217 | 221 | /// the module structure, so the mapping is recorded and consulted when a |
| 218 | 222 | /// bare call is resolved. |
| @@ -288,6 +292,8 @@ impl Lowerer { | ||
| 288 | 292 | type_generics: HashMap::new(), |
| 289 | 293 | assoc: HashMap::new(), |
| 290 | 294 | assoc_consts: HashMap::new(), |
| 295 | + bitflags: std::collections::HashSet::new(), | |
| 296 | + flag_consts: HashMap::new(), | |
| 291 | 297 | use_map: HashMap::new(), |
| 292 | 298 | structs: HashMap::new(), |
| 293 | 299 | enums: HashMap::new(), |
| @@ -554,6 +560,9 @@ impl Lowerer { | ||
| 554 | 560 | } |
| 555 | 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 | 566 | Item::Mod(m) if m.content.is_some() => { |
| 558 | 567 | let items = m.content.as_ref().map(|(_, i)| i.clone()).unwrap_or_default(); |
| 559 | 568 | for i in &items { |
| @@ -1178,6 +1187,7 @@ impl Lowerer { | ||
| 1178 | 1187 | } |
| 1179 | 1188 | match item { |
| 1180 | 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 | 1191 | Item::Mod(m) if m.content.is_some() => { |
| 1182 | 1192 | let items = m.content.as_ref().map(|(_, i)| i.clone()).unwrap_or_default(); |
| 1183 | 1193 | for i in &items { |
| @@ -1197,6 +1207,9 @@ impl Lowerer { | ||
| 1197 | 1207 | if matches!(item, Item::Struct(_) | Item::Enum(_) | Item::Const(_)) { |
| 1198 | 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 | 1213 | self.item_inner(item) |
| 1201 | 1214 | } |
| 1202 | 1215 | |
| @@ -1225,6 +1238,7 @@ impl Lowerer { | ||
| 1225 | 1238 | self.blank(); |
| 1226 | 1239 | Ok(()) |
| 1227 | 1240 | } |
| 1241 | + Item::Macro(m) if path_name(&m.mac.path) == "bitflags" => self.emit_bitflags(&m.mac), | |
| 1228 | 1242 | Item::Type(_) => Ok(()), // expanded at every use site |
| 1229 | 1243 | Item::Trait(t) => { |
| 1230 | 1244 | // 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 | 1620 | fn emit_enum(&mut self, def: &EnumDef) { |
| 1420 | 1621 | let name = ident(&def.name); |
| 1421 | 1622 | let g = Self::gen_list( |
| @@ -2860,6 +3061,18 @@ impl Lowerer { | ||
| 2860 | 3061 | if name == "None" { |
| 2861 | 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 | 3076 | // `Grid::BORDER`: a `const` declared inside an `impl`. |
| 2864 | 3077 | if let Some(q) = p.path.segments.iter().rev().nth(1).map(|s| s.ident.to_string()) { |
| 2865 | 3078 | let q = if q == "Self" { |
| @@ -3313,7 +3526,12 @@ impl Lowerer { | ||
| 3313 | 3526 | // Rust's `!` is logical on bool and bitwise-complement on integers. |
| 3314 | 3527 | // Nim spells those `not` and `not` as well, so one mapping covers |
| 3315 | 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 | 3535 | UnOp::Deref(_) => Ok(v), |
| 3318 | 3536 | _ => Err("unsupported unary operator".into()), |
| 3319 | 3537 | } |
| @@ -3933,6 +4151,19 @@ impl Lowerer { | ||
| 3933 | 4151 | q |
| 3934 | 4152 | }; |
| 3935 | 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 | 4167 | let arg_tys: Vec<Option<Nim>> = args.iter().map(|a| a.ty.clone()).collect(); |
| 3937 | 4168 | let ret = Self::instantiate(sig, &arg_tys); |
| 3938 | 4169 | let nim = self |
| @@ -4086,6 +4317,34 @@ impl Lowerer { | ||
| 4086 | 4317 | let a0 = args.first().map(|a| a.code.clone()); |
| 4087 | 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 | 4348 | let (code, ty) = match name.as_str() { |
| 4090 | 4349 | // Rust's `len()` is `usize`; Nim's is `int`. The conversion is |
| 4091 | 4350 | // 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 loses | 220 | /// `use` brings a name into scope from another module. Flattening loses |
| 217 | /// the module structure, so the mapping is recorded and consulted when a | 221 | /// 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 site | 1242 | 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 generates | 1244 | // 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 covers | 3527 | // 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 | q | 4151 | 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 = self | 4169 | 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 is | 4349 | // 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 @@ | ||
| 3 | 3 | //! Usage: rustnim <input.rs> [-o <output.nim>] |
| 4 | 4 | |
| 5 | 5 | mod fmt; |
| 6 | +mod macros; | |
| 6 | 7 | mod lower; |
| 7 | 8 | mod ty; |
| 8 | 9 | |
| @@ -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> { | ||
| 239 | 239 | .collect::<Result<_, _>>()?; |
| 240 | 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 | 270 | Type::ImplTrait(i) => { |
| 243 | 271 | // `impl AsRef<[u8]>` and friends: fall back to the bound's own |
| 244 | 272 | // 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 own | 271 | // `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 { | ||
| 113 | 113 | stdin: Option<String>, |
| 114 | 114 | /// `--cfg` flags for rustnim. rustc gets `--cfg feature="x"` to match. |
| 115 | 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 | 122 | fn directives(src: &str) -> Directives { |
| @@ -137,6 +141,7 @@ fn directives(src: &str) -> Directives { | ||
| 137 | 141 | "skip" => d.skip = Some(val), |
| 138 | 142 | "args" => d.args = val.split_whitespace().map(str::to_string).collect(), |
| 139 | 143 | "cfg" => d.cfg.push(val), |
| 144 | + "extern" => d.externs.push(val), | |
| 140 | 145 | "stdin" => d.stdin = Some(format!("{val}\n")), |
| 141 | 146 | _ => {} |
| 142 | 147 | } |
| @@ -144,6 +149,38 @@ fn directives(src: &str) -> Directives { | ||
| 144 | 149 | 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 | 184 | /// Byte-for-byte diff, rendered readably: show the first differing line with |
| 148 | 185 | /// escapes, so a trailing-newline or whitespace difference is visible. |
| 149 | 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 | 313 | // -- stage: rustc. No -O: debug profile, overflow checks on. |
| 277 | 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 | 326 | let rc = match run( |
| 279 | - Command::new("rustc") | |
| 280 | - .arg("--edition=2021") | |
| 281 | - .arg("-A").arg("warnings") | |
| 327 | + rustc | |
| 282 | 328 | .args(d.cfg.iter().flat_map(|c| { |
| 283 | 329 | // rustc spells it `feature="x"`; the directive uses the |
| 284 | 330 | // 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 | d | 149 | 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 with | 184 | /// 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 the | 329 | // 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. |