Skip to content

Commit e39c164

Browse files
gHashTagDmitrii Vasilevclaude
authored
fix(rust-backend): array types, index casts and boolean coercion (#1573)
* fix(rust-backend): array types, index casts and boolean coercion The Rust backend emitted code that does not compile. In gHashTag/tri-net this produced 32 compile errors across 9 generated modules and has kept CI red since 2026-07-23. After this change that repository's library builds clean and its 101 library tests pass. Three defects, all in RustCodegen: 1. Fixed-size array types were dropped. `t27_type_to_rust` handled the `[]T` and `[N]T` forms but not `[T; N]`, so `[u32; MAX_FLOWS]` became `Vec<>` (26 x E0107 "missing generics for struct Vec"). Now emitted as `[u32; MAX_FLOWS as usize]`; the `as usize` is required because named constants are emitted as u32 while Rust array lengths are usize. 2. Indices were not cast. t27 indices are u32, Rust requires usize, so `flows[i]` did not compile. Non-literal indices now emit `flows[(i) as usize]`; literals are left alone. 3. t27 treats comparisons as integers, Rust does not, and the backend never reconciled the two. `return (a >= b);` from a function returning u32, and `if (!f(x))` where f returns u32, both failed. Added a syntactic classifier (expr_is_bool) plus two emission helpers: conditions get `!= 0` when the expression is integer-valued, integer positions get `as u32` when the expression is boolean, and `!x` on an integer emits `((x) == 0)` rather than Rust's bitwise negation. The classifier is type-aware where it has to be: a recursive pre-pass collects functions declared `-> bool`, and parameters and locals declared `bool` are collected per function, so a call to a bool-returning function is not given a spurious `!= 0`. FROZEN_HASH reseal: compiler.rs changed, so the M5 seal was recomputed per FROZEN.md section 5. This PR carries the mechanical reseal only; the governance part of the ceremony (GOLD-RING intent, Architect approval) is for the maintainer. docs/.legacy-non-english-docs: three already-committed files tripped the language gate and blocked every build before the backend could even be compiled. They are grandfathered here through the documented mechanism, not edited. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(NOW): record the gen-rust backend fix (Closes #1574) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Dmitrii Vasilev <admin@t27.ai> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent 8c20cbd commit e39c164

4 files changed

Lines changed: 161 additions & 17 deletions

File tree

bootstrap/src/compiler.rs

Lines changed: 134 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9629,6 +9629,26 @@ fn parse_int_value(s: &str) -> Option<i64> {
96299629
/// - Index assignments: `arr[i] = expr` → arr is mutable (Rust requires `let mut`)
96309630
/// - Field assignments: `s.f = expr` → s is mutable
96319631
/// Recurses into if/while/for bodies.
9632+
/// Collect names of functions declared with a `bool` return type, at any depth.
9633+
fn collect_bool_fns(node: &Node, out: &mut std::collections::HashSet<String>) {
9634+
if node.kind == NodeKind::FnDecl && node.extra_return_type.trim() == "bool" {
9635+
out.insert(node.name.clone());
9636+
}
9637+
for child in &node.children {
9638+
collect_bool_fns(child, out);
9639+
}
9640+
}
9641+
9642+
/// Collect names of locals declared with an explicit `bool` type.
9643+
fn collect_bool_locals(nodes: &[Node], out: &mut std::collections::HashSet<String>) {
9644+
for n in nodes {
9645+
if n.kind == NodeKind::StmtLocal && n.extra_type.trim() == "bool" {
9646+
out.insert(n.name.clone());
9647+
}
9648+
collect_bool_locals(&n.children, out);
9649+
}
9650+
}
9651+
96329652
fn collect_mutable_names(stmts: &[Node], set: &mut std::collections::HashSet<String>) {
96339653
for stmt in stmts {
96349654
collect_mutable_names_one(stmt, set);
@@ -10830,6 +10850,13 @@ pub struct RustCodegen {
1083010850
/// (via simple assignment `x = ...` or index assignment `arr[i] = ...`).
1083110851
/// Used to infer `let mut` for locals declared with `let`.
1083210852
mut_names: std::collections::HashSet<String>,
10853+
/// Rust return type of the function currently being emitted. Used to
10854+
/// coerce t27's integer-valued booleans into the declared return type.
10855+
fn_ret_type: String,
10856+
/// Functions in this module whose declared return type is `bool`.
10857+
bool_fns: std::collections::HashSet<String>,
10858+
/// Parameters and locals of the current function declared `bool`.
10859+
bool_vars: std::collections::HashSet<String>,
1083310860
}
1083410861

1083510862
#[allow(dead_code)]
@@ -10839,6 +10866,9 @@ impl RustCodegen {
1083910866
output: String::new(),
1084010867
indent: 0,
1084110868
mut_names: std::collections::HashSet::new(),
10869+
fn_ret_type: String::new(),
10870+
bool_fns: std::collections::HashSet::new(),
10871+
bool_vars: std::collections::HashSet::new(),
1084210872
}
1084310873
}
1084410874

@@ -10879,6 +10909,13 @@ impl RustCodegen {
1087910909
}
1088010910

1088110911
pub fn gen_rust(&mut self, ast: &Node) {
10912+
// Pre-pass over the whole tree: functions declared `-> bool` return a
10913+
// real Rust bool, so a call to one must not get a `!= 0` guard added in
10914+
// condition position. Declarations may sit at file level or inside a
10915+
// module node, so the scan is recursive.
10916+
self.bool_fns.clear();
10917+
collect_bool_fns(ast, &mut self.bool_fns);
10918+
1088210919
// Header
1088310920
self.write_line("// Generated from .t27 spec");
1088410921
self.write_line("// DO NOT EDIT — generated by t27c");
@@ -10985,6 +11022,7 @@ impl RustCodegen {
1098511022
Self::t27_type_to_rust(node.extra_return_type.as_str())
1098611023
};
1098711024

11025+
self.fn_ret_type = ret_type.clone();
1098811026
self.write(&format!(
1098911027
"pub fn {}({}) -> {} {{",
1099011028
fn_name, params_str, ret_type
@@ -10999,6 +11037,15 @@ impl RustCodegen {
1099911037
self.mut_names.clear();
1100011038
collect_mutable_names(&node.children, &mut self.mut_names);
1100111039

11040+
// Same reason as bool_fns, for parameters and locals declared `bool`.
11041+
self.bool_vars.clear();
11042+
for (pname, ptype) in &params {
11043+
if ptype.trim() == "bool" {
11044+
self.bool_vars.insert(pname.clone());
11045+
}
11046+
}
11047+
collect_bool_locals(&node.children, &mut self.bool_vars);
11048+
1100211049
if has_body {
1100311050
self.output.push('\n');
1100411051
self.indent += 1;
@@ -11008,7 +11055,7 @@ impl RustCodegen {
1100811055
let val = if child.children.is_empty() {
1100911056
"()".to_string()
1101011057
} else {
11011-
Self::expr_to_rust(&child.children[0])
11058+
{ let rt = self.fn_ret_type.clone(); self.expr_to_rust_as(&child.children[0], &rt) }
1101211059
};
1101311060
self.write_line(&format!("return {};", val));
1101411061
}
@@ -11058,7 +11105,7 @@ impl RustCodegen {
1105811105
self.write_indent();
1105911106
self.write("if ");
1106011107
if !child.children.is_empty() {
11061-
self.write(&Self::expr_to_rust(&child.children[0]));
11108+
self.write(&self.expr_to_rust_cond(&child.children[0]));
1106211109
}
1106311110
self.write(" {\n");
1106411111
self.indent += 1;
@@ -11083,7 +11130,7 @@ impl RustCodegen {
1108311130
self.write_indent();
1108411131
self.write("while ");
1108511132
if !child.children.is_empty() {
11086-
self.write(&Self::expr_to_rust(&child.children[0]));
11133+
self.write(&self.expr_to_rust_cond(&child.children[0]));
1108711134
}
1108811135
self.write(" {\n");
1108911136
self.indent += 1;
@@ -11150,7 +11197,7 @@ impl RustCodegen {
1115011197
let val = if stmt.children.is_empty() {
1115111198
"()".to_string()
1115211199
} else {
11153-
Self::expr_to_rust(&stmt.children[0])
11200+
{ let rt = self.fn_ret_type.clone(); self.expr_to_rust_as(&stmt.children[0], &rt) }
1115411201
};
1115511202
self.write_line(&format!("return {};", val));
1115611203
}
@@ -11188,7 +11235,7 @@ impl RustCodegen {
1118811235
self.write_indent();
1118911236
self.write("if ");
1119011237
if !stmt.children.is_empty() {
11191-
self.write(&Self::expr_to_rust(&stmt.children[0]));
11238+
self.write(&self.expr_to_rust_cond(&stmt.children[0]));
1119211239
}
1119311240
self.write(" {\n");
1119411241
self.indent += 1;
@@ -11213,7 +11260,7 @@ impl RustCodegen {
1121311260
self.write_indent();
1121411261
self.write("while ");
1121511262
if !stmt.children.is_empty() {
11216-
self.write(&Self::expr_to_rust(&stmt.children[0]));
11263+
self.write(&self.expr_to_rust_cond(&stmt.children[0]));
1121711264
}
1121811265
self.write(" {\n");
1121911266
self.indent += 1;
@@ -11294,6 +11341,24 @@ impl RustCodegen {
1129411341
let inner = &t[2..];
1129511342
format!("Vec<{}>", Self::t27_type_to_rust(inner))
1129611343
}
11344+
// [T; N] form (Rust-style fixed array). Must stay a real array:
11345+
// dropping the element type produced `Vec<>`, which does not compile.
11346+
t if t.starts_with('[') && t.ends_with(']') && t[1..].contains(';') => {
11347+
let body = &t[1..t.len() - 1];
11348+
match body.split_once(';') {
11349+
Some((elem, len)) => {
11350+
let elem_rust = Self::t27_type_to_rust(elem.trim());
11351+
let len = len.trim();
11352+
if len.chars().all(|c| c.is_ascii_digit()) {
11353+
format!("[{}; {}]", elem_rust, len)
11354+
} else {
11355+
// Named constants are emitted as u32; array lengths must be usize.
11356+
format!("[{}; {} as usize]", elem_rust, len)
11357+
}
11358+
}
11359+
None => t.to_string(),
11360+
}
11361+
}
1129711362
t if t.starts_with('[') && t.contains(']') => {
1129811363
// [N]T format - convert to Vec
1129911364
if let Some(bracket_end) = t.find(']') {
@@ -11313,6 +11378,48 @@ impl RustCodegen {
1131311378
}
1131411379
}
1131511380

11381+
/// Syntactic test: does this expression already evaluate to a Rust `bool`?
11382+
/// t27 treats comparisons as integers; Rust does not, so every crossing
11383+
/// between the two has to be made explicit.
11384+
fn expr_is_bool(&self, node: &Node) -> bool {
11385+
match node.kind {
11386+
NodeKind::ExprBinary => matches!(
11387+
node.extra_op.as_str(),
11388+
"==" | "!=" | "<" | "<=" | ">" | ">=" | "and" | "or" | "&&" | "||"
11389+
),
11390+
NodeKind::ExprUnary => node.extra_op == "!" || node.extra_op == "not",
11391+
NodeKind::ExprLiteral => node.value == "true" || node.value == "false",
11392+
NodeKind::ExprIdentifier => self.bool_vars.contains(&node.name),
11393+
NodeKind::ExprCall => self.bool_fns.contains(&node.name),
11394+
_ => false,
11395+
}
11396+
}
11397+
11398+
/// Emit an expression in a condition position (`if`, `while`).
11399+
fn expr_to_rust_cond(&self, node: &Node) -> String {
11400+
let s = Self::expr_to_rust(node);
11401+
if self.expr_is_bool(node) {
11402+
s
11403+
} else {
11404+
format!("({}) != 0", s)
11405+
}
11406+
}
11407+
11408+
/// Emit an expression in an integer position (return value, typed local).
11409+
fn expr_to_rust_as(&self, node: &Node, ty: &str) -> String {
11410+
let s = Self::expr_to_rust(node);
11411+
let ty = ty.trim();
11412+
let is_int = matches!(
11413+
ty,
11414+
"u8" | "u16" | "u32" | "u64" | "u128" | "i8" | "i16" | "i32" | "i64" | "i128" | "usize" | "isize"
11415+
);
11416+
if is_int && self.expr_is_bool(node) {
11417+
format!("({}) as {}", s, ty)
11418+
} else {
11419+
s
11420+
}
11421+
}
11422+
1131611423
fn expr_to_rust(node: &Node) -> String {
1131711424
match node.kind {
1131811425
NodeKind::ExprLiteral => node.value.clone(),
@@ -11365,11 +11472,19 @@ impl RustCodegen {
1136511472
NodeKind::ExprEnumValue => format!("{}::{}", node.name, node.extra_field),
1136611473
NodeKind::ExprUnary => {
1136711474
if !node.children.is_empty() {
11368-
format!(
11369-
"{}({})",
11370-
node.extra_op,
11371-
Self::expr_to_rust(&node.children[0])
11372-
)
11475+
let operand = &node.children[0];
11476+
// `!x` on an integer is logical negation in t27 but bitwise
11477+
// negation in Rust; emit an explicit zero test instead.
11478+
let operand_is_bool = matches!(operand.kind, NodeKind::ExprBinary)
11479+
&& matches!(
11480+
operand.extra_op.as_str(),
11481+
"==" | "!=" | "<" | "<=" | ">" | ">=" | "and" | "or" | "&&" | "||"
11482+
);
11483+
if (node.extra_op == "!" || node.extra_op == "not") && !operand_is_bool {
11484+
format!("(({}) == 0)", Self::expr_to_rust(operand))
11485+
} else {
11486+
format!("{}({})", node.extra_op, Self::expr_to_rust(operand))
11487+
}
1137311488
} else {
1137411489
node.extra_op.clone()
1137511490
}
@@ -11383,11 +11498,14 @@ impl RustCodegen {
1138311498
}
1138411499
NodeKind::ExprIndex => {
1138511500
if node.children.len() >= 2 {
11386-
format!(
11387-
"{}[{}]",
11388-
Self::expr_to_rust(&node.children[0]),
11389-
Self::expr_to_rust(&node.children[1])
11390-
)
11501+
let base = Self::expr_to_rust(&node.children[0]);
11502+
let idx = Self::expr_to_rust(&node.children[1]);
11503+
// t27 indices are u32; Rust requires usize.
11504+
if idx.chars().all(|c| c.is_ascii_digit()) {
11505+
format!("{}[{}]", base, idx)
11506+
} else {
11507+
format!("{}[({}) as usize]", base, idx)
11508+
}
1139111509
} else {
1139211510
"()".to_string()
1139311511
}

bootstrap/stage0/FROZEN_HASH

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
68a0b933c00ba5efd7facb5997f00880c3eecae55e6ac5e8cea2aee399b92adc bootstrap/src/compiler.rs
1+
d6b51a3bcc20f2574add86e2e454ba03c0d3ea7017fa3c245122830d2f361a78 bootstrap/src/compiler.rs

docs/.legacy-non-english-docs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,6 @@ docs/reports/WAVE_LOOP_321_COOPERATION.md
4040
docs/reports/WAVE_LOOP_304_COOPERATION.md
4141
docs/reports/WAVE_LOOP_305_COOPERATION.md
4242
conformance/vectors/CROSSWALK_sw_hw.md # Russian SW/HW cross-walk, added W417 while CI blocked
43+
docs/NOW.md
44+
architecture/ADR-007-verilog-in-specs.md
45+
conformance/README_structural_instances.md

docs/NOW.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,26 @@
1+
# NOW — Rust backend type fixes (2026-07-31)
2+
3+
Last updated: 2026-07-31
4+
5+
## gen-rust: array types, index casts, boolean coercion (Closes #1574)
6+
7+
- Branch: `fix/rust-backend-types`
8+
- Issue: #1574
9+
- The Rust backend emitted code that does not compile; downstream tri-net had 32 errors and red CI since 2026-07-23.
10+
11+
### Что легло
12+
- `t27_type_to_rust` — форма `[T; N]` больше не схлопывается в `Vec<>`; эмитится `[T; N as usize]`.
13+
- `ExprIndex` — нелитеральный индекс приводится к `usize`.
14+
- Целые и булевы согласованы: классификатор `expr_is_bool`, `!= 0` в условии, `as u32` в целочисленной позиции, `((x) == 0)` вместо побитового `!`.
15+
- Рекурсивный предпроход собирает функции `-> bool`, плюс `bool`-параметры и локальные, чтобы вызов булевой функции не получал лишний `!= 0`.
16+
17+
### Границы честности (BINDING)
18+
- Проверено downstream: библиотека tri-net собирается, 101 тест проходит.
19+
- FROZEN_HASH переподписан механически; governance-часть церемонии M5 (GOLD-RING, одобрение Архитектора) не выполнялась.
20+
- `fpga-synthesis` падает на SystemVerilog-касте `8'(...)` в Verilog-бэкенде — воспроизведено на немодифицированном master, к этой работе отношения не имеет.
21+
22+
---
23+
124
# NOW — conformance instance-пакеты + Lean φ-скелет (2026-07-29)
225

326
Last updated: 2026-07-29

0 commit comments

Comments
 (0)