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
93 changes: 93 additions & 0 deletions bootstrap/src/compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21896,12 +21896,105 @@ pub fn typecheck_ast(ast: &Node) -> TypeCheckResult {
}
}

// A constant must fit the type it declares.
//
// `const EXP_OFFSET: u32 = 1792...173` (185 digits) typechecked clean, and
// every backend emitted the digits verbatim: Rust as `pub const
// EXP_OFFSET: u32`, Verilog as `localparam [31:0]`. Only `cc` said anything
// -- "integer literal is too large to be represented in any integer type" --
// and it is the fourth backend, so three of the four carried a value 590
// bits wide in a 32-bit box without a word.
//
// Ten constants across five specs. The declared width is the claim; the
// literal is the value; nothing compared them.
check_const_widths(ast, &mut result);

if result.error_count > 0 {
result.ok = false;
}
result
}

/// The number of value bits a t27 integer type holds, or `None` if the type is
/// not a fixed-width integer.
///
/// Signed types report one bit fewer: `i32` holds 2^31 - 1, not 2^32 - 1. A
/// check that used the full width would accept `const X: i32 = 3000000000`,
/// which is the same defect with a smaller number.
pub fn int_value_bits(ty: &str) -> Option<u32> {
match ty.trim() {
"u8" => Some(8),
"u16" => Some(16),
"u32" => Some(32),
"u64" => Some(64),
"usize" => Some(64),
"i8" => Some(7),
"i16" => Some(15),
"i32" => Some(31),
"i64" => Some(63),
"isize" => Some(63),
_ => None,
}
}

/// Whether a decimal literal fits a type of `bits` value bits.
///
/// The `Err` arm is the load-bearing one, not a fallback. The values that
/// provoked this are 185 digits long, so `parse::<u128>` returns `Err` rather
/// than a wrong number -- too wide to parse and too wide to fit are the same
/// answer here. A first draft carried an explicit `len() > 20` guard above this
/// and a comment explaining why it was needed; deleting the guard changed no
/// test, because it never ran.
pub fn literal_fits(digits: &str, bits: u32) -> bool {
let d = digits.trim_start_matches('0');
if d.is_empty() {
return true;
}
match d.parse::<u128>() {
Ok(v) => v < (1u128 << bits),
Err(_) => false,
}
}

fn check_const_widths(node: &Node, result: &mut TypeCheckResult) {
if node.kind == NodeKind::ConstDecl {
// The literal is the first child, not `value`: `decl.value` carries only
// the verbatim text of a tagged union. Reading the wrong field is why the
// first version of this check fired on nothing and reported clean.
let lit = node
.children
.first()
.filter(|c| c.kind == NodeKind::ExprLiteral)
.map(|c| if c.value.is_empty() { c.name.clone() } else { c.value.clone() })
.unwrap_or_default();
if let Some(bits) = int_value_bits(&node.extra_type) {
// The lexer strips digit separators, so the literal arrives without
// them. A filter here changed no test either -- verified by removing
// it -- and `const A: u32 = 4_294_967_295` is checked end to end in
// tests/const_width.rs, where the lexer's behaviour is what the
// assertion actually rests on.
let raw: String = lit.clone();
if !raw.is_empty() && raw.chars().all(|c| c.is_ascii_digit()) && !literal_fits(&raw, bits)
{
result.error_count += 1;
let line = if node.line > 0 {
format!(":{}", node.line)
} else {
String::new()
};
result.errors.push(format!(
"error: constant '{}'{} declares {} but its value has {} digits, \
which no {} can hold",
node.name, line, node.extra_type, raw.len(), node.extra_type
));
}
}
}
for c in &node.children {
check_const_widths(c, result);
}
}

/// `ret` is the enclosing function's declared return type, threaded so the
/// RETURN position can be compared like the other three.
///
Expand Down
2 changes: 1 addition & 1 deletion bootstrap/stage0/FROZEN_HASH
Original file line number Diff line number Diff line change
@@ -1 +1 @@
f54e190f098bb7663bfdea7fdbde6f2450fd38174bf407073fc6f6b17acb4275
7d3956bee4446a11d8752b74dea8e53f9d5f17bd44b99a1d54bacd47e59aa8b4
101 changes: 101 additions & 0 deletions bootstrap/tests/const_width.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
//! A constant must fit the type it declares.
//!
//! `const EXP_OFFSET: u32 = 1792...173` -- 185 digits -- typechecked clean, and
//! every backend emitted the digits verbatim: Rust as `pub const EXP_OFFSET:
//! u32`, Verilog as `localparam [31:0]`. Only `cc` said anything, and it is the
//! fourth backend, so three of the four carried a 590-bit value in a 32-bit box
//! without a word.
//!
//! The declared width is a claim, the literal is a value, and nothing compared
//! them. Ten constants across five specs.

use std::io::Write;
use std::process::Command;

/// Typecheck a source string through the shipped binary and return only the
/// width errors.
///
/// Through the binary, not a library call: this crate has no lib target, and a
/// test that reimplemented the check would pass against a compiler that never
/// shipped it.
fn errors_of(src: &str) -> Vec<String> {
let dir = std::env::temp_dir().join(format!(
"t27-constwidth-{}-{}",
std::process::id(),
src.len()
));
std::fs::create_dir_all(&dir).expect("temp dir");
let path = dir.join("m.t27");
let mut f = std::fs::File::create(&path).expect("write spec");
f.write_all(src.as_bytes()).expect("write spec");
let out = Command::new(env!("CARGO_BIN_EXE_t27c"))
.arg("typecheck")
.arg(&path)
.output()
.expect("run t27c");
let _ = std::fs::remove_dir_all(&dir);
let text = format!(
"{}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
text.lines()
.filter(|l| l.contains("which no"))
.map(|l| l.to_string())
.collect()
}

#[test]
fn a_literal_wider_than_its_declared_type_is_an_error() {
let e = errors_of("module m\n\nconst HUGE: u32 = 99999999999999999999999999999999999999\n");
assert_eq!(e.len(), 1, "expected one width error, got {e:?}");
assert!(e[0].contains("38 digits"), "{}", e[0]);
assert!(e[0].contains("u32"), "{}", e[0]);
}

/// The value that provoked this does not fit any Rust integer either.
///
/// A checker that parsed the literal into `u128` would overflow on exactly the
/// inputs it exists to reject and report nothing -- the failure would look like
/// a clean file. So the digit count is compared first.
#[test]
fn a_literal_too_wide_for_u128_is_still_rejected() {
let big = "1".repeat(185);
let e = errors_of(&format!("module m\n\nconst E: u32 = {big}\n"));
assert_eq!(e.len(), 1, "185 digits must be rejected, not overflow away");
assert!(e[0].contains("185 digits"), "{}", e[0]);
}

#[test]
fn a_literal_that_fits_is_left_alone() {
for (ty, v) in [
("u8", "255"),
("u16", "65535"),
("u32", "4294967295"),
("u64", "18446744073709551615"),
("i32", "2147483647"),
] {
let e = errors_of(&format!("module m\n\nconst C: {ty} = {v}\n"));
assert!(e.is_empty(), "{ty} = {v} must be accepted, got {e:?}");
}
}

/// Signed types hold one bit fewer.
///
/// A check that used the full width would accept `const X: i32 = 3000000000`,
/// which is the same defect with a smaller number and no compiler to catch it:
/// `cc` only complains above 2^64.
#[test]
fn a_signed_type_holds_one_bit_fewer() {
let e = errors_of("module m\n\nconst X: i32 = 3000000000\n");
assert_eq!(e.len(), 1, "2^31 <= 3000000000 < 2^32, so i32 cannot hold it");
let ok = errors_of("module m\n\nconst Y: u32 = 3000000000\n");
assert!(ok.is_empty(), "u32 can, and the same number must be accepted there");
}

/// Underscores are separators, not digits.
#[test]
fn digit_separators_do_not_count_as_width() {
let e = errors_of("module m\n\nconst C: u32 = 4_294_967_295\n");
assert!(e.is_empty(), "4_294_967_295 is u32::MAX, got {e:?}");
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# NOW -- The largest first error is not the largest lever (2026-08-30)

## The largest first error is not the largest lever (Closes #2925)

- C generates for 578 specs and cc accepts 174; the 404-file gap ranked by FIRST error put `default_input`/`valid_input` on top with 74
- measured before building: of the 166 files carrying that error, **0** would compile if it were the only fix -- every one has other independent families
- ranking by first error ranks by POSITION IN THE FILE, not by blocking power; only **20 of 404** files are blocked by exactly one family, median is 4-5
- the real find came from the 4 single-family files: `const EXP_OFFSET: u32 = 1792...173` (185 digits) typechecked clean and three backends emitted it verbatim
- ten constants across five specs; only cc ever said anything, and it is the fourth backend
- two branches of my own fix were DEAD with comments explaining why they were needed: `parse::<u128>` returns Err rather than overflowing, and the lexer already strips separators -- both removed after their mutations changed no test
40 changes: 38 additions & 2 deletions docs/reports/suite_expectations.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"schema_version": 1,
"generated_by": "t27c suite --bless-expectations",
"max_gate_failures": 2,
"max_entries": 147,
"max_entries": 152,
"entries": [
{
"path": "specs/account/repo.t27",
Expand Down Expand Up @@ -1417,7 +1417,43 @@
"reason": "Expected LBrace, got Semicolon (';')",
"issue": 1959,
"expires": "2026-11-30"
},
{
"path": "specs/numeric/gft64.t27",
"phase": "typecheck",
"reason": "EXP_OFFSET and OFFSET_MAX declare u32 and carry values 12 to 187 digits wide. Newly DETECTED, not newly broken: the constants are unchanged and three backends still emit them verbatim -- only cc ever said so. The header says E_t = round((N-1)/phi^2) = 391, so which number is meant is a question about the format, not the compiler.",
"issue": 2925,
"expires": "2026-11-30"
},
{
"path": "specs/numeric/gft128.t27",
"phase": "typecheck",
"reason": "EXP_OFFSET and OFFSET_MAX declare u32 and carry values 12 to 187 digits wide. Newly DETECTED, not newly broken: the constants are unchanged and three backends still emit them verbatim -- only cc ever said so. The header says E_t = round((N-1)/phi^2) = 391, so which number is meant is a question about the format, not the compiler.",
"issue": 2925,
"expires": "2026-11-30"
},
{
"path": "specs/numeric/gft256.t27",
"phase": "typecheck",
"reason": "EXP_OFFSET and OFFSET_MAX declare u32 and carry values 12 to 187 digits wide. Newly DETECTED, not newly broken: the constants are unchanged and three backends still emit them verbatim -- only cc ever said so. The header says E_t = round((N-1)/phi^2) = 391, so which number is meant is a question about the format, not the compiler.",
"issue": 2925,
"expires": "2026-11-30"
},
{
"path": "specs/numeric/gft512.t27",
"phase": "typecheck",
"reason": "EXP_OFFSET and OFFSET_MAX declare u32 and carry values 12 to 187 digits wide. Newly DETECTED, not newly broken: the constants are unchanged and three backends still emit them verbatim -- only cc ever said so. The header says E_t = round((N-1)/phi^2) = 391, so which number is meant is a question about the format, not the compiler.",
"issue": 2925,
"expires": "2026-11-30"
},
{
"path": "specs/numeric/gft1024.t27",
"phase": "typecheck",
"reason": "EXP_OFFSET and OFFSET_MAX declare u32 and carry values 12 to 187 digits wide. Newly DETECTED, not newly broken: the constants are unchanged and three backends still emit them verbatim -- only cc ever said so. The header says E_t = round((N-1)/phi^2) = 391, so which number is meant is a question about the format, not the compiler.",
"issue": 2925,
"expires": "2026-11-30"
}
],
"_why_entries_fell": "151 -> 150: specs/pins/emitter_xdc.t27 [typecheck] was fixed by #2906 -- an integer literal above i64::MAX fell to the float branch, so a bit mask read as a float -- and the ledger entry outlived the failure. The ratchet reported it as an UNEXPECTED PASS and named the repair itself: 'fixed -- remove from the ledger'. Removed, and the cap lowered in the same commit so the next entry cannot hide in the slack."
"_why_entries_fell": "151 -> 150: specs/pins/emitter_xdc.t27 [typecheck] was fixed by #2906 -- an integer literal above i64::MAX fell to the float branch, so a bit mask read as a float -- and the ledger entry outlived the failure. The ratchet reported it as an UNEXPECTED PASS and named the repair itself: 'fixed -- remove from the ledger'. Removed, and the cap lowered in the same commit so the next entry cannot hide in the slack.",
"_why_entries_rose": "a new typecheck rule (constant literal must fit its declared type, #2925) detects a defect that was always present. The five gft specs are recorded rather than edited because their correct values are a question about the GF-T ladder's definition, not about the compiler. Nothing regressed."
}
Loading