From 060cf9e2481a3f1cbb7d28507f3eff051c9180a8 Mon Sep 17 00:00:00 2001 From: Vasilev Dmitrii Date: Sun, 9 Aug 2026 02:13:30 +0700 Subject: [PATCH] fix(parser): braced if-expr arms, paren-less conditions, &-transparent types; fpga specs repaired Refs #1960 The #1941 hardening surfaced a layer of constructs the parser never supported but silently swallowed. This lands the top classes: - if-EXPRESSIONS accept braced arms: `if (c) { 2 } else { 0 }` - if/while STATEMENTS accept paren-less Rust-style conditions, with the struct-literal-in-condition rule (a `{` after the condition opens the body, never a struct literal) - reference types are transparent at the spec level (&str/&T parse as the referent) - spec repairs: mac.t27 pack_trit; spi.t27 (THREE match constructs -- the FSM tick, prescaler and SCK dispatch had been silently dropped forever); fifo.t27 (four literal missing-paren typos) fpga-build --smoke: 2 -> 21 of 35 modules generate. Remaining tails begin at the given/then BDD fn form (linker). tri-net's 77-spec icarus gate is green; unit suite at the single pre-existing red (bitnet_layer). FROZEN_HASH resealed. Co-Authored-By: Claude Fable 5 --- NOW.md | 9 +++ bootstrap/src/compiler.rs | 80 +++++++++++++++++++---- bootstrap/stage0/FROZEN_HASH | 2 +- docs/NOW.md | 9 +++ specs/fpga/fifo.t27 | 8 +-- specs/fpga/spi.t27 | 120 ++++++++++++++++------------------- 6 files changed, 146 insertions(+), 82 deletions(-) diff --git a/NOW.md b/NOW.md index 34fc2b1b9..4d4c2e205 100644 --- a/NOW.md +++ b/NOW.md @@ -2,6 +2,15 @@ Last updated: 2026-08-08 +## parser: braced if-expr arms, paren-less conditions, &-transparent types; fpga specs repaired (Refs #1960) + +- If-EXPRESSIONS accept braced arms (`if (c) { 2 } else { 0 }`); if/while STATEMENTS accept paren-less Rust-style conditions with the struct-literal-in-condition rule (a `{` after the cond opens the body) +- Reference types are transparent (`&str`/`&T` parse as the referent) +- mac.t27 pack_trit (braced if-expr), spi.t27 (three `match` constructs -- FSM tick, prescaler, SCK -- silently dropped for ever), fifo.t27 (four literal missing-paren typos) repaired +- fpga-build --smoke: 2 -> 21 of 35 modules generate; remaining tails are the given/then BDD fn form (linker) onward +- tri-net 77-spec icarus gate green, unit suite at the single pre-existing red +- FROZEN_HASH resealed + ## gen-verilog: W458 keeps the legacy [N]T binding; unit contracts updated (Refs #1948) - The W458 array-param exclusion narrows to rust-style [T; N] primitives only; legacy [N]T keeps its module-array ROM binding contract diff --git a/bootstrap/src/compiler.rs b/bootstrap/src/compiler.rs index 3f729c9ec..ba8b26a0e 100644 --- a/bootstrap/src/compiler.rs +++ b/bootstrap/src/compiler.rs @@ -834,6 +834,9 @@ pub struct Parser { current: Token, peek: Token, pending_pragma: String, + /// Inside a paren-less if/while condition a `{` opens the BODY, never a + /// struct literal (the Rust rule); parse_primary consults this. + no_struct_literal: bool, } #[derive(Clone)] @@ -853,6 +856,7 @@ impl Parser { current: first, peek: second, pending_pragma: String::new(), + no_struct_literal: false, } } @@ -1590,6 +1594,12 @@ impl Parser { fn parse_type_annotation(&mut self) -> String { let mut ty = String::new(); + // Reference types are transparent at the spec level: `&str` / `&T` + // parse as their referent (t27#1960). + if self.current.kind == TokenKind::Amp { + self.advance(); // consume & + } + // Handle tuple type: (T1, T2, ...). Keep the raw textual form so that // named tuples like (a: T, b: T) do not hang the parser and are stored // as-is for backends that do not need to unpack them. @@ -1772,6 +1782,13 @@ impl Parser { self.advance(); // consume ! } + // Reference types are transparent at the spec level: `&str` / `&T` + // parse as their referent (t27#1960 fallout -- the old recovery + // silently dropped whole fns over the `&`). + if self.current.kind == TokenKind::Amp { + self.advance(); // consume & + } + // Return type (identifier, tuple, or []T / [N]T / [][]const u8 slice/array types, or void) if self.current.kind == TokenKind::LParen { // Tuple return type: (u32, u32) @@ -2161,10 +2178,21 @@ impl Parser { let mut if_node = Node::new(NodeKind::StmtIf); self.advance(); // consume 'if' - // Condition in parentheses - self.expect(TokenKind::LParen)?; - let cond = self.parse_expr()?; - self.expect(TokenKind::RParen)?; + // Condition: `if (cond) { ... }` or the paren-less Rust-style + // `if cond { ... }` -- both appear across the spec corpus, and the + // paren-less form was silently DROPPED by the old statement recovery + // (t27#1960 fallout of the #1941 hardening). + let cond = if self.current.kind == TokenKind::LParen { + self.advance(); // consume ( + let c = self.parse_expr()?; + self.expect(TokenKind::RParen)?; + c + } else { + self.no_struct_literal = true; + let c = self.parse_expr(); + self.no_struct_literal = false; + c? + }; if_node.children.push(cond); // Then branch: { ... } @@ -2233,10 +2261,18 @@ impl Parser { let mut while_node = Node::new(NodeKind::StmtWhile); self.advance(); // consume 'while' - // Condition in parentheses - self.expect(TokenKind::LParen)?; - let cond = self.parse_expr()?; - self.expect(TokenKind::RParen)?; + // Condition: parenthesized or paren-less (see parse_if_stmt). + let cond = if self.current.kind == TokenKind::LParen { + self.advance(); // consume ( + let c = self.parse_expr()?; + self.expect(TokenKind::RParen)?; + c + } else { + self.no_struct_literal = true; + let c = self.parse_expr(); + self.no_struct_literal = false; + c? + }; while_node.children.push(cond); // Body: { ... } @@ -2836,8 +2872,9 @@ impl Parser { } } - // Check for struct literal: Name{ .field = expr, ... } - if self.current.kind == TokenKind::LBrace { + // Check for struct literal: Name{ .field = expr, ... }. In a + // paren-less condition the `{` opens the statement body. + if self.current.kind == TokenKind::LBrace && !self.no_struct_literal { return self.parse_struct_literal(name); } @@ -3061,8 +3098,18 @@ impl Parser { let cond = self.parse_expr()?; self.expect(TokenKind::RParen)?; - // Then expression - let then_expr = self.parse_expr()?; + // Then expression. Braced arms (`if (c) { 2 } else { 0 }`) are the + // common spec spelling; the bare-expression form stays supported. + // Before #1941 the brace was silently swallowed by statement + // recovery -- now it must parse (t27#1960, mac.t27 pack_trit). + let then_expr = if self.current.kind == TokenKind::LBrace { + self.advance(); // consume { + let e = self.parse_expr()?; + self.expect(TokenKind::RBrace)?; + e + } else { + self.parse_expr()? + }; // else expression let mut if_node = Node::new(NodeKind::ExprIf); @@ -3071,7 +3118,14 @@ impl Parser { if self.current.kind == TokenKind::KwElse { self.advance(); // consume 'else' - let else_expr = self.parse_expr()?; + let else_expr = if self.current.kind == TokenKind::LBrace { + self.advance(); // consume { + let e = self.parse_expr()?; + self.expect(TokenKind::RBrace)?; + e + } else { + self.parse_expr()? + }; if_node.children.push(else_expr); } diff --git a/bootstrap/stage0/FROZEN_HASH b/bootstrap/stage0/FROZEN_HASH index e2df38d79..fdc30c525 100644 --- a/bootstrap/stage0/FROZEN_HASH +++ b/bootstrap/stage0/FROZEN_HASH @@ -1 +1 @@ -c808d130f5af01ba082ba255737cacda0f7a2fec507949ba07fdf6fa13da7f06 +ac0310ff69d2c211d638757eaa8704e80a968918ccb93f5fe4ebb353e99a3788 diff --git a/docs/NOW.md b/docs/NOW.md index 8d14256b5..d1a65a9bf 100644 --- a/docs/NOW.md +++ b/docs/NOW.md @@ -2,6 +2,15 @@ Last updated: 2026-08-08 +## parser: braced if-expr arms, paren-less conditions, &-transparent types; fpga specs repaired (Refs #1960) + +- If-EXPRESSIONS accept braced arms (`if (c) { 2 } else { 0 }`); if/while STATEMENTS accept paren-less Rust-style conditions with the struct-literal-in-condition rule (a `{` after the cond opens the body) +- Reference types are transparent (`&str`/`&T` parse as the referent) +- mac.t27 pack_trit (braced if-expr), spi.t27 (three `match` constructs -- FSM tick, prescaler, SCK -- silently dropped for ever), fifo.t27 (four literal missing-paren typos) repaired +- fpga-build --smoke: 2 -> 21 of 35 modules generate; remaining tails are the given/then BDD fn form (linker) onward +- tri-net 77-spec icarus gate green, unit suite at the single pre-existing red +- FROZEN_HASH resealed + ## gen-verilog: W458 keeps the legacy [N]T binding; unit contracts updated (Refs #1948) - The W458 array-param exclusion narrows to rust-style [T; N] primitives only; legacy [N]T keeps its module-array ROM binding contract diff --git a/specs/fpga/fifo.t27 b/specs/fpga/fifo.t27 index 42589e36a..20988cb9d 100644 --- a/specs/fpga/fifo.t27 +++ b/specs/fpga/fifo.t27 @@ -200,16 +200,16 @@ module Fifo { if (cfg.name == "") { errors = errors + 1; } - if (cfg.depth == 0 { + if (cfg.depth == 0) { errors = errors + 1; } - if (cfg.data_width == 0 { + if (cfg.data_width == 0) { errors = errors + 1; } - if (cfg.has_almost_empty and cfg.almost_empty_threshold >= cfg.depth { + if (cfg.has_almost_empty and cfg.almost_empty_threshold >= cfg.depth) { errors = errors + 1; } - if (cfg.has_almost_full and cfg.almost_full_threshold >= cfg.depth { + if (cfg.has_almost_full and cfg.almost_full_threshold >= cfg.depth) { errors = errors + 1; } diff --git a/specs/fpga/spi.t27 b/specs/fpga/spi.t27 index adf0b4bb5..84377068a 100644 --- a/specs/fpga/spi.t27 +++ b/specs/fpga/spi.t27 @@ -111,17 +111,18 @@ module SPI_Master; // spi_get_prescaler_div() 413 u32 // Get actual prescaler divider value fn spi_get_prescaler_div() -> u32 { - match spi.prescaler { - PRESCALER_2 => 2u32, - PRESCALER_4 => 4u32, - PRESCALER_8 => 8u32, - PRESCALER_16 => 16u32, - PRESCALER_32 => 32u32, - PRESCALER_64 => 64u32, - PRESCALER_128 => 128u32, - PRESCALER_256 => 256u32, - _ => 16u32, - } + // Was a `match` expression, which t27 has never parsed -- the whole + // dispatch was silently DROPPED before the #1941 hardening (the fn + // was an unimplemented stub). If-chain now. + if (spi.prescaler == PRESCALER_2) { return 2; } + if (spi.prescaler == PRESCALER_4) { return 4; } + if (spi.prescaler == PRESCALER_8) { return 8; } + if (spi.prescaler == PRESCALER_16) { return 16; } + if (spi.prescaler == PRESCALER_32) { return 32; } + if (spi.prescaler == PRESCALER_64) { return 64; } + if (spi.prescaler == PRESCALER_128) { return 128; } + if (spi.prescaler == PRESCALER_256) { return 256; } + return 16; } // spi_get_sck_freq() 414 u32 @@ -178,11 +179,10 @@ module SPI_Master; fn spi_get_sck() -> bool { // In Mode 0: SCK is low in idle // Alternates during transfer - match spi.tx_state { - TX_BIT => false, // SCK low (setup) - RX_BIT => true, // SCK high (sample) - _ => SPI_CPOL == 0, - } + // Was a `match` expression (never parsed; silently dropped pre-#1941). + if (spi.tx_state == TX_BIT) { return false; } // SCK low (setup) + if (spi.tx_state == RX_BIT) { return true; } // SCK high (sample) + return SPI_CPOL == 0; } // spi_get_mosi() 421 bool @@ -197,30 +197,25 @@ module SPI_Master; // spi_tick() 422 void // Process one system clock cycle fn spi_tick() -> void { - match spi.state { - SPI_IDLE => { - // Do nothing, waiting for transfer - } - SPI_CS_ASSERT => { - spi.cs_assert_cnt = spi.cs_assert_cnt + 1; - if (spi.cs_assert_cnt >= (CS_ASSERT_DELAY * CLK_FREQ / 1_000_000_000)) { - spi.cs_assert_cnt = 0; - spi.cs_asserted = true; - spi.state = SPI_TRANSFER; - spi.tx_state = TX_BIT; - } + // Was a `match` statement (never parsed; the whole FSM tick was + // silently dropped pre-#1941). If/else-if chain now. + if (spi.state == SPI_CS_ASSERT) { + spi.cs_assert_cnt = spi.cs_assert_cnt + 1; + if (spi.cs_assert_cnt >= (CS_ASSERT_DELAY * CLK_FREQ / 1_000_000_000)) { + spi.cs_assert_cnt = 0; + spi.cs_asserted = true; + spi.state = SPI_TRANSFER; + spi.tx_state = TX_BIT; } - SPI_TRANSFER => { - spi_transfer_bit(); - } - SPI_CS_DEASSERT => { - spi.cs_deassert_cnt = spi.cs_deassert_cnt + 1; - if (spi.cs_deassert_cnt >= (CS_DEASSERT_DELAY * CLK_FREQ / 1_000_000_000)) { - spi.cs_deassert_cnt = 0; - spi.cs_asserted = false; - spi.state = SPI_IDLE; - spi.busy = false; - } + } else if (spi.state == SPI_TRANSFER) { + spi_transfer_bit(); + } else if (spi.state == SPI_CS_DEASSERT) { + spi.cs_deassert_cnt = spi.cs_deassert_cnt + 1; + if (spi.cs_deassert_cnt >= (CS_DEASSERT_DELAY * CLK_FREQ / 1_000_000_000)) { + spi.cs_deassert_cnt = 0; + spi.cs_asserted = false; + spi.state = SPI_IDLE; + spi.busy = false; } } } @@ -231,34 +226,31 @@ module SPI_Master; const prescaler_div = spi_get_prescaler_div(); spi.bit_counter = spi.bit_counter + 1; - match spi.tx_state { - TX_BIT => { - if (spi.bit_counter >= prescaler_div / 2) { - spi.bit_counter = 0; - spi.tx_state = RX_BIT; - } + // Was a `match` statement (silently dropped pre-#1941). + if (spi.tx_state == TX_BIT) { + if (spi.bit_counter >= prescaler_div / 2) { + spi.bit_counter = 0; + spi.tx_state = RX_BIT; } - RX_BIT => { - if (spi.bit_counter >= prescaler_div / 2) { - // Sample MISO 424 in spec-level simulation this is a placeholder; - // Verilog emission reads the actual MISO input pin - const miso_bit = false; - spi.rx_data = (spi.rx_data << 1) | (if miso_bit { 1u32 } else { 0u32 }); - spi.bit_count = spi.bit_count + 1; - spi.bit_counter = 0; - - if (spi.bit_count >= spi.data_width) { - spi.tx_state = WAIT_EDGE; - } else { - spi.tx_state = TX_BIT; - } + } else if (spi.tx_state == RX_BIT) { + if (spi.bit_counter >= prescaler_div / 2) { + // Sample MISO 424 in spec-level simulation this is a placeholder; + // Verilog emission reads the actual MISO input pin + const miso_bit = false; + spi.rx_data = (spi.rx_data << 1) | (if (miso_bit) { 1 } else { 0 }); + spi.bit_count = spi.bit_count + 1; + spi.bit_counter = 0; + + if (spi.bit_count >= spi.data_width) { + spi.tx_state = WAIT_EDGE; + } else { + spi.tx_state = TX_BIT; } } - WAIT_EDGE => { - if (spi.bit_counter >= prescaler_div / 2) { - spi.bit_counter = 0; - spi.state = SPI_CS_DEASSERT; - } + } else if (spi.tx_state == WAIT_EDGE) { + if (spi.bit_counter >= prescaler_div / 2) { + spi.bit_counter = 0; + spi.state = SPI_CS_DEASSERT; } } }