Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 104 additions & 1 deletion bootstrap/src/compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21942,6 +21942,23 @@ fn collect_bool_fns(node: &Node, out: &mut std::collections::HashSet<String>) {
}
}

/// 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<String>) {
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?
///
Expand Down Expand Up @@ -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<String>,
/// 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<String>,
/// Parameters and locals of the current function declared `bool`.
bool_vars: std::collections::HashSet<String>,
/// Module-level `var x : bool`. `bool_vars` is cleared per function and
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 => {
Expand Down
2 changes: 1 addition & 1 deletion bootstrap/stage0/FROZEN_HASH
Original file line number Diff line number Diff line change
@@ -1 +1 @@
745cf51f9c7e719065ddb9fbabc682d9425687883062212d4a63ba72cef759d1 bootstrap/src/compiler.rs
8dca9dd2df9413543e14977a91584195cfaaa47b7e039c379efc8e1eb5fa2863 bootstrap/src/compiler.rs
83 changes: 83 additions & 0 deletions bootstrap/tests/backend_behaviour.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> {
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}"
);
}
Original file line number Diff line number Diff line change
@@ -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.
65 changes: 65 additions & 0 deletions specs/rings/ring_103_phi_sgd.t27
Original file line number Diff line number Diff line change
@@ -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);
}
Loading