Skip to content

Commit b84e2f6

Browse files
committed
Reject semicolonless simple statements
1 parent d51ba04 commit b84e2f6

4 files changed

Lines changed: 135 additions & 7 deletions

File tree

docs/moon_wgsl-issue-tracker.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Last updated: 2026-05-08
1313

1414
| ID | Source | Problem | Status | Notes |
1515
| --- | --- | --- | --- | --- |
16+
| `WGSL-261` | Strict statement terminators | The statement collector treated a closing `}` as a valid terminator for simple statements, so invalid WGSL like `z = 1.0 }` could be accepted by moon_wgsl even though Naga rejects it with `expected ;`. | `DONE` | Statement collection now records whether a statement ended with a top-level semicolon, and strict AST validation marks semicolon-required statements invalid when the semicolon is missing. Template interpolation braces from naga_oil-style `#{...}` are tracked separately from WGSL block braces, so recursive block validation does not descend into source templates. Added parser regressions for assignment, return, and function-call statements without semicolons, and verified the real wgpu `issue_4485.wgsl` invalid source is now rejected before IR lowering. |
1617
| `WGSL-260` | Composer semantic phase architecture | Even after composer rewrites became symbol-binding-first, several phase boundaries still compressed semantic objects into strings: semantic reference facts were `HashSet[String]`, compose bindings carried `from_name/to_name/identity?`, live binding resolution stored `resolved_to_name`, and transform rewrite bindings accepted string reference names. | `DONE` | Added structured semantic reference path sets, changed compose bindings to carry `WgslSemanticReferencePath` plus a non-optional `WgslComposeSymbolTarget`, changed live binding resolution to pass target symbol objects instead of resolved-name strings, and changed transform rewrite plans to accept `WgslReferencePath` rather than string bindings. Added architecture guardrails banning `add_symbol_binding`, optional identity compose bindings, string-only semantic reference sets, and resolved-name phase state. Verification is tracked with this audit pass on 2026-05-08. |
1718
| `WGSL-259` | Composer symbol-binding architecture | `WgslReferenceRewritePlan` carried stable symbol identities but still exposed binding rewrites by downgrading them into `WgslRenamePlan`; the final AST rewrite therefore remained name-first at the execution boundary, and unqualified duplicate import bindings could remain ambiguous until transform declined a rewrite. | `DONE` | Removed the binding-plan-to-rename-plan adapters, removed optional identity from plain rename rules, and made reference/declaration binding rewrites consume `WgslReferenceRewriteBinding` directly when collecting AST identifier nodes. Composer binding sets now reject same-scope/same-reference bindings that point at different identities, including unqualified imports. Added guardrails preventing `reference_rename_plan` / `global_declaration_rename_plan` from returning and documenting that composer rewrites must remain symbol-binding-first. Verification is tracked with this audit pass on 2026-05-08. |
1819
| `WGSL-258` | 100% WGSL/naga-oil quality gate | External real-project WGSL scanning still allowed skipped files to remain implicit, so coverage regressions could hide behind a stable aggregate skipped count. | `IN_PROGRESS` | Replaced the skipped-file path with `testdata/external_wgsl_corpus_expected_failures.tsv`; the external corpus gate now reports `skipped=0` and every non-materialized file is an expected-failure contract keyed by `(repo, rel_path, reason)`. Added real Bevy sprite/UI render source roots plus concrete Bevy tonemapping, deferred, environment-map, SSR, UI, and meshlet profiles; added texture-atomic/cooperative-matrix/64-bit-image-atomic/f64/mesh-shader/ray-hit-vertex-position/per-vertex/binding-array IR/oracle validation plus duplicate import-path candidate selection for Bevy dummy/real modules. Added manifest-owned source materialization for real projects that concatenate, generate, or specialize WGSL before shader module creation, covering wgpu timestamp normalization, wgpu mesh shaders, wgpu ray-query vertex-return shaders, wgpu binding-array and per-vertex shaders, webgpu-samples Cornell shader suffixes/template replacement, webgpu-samples skinned-mesh generated vertex input, and primitive-index/barycentric/multiview validation capabilities. This raises Bevy to 158 source-valid / 149 composed-valid, wgpu to 77 source-valid, Naga to 122 source-valid, webgpu-samples to 68 source-valid, and total external corpus coverage to 475 source-valid files. Remaining work is to burn down the 25 expected failures by fixing the listed compose/IR/profile gaps or proving the external source is intentionally invalid under standalone validation. |

parser/wgsl_ast_expr_type.mbt

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1084,20 +1084,63 @@ priv struct WgslTokenDepth {
10841084
mut angle : Int
10851085
mut bracket : Int
10861086
mut brace : Int
1087+
mut source_template_pending_hash : Bool
1088+
mut source_template_brace : Int
10871089
}
10881090

10891091
///|
10901092
fn WgslTokenDepth::WgslTokenDepth() -> WgslTokenDepth {
1091-
WgslTokenDepth::{ paren: 0, angle: 0, bracket: 0, brace: 0 }
1093+
WgslTokenDepth::{
1094+
paren: 0,
1095+
angle: 0,
1096+
bracket: 0,
1097+
brace: 0,
1098+
source_template_pending_hash: false,
1099+
source_template_brace: 0,
1100+
}
10921101
}
10931102

10941103
///|
10951104
fn WgslTokenDepth::at_top(self : WgslTokenDepth) -> Bool {
1096-
self.paren == 0 && self.angle == 0 && self.bracket == 0 && self.brace == 0
1105+
self.paren == 0 &&
1106+
self.angle == 0 &&
1107+
self.bracket == 0 &&
1108+
self.brace == 0 &&
1109+
self.source_template_brace == 0
1110+
}
1111+
1112+
///|
1113+
fn WgslTokenDepth::observe_source_template(
1114+
self : WgslTokenDepth,
1115+
token : WgslToken,
1116+
) -> Bool {
1117+
if self.source_template_brace > 0 {
1118+
match token.punctuation_code() {
1119+
Some(123) => self.source_template_brace = self.source_template_brace + 1
1120+
Some(125) => self.source_template_brace = self.source_template_brace - 1
1121+
_ => ()
1122+
}
1123+
self.source_template_pending_hash = false
1124+
return true
1125+
}
1126+
if self.source_template_pending_hash && token.is_punctuation(123) {
1127+
self.source_template_brace = 1
1128+
self.source_template_pending_hash = false
1129+
return true
1130+
}
1131+
if token.is_punctuation(35) {
1132+
self.source_template_pending_hash = true
1133+
} else if !token.is_whitespace() {
1134+
self.source_template_pending_hash = false
1135+
}
1136+
false
10971137
}
10981138

10991139
///|
11001140
fn WgslTokenDepth::observe(self : WgslTokenDepth, token : WgslToken) -> Unit {
1141+
if self.observe_source_template(token) {
1142+
return
1143+
}
11011144
match token.operator_text() {
11021145
Some(">>") => {
11031146
if self.angle >= 2 {
@@ -1127,6 +1170,9 @@ fn WgslTokenDepth::observe_statement_token(
11271170
self : WgslTokenDepth,
11281171
token : WgslToken,
11291172
) -> Unit {
1173+
if self.observe_source_template(token) {
1174+
return
1175+
}
11301176
match token.punctuation_code() {
11311177
Some(40) => self.paren = self.paren + 1
11321178
Some(41) => self.paren = self.paren - 1

parser/wgsl_ast_statements.mbt

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -423,7 +423,11 @@ fn WgslDeclParser::collect_statement_blocks(
423423
) -> Array[WgslBlock] {
424424
let blocks : Array[WgslBlock] = []
425425
let open_stack : Array[Int] = []
426+
let source_template = WgslTokenDepth()
426427
for i in 0..<tokens.length() {
428+
if source_template.observe_source_template(tokens[i]) {
429+
continue
430+
}
427431
if tokens[i].is_punctuation(123) {
428432
open_stack.push(i)
429433
} else if tokens[i].is_punctuation(125) {
@@ -453,12 +457,14 @@ fn collect_wgsl_statement_scope_tokens(
453457
) -> Array[WgslToken] {
454458
let scope_tokens : Array[WgslToken] = []
455459
let mut block_depth = 0
460+
let source_template = WgslTokenDepth()
456461
for token in tokens {
457-
if token.is_punctuation(123) {
462+
let source_template_token = source_template.observe_source_template(token)
463+
if !source_template_token && token.is_punctuation(123) {
458464
block_depth = block_depth + 1
459465
continue
460466
}
461-
if token.is_punctuation(125) && block_depth > 0 {
467+
if !source_template_token && token.is_punctuation(125) && block_depth > 0 {
462468
block_depth = block_depth - 1
463469
continue
464470
}
@@ -1072,12 +1078,17 @@ fn wgsl_statement_has_invalid_expression(
10721078
kind : WgslStatementKind,
10731079
tokens : Array[WgslToken],
10741080
expressions : Array[WgslStatementExpression],
1081+
missing_semicolon_at_statement_boundary : Bool,
10751082
) -> Bool {
10761083
for token in tokens {
10771084
if token.is_punctuation(35) {
10781085
return false
10791086
}
10801087
}
1088+
if wgsl_statement_requires_semicolon(kind, tokens) &&
1089+
missing_semicolon_at_statement_boundary {
1090+
return true
1091+
}
10811092
match kind {
10821093
Return =>
10831094
tokens.length() > 1 &&
@@ -1126,6 +1137,34 @@ fn wgsl_statement_has_invalid_expression(
11261137
}
11271138
}
11281139

1140+
///|
1141+
fn wgsl_statement_requires_semicolon(
1142+
kind : WgslStatementKind,
1143+
tokens : Array[WgslToken],
1144+
) -> Bool {
1145+
match tokens {
1146+
[token, ..] if token.is_punctuation(35) => return false
1147+
[token, ..] if token.is_identifier("case") || token.is_identifier("default") =>
1148+
return false
1149+
[] => return false
1150+
_ => ()
1151+
}
1152+
match kind {
1153+
LocalDeclaration
1154+
| Return
1155+
| Break
1156+
| Continue
1157+
| Discard
1158+
| ConstAssert
1159+
| Assignment
1160+
| Increment
1161+
| Decrement
1162+
| FunctionCall
1163+
| Expression => true
1164+
_ => false
1165+
}
1166+
}
1167+
11291168
///|
11301169
fn wgsl_statement_payload_from_ast(
11311170
kind : WgslStatementKind,
@@ -1283,6 +1322,7 @@ fn WgslDeclParser::collect_statement(self : WgslDeclParser) -> WgslStatement? {
12831322
let mut first_identifier : String? = None
12841323
let mut start_pos = -1
12851324
let mut end_pos = -1
1325+
let mut missing_semicolon_at_statement_boundary = false
12861326
let depth = WgslTokenDepth()
12871327
fn record_span(start : Int, end_ : Int) -> Unit {
12881328
if start_pos < 0 {
@@ -1295,7 +1335,12 @@ fn WgslDeclParser::collect_statement(self : WgslDeclParser) -> WgslStatement? {
12951335
}
12961336
while true {
12971337
match self.view() {
1298-
[token, ..] if depth.at_top() && token.is_punctuation(125) => break
1338+
[token, ..] if depth.at_top() && token.is_punctuation(125) => {
1339+
if tokens.length() > 0 {
1340+
missing_semicolon_at_statement_boundary = true
1341+
}
1342+
break
1343+
}
12991344
[token, .. rest] if depth.at_top() && token.is_punctuation(59) => {
13001345
let pos = token.pos()
13011346
record_span(pos, pos + 1)
@@ -1323,7 +1368,12 @@ fn WgslDeclParser::collect_statement(self : WgslDeclParser) -> WgslStatement? {
13231368
}
13241369
continue
13251370
}
1326-
[] => break
1371+
[] => {
1372+
if tokens.length() > 0 {
1373+
missing_semicolon_at_statement_boundary = true
1374+
}
1375+
break
1376+
}
13271377
}
13281378
}
13291379
if start_pos < 0 || end_pos < start_pos {
@@ -1378,7 +1428,7 @@ fn WgslDeclParser::collect_statement(self : WgslDeclParser) -> WgslStatement? {
13781428
expressions,
13791429
)
13801430
let invalid_expression = wgsl_statement_has_invalid_expression(
1381-
kind, scope_tokens, expressions,
1431+
kind, scope_tokens, expressions, missing_semicolon_at_statement_boundary,
13821432
)
13831433
let blocks = self.collect_statement_blocks(tokens)
13841434
Some(

parser/wgsl_ast_wbtest.mbt

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,37 @@ test "strict WGSL translation unit rejects invalid numeric literals" {
129129
}
130130
}
131131

132+
///|
133+
test "strict WGSL translation unit rejects semicolonless simple statements" {
134+
let sources = [
135+
(
136+
#|fn assignment_missing_semicolon() {
137+
#| var z = 0.0;
138+
#| z = 1.0
139+
#|}
140+
),
141+
(
142+
#|fn return_missing_semicolon() -> u32 {
143+
#| return 1u
144+
#|}
145+
),
146+
(
147+
#|fn call_missing_semicolon() {
148+
#| workgroupBarrier()
149+
#|}
150+
),
151+
]
152+
for source in sources {
153+
let mut failed = false
154+
ignore(parse_wgsl_translation_unit_strict(source)) catch {
155+
_ => failed = true
156+
}
157+
guard failed is true else {
158+
abort("expected missing statement semicolon to fail: \{source}")
159+
}
160+
}
161+
}
162+
132163
///|
133164
test "WGSL numeric literal tokens expose raw descriptor" {
134165
let source =

0 commit comments

Comments
 (0)