diff --git a/bootstrap/src/compiler.rs b/bootstrap/src/compiler.rs index 4bc4c37081..44b58079b6 100644 --- a/bootstrap/src/compiler.rs +++ b/bootstrap/src/compiler.rs @@ -21942,6 +21942,23 @@ fn collect_bool_fns(node: &Node, out: &mut std::collections::HashSet) { } } +/// Collect the name of EVERY function declared anywhere in the tree. +/// +/// Deliberately not keyed on the return type. `collect_fn_ret_types` skips a +/// declaration whose `extra_return_type` is empty, and 3 of the 30 corpus +/// declarations that shadow a math builtin use the Zig-style return syntax +/// (`pub fn min(a: usize, b: usize) usize`) rather than `->`. A guard built on +/// that map would therefore be narrower than its subject exactly where it is +/// needed, so this walks `FnDecl` unconditionally. +fn collect_declared_fns(node: &Node, out: &mut std::collections::HashSet) { + if node.kind == NodeKind::FnDecl { + out.insert(node.name.clone()); + } + for child in &node.children { + collect_declared_fns(child, out); + } +} + /// Collect names of locals declared with an explicit `bool` type. /// Is this expression bool-valued from its SHAPE alone? /// @@ -23657,6 +23674,15 @@ pub struct RustCodegen { fn_ret_type: String, /// Functions in this module whose declared return type is `bool`. bool_fns: std::collections::HashSet, + /// True while the initialiser of a `pub const` is being emitted. Rust's + /// float methods are not `const fn`, so a builtin rewrite is invalid there + /// in any spelling: `(x).sqrt()` is E0015 where the bare `sqrt(x)` was + /// E0425. Both fail, but only the first is a NEW failure introduced here. + in_const_init: bool, + /// Every function name this file declares, whatever its return type. + /// Guards the bare math-builtin lowering: a spec's own `fn abs` must keep + /// its call, not become `(x).abs()`. + declared_fns: std::collections::HashSet, /// Parameters and locals of the current function declared `bool`. bool_vars: std::collections::HashSet, /// Module-level `var x : bool`. `bool_vars` is cleared per function and @@ -23714,6 +23740,8 @@ impl RustCodegen { mut_names: std::collections::HashSet::new(), fn_ret_type: String::new(), bool_fns: std::collections::HashSet::new(), + declared_fns: std::collections::HashSet::new(), + in_const_init: false, bool_vars: std::collections::HashSet::new(), bool_module_vars: std::collections::HashSet::new(), bool_fields: std::collections::HashSet::new(), @@ -23829,6 +23857,10 @@ impl RustCodegen { self.bool_fns.clear(); collect_bool_fns(ast, &mut self.bool_fns); + // Same pre-pass, for the bare math-builtin guard below. + self.declared_fns.clear(); + collect_declared_fns(ast, &mut self.declared_fns); + // Same pre-pass, for the integer widths used by `infer_int_type`: // callee return types and module-level constants are both visible from // any function body, so they are collected once over the whole tree. @@ -24084,7 +24116,10 @@ impl RustCodegen { let value = if node.children.is_empty() { "()".to_string() } else { - self.expr_to_rust(&node.children[0]) + self.in_const_init = !node.extra_mutable; + let v = self.expr_to_rust(&node.children[0]); + self.in_const_init = false; + v }; // A module-level `var` is MUTABLE. gen-verilog lowers it to a `reg`, // gen-c to a `static`, Zig to a `var` -- all three mean shared mutable @@ -25314,6 +25349,74 @@ impl RustCodegen { if let Some(built) = Self::zig_builtin_to_rust(&node.name, &args) { return built; } + // Specs write the math builtins BARE -- `abs(x)`, `min(a, b)` -- + // and the table above answers only to Zig's `@abs`, because its + // first line returns None for any name without the sigil. Rust + // has none of these as free functions, so the bare call was + // emitted verbatim and rustc replied "cannot find function `abs` + // in this scope". Measured: 118 of the 650 specs contain such a + // call and 99 of those parse. + // + // The repair routes the bare name through the SAME table rather + // than restating it, so the two spellings cannot drift apart. + // The Zig backend closed this identical class at `gen_expr`. + // + // Guarded by `declared_fns`: 30 declarations across the corpus + // give one of these names to a spec's own function (`fn floor` in + // 10 specs, `fn abs` in 9). Rewriting those into a Rust method + // would be a wrong translation that COMPILES, which is strictly + // worse than a bare name that does not. + // The receiver must carry a type. Rust cannot call a method on + // an unsuffixed float literal -- `(5.0).sqrt()` is E0689, "can't + // call method `sqrt` on ambiguous numeric type `{float}`" -- + // whereas the bare `sqrt(5.0)` merely fails with E0425 as it + // always did. Measured over the corpus: without this guard the + // rewrite swapped one error for another in 4 files and added + // E0689 where none existed, which is a regression even though + // neither form compiled. `sqrt` is also not a `const fn`, so a + // literal receiver inside a `const` initialiser could not work + // in any spelling. + // The test is for an IDENTIFIER, not for a single literal token. + // `(5.0).sqrt()` and `((2.0 / 3.141592653589793)).sqrt()` are + // both `{float}` to rustc; only a typed name rules the ambiguity + // out. Measured: the single-token form of this test still + // admitted the compound one and left E0689 in 2 files. + let receiver_is_typed = args + .first() + .map(|a| a.chars().any(|c| c.is_ascii_alphabetic())) + .unwrap_or(false); + if receiver_is_typed + && !self.in_const_init + && !self.declared_fns.contains(&node.name) + && matches!( + node.name.as_str(), + "abs" + | "sqrt" + | "round" + | "floor" + | "ceil" + | "trunc" + | "exp" + // `log` is deliberately absent. Rust's `f64::log` + // takes a BASE argument, so `(x).log()` is E0061, + // "this method takes 1 argument but 0 were + // supplied"; the natural log is `ln`. The existing + // `@log` arm in `zig_builtin_to_rust` carries the + // same defect. It is filed rather than changed + // under an unrelated title. + | "sin" + | "cos" + | "tan" + | "min" + | "max" + ) + { + if let Some(built) = + Self::zig_builtin_to_rust(&format!("@{}", node.name), &args) + { + return built; + } + } format!("{}({})", node.name, args.join(", ")) } NodeKind::ExprArrayLiteral => { diff --git a/bootstrap/stage0/FROZEN_HASH b/bootstrap/stage0/FROZEN_HASH index c772167561..6a3b289eb2 100644 --- a/bootstrap/stage0/FROZEN_HASH +++ b/bootstrap/stage0/FROZEN_HASH @@ -1 +1 @@ -745cf51f9c7e719065ddb9fbabc682d9425687883062212d4a63ba72cef759d1 bootstrap/src/compiler.rs +8dca9dd2df9413543e14977a91584195cfaaa47b7e039c379efc8e1eb5fa2863 bootstrap/src/compiler.rs diff --git a/bootstrap/tests/backend_behaviour.rs b/bootstrap/tests/backend_behaviour.rs index 2612e2cf84..d686f780a6 100644 --- a/bootstrap/tests/backend_behaviour.rs +++ b/bootstrap/tests/backend_behaviour.rs @@ -286,3 +286,86 @@ fn rust_keeps_the_arms_of_a_switch() { "an empty match reached the output:\n{src}" ); } + +/// The emitted Rust as TEXT, for the one property that cannot be observed by +/// running: what a file that deliberately does NOT compile was emitted as. +fn rust_text(spec: &str, tag: &str) -> Option { + let dir = tmp_dir(tag); + Some(generate("gen-rust", spec, &dir, tag)) +} + +/// Specs write the math builtins bare. `zig_builtin_to_rust` knew all of them +/// and its first line returned `None` for any name without a `@`, so the bare +/// call reached rustc verbatim: "cannot find function `abs` in this scope". +const SPEC_BARE_MATH_BUILTINS: &str = r#" +module builtins_probe { + fn mix(x: f64) -> f64 { + return abs(x) + sqrt(y4()) + floor(y27()) + ceil(y21()) + round(y25()) + min(x, y9()) + max(x, y9()); + } + fn y4() -> f64 { return 4.0; } + fn y27() -> f64 { return 2.7; } + fn y21() -> f64 { return 2.1; } + fn y25() -> f64 { return 2.5; } + fn y9() -> f64 { return 9.0; } +} +"#; + +/// The guard. A spec may name its own function `abs`, and 30 declarations in +/// the corpus do exactly that (`fn floor` in 10 specs, `fn abs` in 9). This +/// one is deliberately NOT absolute value, so a wrong redirect to `f64::abs` +/// changes the printed answer instead of merely changing the text. +const SPEC_USER_DEFINED_ABS: &str = r#" +module guard_probe { + fn abs(x: f64) -> f64 { return x + 100.0; } + fn call_it(y: f64) -> f64 { return abs(y); } +} +"#; + +#[test] +fn rust_lowers_bare_math_builtins_to_methods() { + // -3 -> 3, sqrt 4 -> 2, floor 2.7 -> 2, ceil 2.1 -> 3, round 2.5 -> 3, + // min(-3, 9) -> -3, max(-3, 9) -> 9. 3 + 2 + 2 + 3 + 3 - 3 + 9 = 19. + let Some(out) = rust_says( + SPEC_BARE_MATH_BUILTINS, + "fn main(){ println!(\"{}\", mix(-3.0)); }\n", + "rust-bare-math-builtins", + ) else { + return; + }; + assert_eq!(out, "19", "a bare `abs(` does not compile in Rust at all"); +} + +#[test] +fn a_spec_declaring_its_own_abs_keeps_its_own_abs() { + // The spec's `abs` adds 100. Redirecting the call to `f64::abs` would + // print 3 -- a wrong translation that COMPILES, which is strictly worse + // than the bare name that does not. This test fails on 3 and passes on 97. + let Some(out) = rust_says( + SPEC_USER_DEFINED_ABS, + "fn main(){ println!(\"{}\", call_it(-3.0)); }\n", + "rust-user-defined-abs", + ) else { + return; + }; + assert_eq!(out, "97", "the spec's own `fn abs` must win over the builtin"); +} + +#[test] +fn a_literal_receiver_is_left_bare() { + // `(5.0).sqrt()` is E0689, "can't call method `sqrt` on ambiguous numeric + // type `{float}`", and `sqrt` is not a `const fn` either, so no spelling of + // a builtin works in a `const` initialiser. Leaving the bare call there + // keeps the pre-existing E0425 rather than trading it for a new failure. + let src = "module lit_probe {\n const K : f64 = sqrt(5.0);\n fn g(x: f64) -> f64 { return sqrt(x); }\n}\n"; + let Some(text) = rust_text(src, "rust-literal-receiver") else { + return; + }; + assert!( + text.contains("sqrt(5.0)"), + "a literal receiver must stay bare, got:\n{text}" + ); + assert!( + text.contains("(x).sqrt()"), + "a typed receiver must become a method call, got:\n{text}" + ); +} diff --git a/docs/now/2026-09-08-a-backend-that-knew-the-answer-and-never-was-asked.md b/docs/now/2026-09-08-a-backend-that-knew-the-answer-and-never-was-asked.md new file mode 100644 index 0000000000..469ed220f4 --- /dev/null +++ b/docs/now/2026-09-08-a-backend-that-knew-the-answer-and-never-was-asked.md @@ -0,0 +1,10 @@ +# NOW -- A backend that knew the answer and never was asked (2026-09-08) + +## A backend that knew the answer and never was asked (Closes #3400) + +- `gen-rust` emitted the math builtins as bare free functions -- `abs(x)`, `min(a, b)` -- which Rust does not have, so rustc replied `error[E0425]: cannot find function abs in this scope`. `zig_builtin_to_rust` already translated all of them correctly; its first line returns `None` for any name without the `@` sigil, and specs write them bare. The repair routes the bare name through the SAME table rather than restating it, so the two spellings cannot drift. +- Three traps, each measured rather than anticipated: **30** corpus declarations give one of these names to a spec's own function (`fn floor` in 10 specs, `fn abs` in 9), and 3 of the 30 declare their return type without `->`, so a guard built on `collect_fn_ret_types` would be narrower than its subject; a literal receiver is `{float}` and gives E0689 in both the single-token and the compound form; and `sqrt` is not a `const fn`, so no spelling works inside a `const`. +- `log` is deliberately excluded: Rust's `f64::log` takes a BASE, so `(x).log()` is E0061. The existing `@log` arm carries the same defect and is filed, not changed here. +- Worth, stated honestly: corpus rustc acceptance moves **429 -> 429 of 650, delta zero**. Nine files' emitted Rust changes and **none** crosses fail to pass. The one file showing a new error class was proven to be a pre-existing defect newly REACHED -- the only diff is line 140, and the offending line 143 is byte-identical in both outputs; E0425 is a name-resolution error that aborts before the borrow checker runs. +- The point is the precondition, not the number. First real port: `specs/rings/ring_103_phi_sgd.t27` replaces the arithmetic of `rings/ring-103-rust/src/lib.rs`, and the generated Rust agrees with the hand-written original **bit-exactly on 20,736 inputs** -- NaN, both infinities, both zeros, subnormals, disabled clip, negative learning rate -- with a harness control proving it can see a one-bit difference. +- Mutation-checked: disabling the `declared_fns` guard fails `a_spec_declaring_its_own_abs_keeps_its_own_abs` and leaves the other nine tests green. diff --git a/specs/rings/ring_103_phi_sgd.t27 b/specs/rings/ring_103_phi_sgd.t27 new file mode 100644 index 0000000000..fb725d7239 --- /dev/null +++ b/specs/rings/ring_103_phi_sgd.t27 @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: Apache-2.0 +// t27/specs/rings/ring_103_phi_sgd.t27 +// Port of rings/ring-103-rust/src/lib.rs -- the phi-tempered SGD step. +// phi^2 + 1/phi^2 = 3 | TRINITY + +module rings::ring_103 { + + // Golden ratio and its reciprocal, as in the hand-written original. + const PHI : f64 = 1.6180339887498948; + const PHI_INV : f64 = 0.6180339887498948; + + // The original computes `PHI_INV as f32` once per step. Rounding the same + // decimal literal to f32 gives the same bit pattern, so the constant is + // written directly at f32 rather than converted. + const PHI_INV_F32 : f32 = 0.6180339887498948; + + // PhiSgd::default_smoke() + const DEFAULT_LR : f32 = 0.01; + const DEFAULT_CLIP : f32 = 1.0; + + // g.clamp(-clip, clip), with clip <= 0 disabling the clamp. + // Written as comparisons rather than min/max so that a NaN gradient + // passes through unchanged, matching f32::clamp. + fn clip_grad(g: f32, clip: f32) -> f32 { + if (clip > 0.0) { + if (g > clip) { + return clip; + } + if (g < -clip) { + return -clip; + } + } + return g; + } + + // One element of `w_i -= lr * (1/phi) * clip(g_i)`. + // Association is left-to-right, as in the original expression + // `self.lr * phi_inv * g_clipped`. + fn phi_sgd_update(w: f32, g: f32, lr: f32, clip: f32) -> f32 { + return w - lr * PHI_INV_F32 * clip_grad(g, clip); + } + + // ring-100's identity witness: phi^2 + 1/phi^2 == 3. + fn identity_witness() -> bool { + return abs(PHI * PHI + 1.0 / (PHI * PHI) - 3.0) < 0.000000000000001; + } + + test "identity_witness_holds" + assert(identity_witness()); + + test "phi_constants_satisfy_trinity_anchor" + assert(abs(PHI * PHI_INV - 1.0) < 0.000000000000001); + + test "zero_gradient_leaves_weight_unchanged" + assert(phi_sgd_update(1.5, 0.0, DEFAULT_LR, DEFAULT_CLIP) == 1.5); + + test "positive_gradient_decreases_weight" + assert(phi_sgd_update(1.0, 0.1, DEFAULT_LR, DEFAULT_CLIP) < 1.0); + + test "clip_limits_step_magnitude" + assert(abs(10.0 - phi_sgd_update(10.0, 1000000.0, 1.0, 1.0)) <= 1.0); + + test "disabled_clip_passes_gradient_through" + assert(clip_grad(5.0, 0.0) == 5.0); +}