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
95 changes: 95 additions & 0 deletions bootstrap/src/compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2432,6 +2432,43 @@ impl Parser {
self.expect(TokenKind::LBrace)?;
self.parse_struct_body(&mut decl)?;
self.expect(TokenKind::RBrace)?;
} else if self.current.kind == TokenKind::Ident
&& self.current.lexeme == "packed"
&& self.peek.kind == TokenKind::KwStruct
{
// `pub const Greeting = packed struct { ... };`
//
// The bare `struct` case below has been handled since the
// beginning; `packed struct` had not, and there is no KwPacked
// in this lexer -- `packed` arrives as an Ident, exactly like
// `union` above. So this declaration fell through to the
// generic expression path and became a ConstDecl whose
// initializer started at the token `packed`.
//
// The Verilog backend then rendered it as a scalar parameter:
//
// parameter [31:0] Greeting = packed;
//
// `packed` is not a value. That line is what a reader of
// specs/demos/hello_world.t27 -- the spec the corpus opens on,
// and the one that claims to show "every part of the language"
// -- gets under the Verilog tab today.
//
// A packed struct is exactly the declaration that matters most
// to a hardware backend: it IS a bit layout. Losing it to a
// keyword token is the worst-placed gap in the emitter.
// Packedness itself is NOT recorded on the node. Nothing
// downstream reads it today, and inventing a field no emitter
// consumes would look like support that does not exist. The
// declaration now reaches the backends as a struct, which is
// the part that was missing.
decl.kind = NodeKind::StructDecl;
self.advance(); // consume 'packed'
self.advance(); // consume 'struct'
self.expect(TokenKind::LBrace)?;
self.parse_struct_body(&mut decl)?;
self.expect(TokenKind::RBrace)?;
fold_sole_enum_field(&mut decl);
} else if self.current.kind == TokenKind::KwStruct {
// pub const Foo = struct { ... };
decl.kind = NodeKind::StructDecl;
Expand Down Expand Up @@ -22914,6 +22951,64 @@ mod tests_hir_module {
}
}

#[cfg(test)]
mod tests_packed_struct_decl {
use super::*;

/// `pub const X = packed struct { ... };` must reach the backends as a
/// StructDecl, exactly like the bare `struct` form.
///
/// Before this was handled, `packed` (an Ident -- there is no KwPacked in
/// this lexer) fell through to the generic expression path, the whole
/// declaration became a ConstDecl, and the Verilog backend rendered its
/// initializer as a scalar parameter:
///
/// parameter [31:0] Greeting = packed;
///
/// `packed` is not a value. No spec in this repository declares one today,
/// which is why nothing caught it -- but the corpus snapshot the website
/// vendors does, in `specs/demos/hello_world.t27`, the spec the Spec
/// Explorer opens on and describes as showing "every part of the language".
/// So this test is the only thing standing between the construct and a
/// silent regression.
#[test]
fn packed_struct_const_parses_as_a_struct_declaration() {
let src = "module m;\npub const Greeting = packed struct {\n length: u8,\n trit: i8,\n};\n";
let lex = Lexer::new(src);
let mut parser = Parser::new(lex);
let root = parser.parse().expect("packed struct should parse");

let greeting = root
.children
.iter()
.find(|d| d.name == "Greeting")
.expect("Greeting declaration should be present");

assert_eq!(
greeting.kind,
NodeKind::StructDecl,
"packed struct must be a StructDecl, not a ConstDecl whose value is the token `packed`"
);
assert_eq!(greeting.children.len(), 2, "both fields should be parsed");
}

/// The bare form must keep working — this test exists so a future edit to
/// the packed branch cannot quietly shadow the one beneath it.
#[test]
fn plain_struct_const_still_parses_as_a_struct_declaration() {
let src = "module m;\npub const Plain = struct {\n a: u8,\n};\n";
let lex = Lexer::new(src);
let mut parser = Parser::new(lex);
let root = parser.parse().expect("plain struct should parse");
let plain = root
.children
.iter()
.find(|d| d.name == "Plain")
.expect("Plain declaration should be present");
assert_eq!(plain.kind, NodeKind::StructDecl);
}
}

#[cfg(test)]
mod tests_ast_to_hir {
use super::*;
Expand Down
2 changes: 1 addition & 1 deletion bootstrap/stage0/FROZEN_HASH
Original file line number Diff line number Diff line change
@@ -1 +1 @@
9d6165ae377f6e10cbf78ad33242a1ea1820941bdce0e3d71467adff34326c44 bootstrap/src/compiler.rs
b41de56a8e25b77e159cf0a792c5a8e40e60c7438bfe8fd97fea89de2a91d8c6 bootstrap/src/compiler.rs
10 changes: 9 additions & 1 deletion docs/NOW.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
# NOW -- Trinity t27 sync

Last updated: 2026-09-05
Last updated: 2026-09-06

## `const X = packed struct {}` lost its name, and yosys never minded (Closes #3383)

- There is no `KwPacked` in this lexer: `packed` arrives as an `Ident`, exactly like `union` does. `parse_const_decl` handled `= struct {` and `= union(enum) {` but not `= packed struct {`, so the declaration fell through to the generic expression path and became a `ConstDecl` whose initializer started at that token.
- Measured on `specs/demos/hello_world.t27` -- the spec the Spec Explorer opens on -- same branch, same spec, only the parser differing. **Before:** `parameter [31:0] Greeting = packed;`, and the struct emitted ANONYMOUSLY (`// struct `, `reg [7:0] _length; // .length`). **After:** the parameter is gone and the struct carries its name (`// struct Greeting`, `reg [7:0] greeting_length;`).
- **What it does NOT do, measured rather than assumed.** yosys 0.63 synthesises both versions to **0 cells, 4 wires, 4 ports**. It never rejected the old output: it printed `Lexer warning: The SystemVerilog keyword 'packed' is not recognized unless read_verilog is called with -sv!` and carried on. So this removes a warning and restores a lost name; it does not change synthesis, and it was never a cause of the corpus's yosys failures. The "module shells / 0 LUTs" condition has a different root cause -- no hardware boundary, which the `entry-points` service already measures.
- Cherry-picked from master (#3384, `f5d8564ab`). Master's corpus contains no `packed struct` at all, so master could not test it; this branch is where `hello_world` lives, and is the first place the fix was verified end to end.
- Suite green here: 889 plus the per-crate suites, 0 failed. FROZEN_HASH resealed with a `shasum`-computed digest, never transcribed.

## The compiler now runs in a browser, and 49 specs turned out to be losing declarations silently

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# NOW -- `packed struct` became a parameter whose value was a keyword (2026-09-06)

## `const X = packed struct {}` was emitted as `parameter = packed` (Closes #3383)

- There is no KwPacked in this lexer -- `packed` arrives as an Ident, exactly like `union` does. parse_const_decl handled `= struct {` and `= union(enum) {` but not `= packed struct {`, so the declaration fell through to the generic expression path and became a ConstDecl whose initializer started at that token. The Verilog backend then rendered it as `parameter [31:0] Greeting = packed;`, and `packed` is not a value.
- Scope stated rather than implied: no spec in this repository declares one, so `grep -rn '= packed struct' --include='*.t27'` returns 0 and this fix changes no corpus number in either direction. I had first written that it was probably a major cause of the yosys rejections, then counted, and corrected it in the commit and the PR instead of leaving that framing standing. The damage is in the corpus snapshot the website vendors, which carries specs/demos/hello_world.t27 -- the spec the Spec Explorer opens on, described as showing "every part of the language" -- with that line live under its Verilog tab on t27.ai.
- Packedness itself is deliberately not recorded on the node. Nothing downstream reads it, and inventing a field no emitter consumes would look like support that does not exist; the declaration now simply reaches the backends as a struct.
- Because nothing exercises the construct, the two regression tests are the entire guard, so they were checked against the defect rather than merely run: with the new parser branch disabled the packed test FAILS and the plain-struct test still passes; enabled, both pass. Full suite 1694 passed, 0 failed. FROZEN_HASH resealed per FROZEN.md §5 / CANON.md M5, with the digest computed by shasum and written by the shell rather than transcribed.
Loading