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
20 changes: 18 additions & 2 deletions compatibility/support-matrix.json
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,22 @@
"evidence": ["fixtures:synthetic/issue-33-switch-break", "upstream:src/tests/switches.opy", "upstream:src/tests/loops.opy"],
"notes": "Issue #33: break is a real HIR statement, validates its enclosing switch/loop context, and is retained without implicit arm exits."
},
{
"id": "syntax/statement-surface",
"name": "del, continue, goto, labels, and dynamic loc+ targets",
"category": "syntax",
"state": "source-supported",
"evidence": ["tests:crates/opy-rs/tests/issue_141_syntax.rs", "upstream:src/tests/loops.opy", "upstream:src/tests/gotos.opy"],
"notes": "Issue #141: audited statements lower to source HIR nodes with spans; continue context and malformed targets produce structured diagnostics. Canonical WIR lowering remains an explicit integration boundary."
},
{
"id": "syntax/augmented-min-max",
"name": "min= and max= modification forms",
"category": "syntax",
"state": "source-supported",
"evidence": ["tests:crates/opy-rs/tests/issue_141_syntax.rs", "upstream:src/tests/operators.opy"],
"notes": "Issue #141: min=/max= are retained as source-semantic binary modifications with provenance; Workshop support is not claimed."
},
{
"id": "syntax/do-while",
"name": "do … while",
Expand Down Expand Up @@ -759,13 +775,13 @@
"summary": {
"byState": {
"planned": 0,
"source-supported": 23,
"source-supported": 25,
"semantic-supported": 13,
"lowering-dependent": 12,
"end-to-end-supported": 8
},
"byCategory": {
"syntax": 14,
"syntax": 16,
"semantics": 14,
"preprocessing": 4,
"macros": 3,
Expand Down
9 changes: 8 additions & 1 deletion crates/opy-cli/tests/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,14 @@ fn support_filters_by_feature_id_and_category() {
let slice: serde_json::Value =
serde_json::from_slice(&by_category.stdout).expect("category JSON");
assert_eq!(slice["category"], "syntax");
assert_eq!(slice["count"], 14);
let features = slice["features"].as_array().expect("filtered features");
assert_eq!(slice["count"], features.len());
assert!(!features.is_empty());
assert!(
features
.iter()
.all(|feature| feature["category"] == "syntax")
);

let unknown = run(&["support", "nope/nothing"]);
assert_eq!(unknown.status.code(), Some(2));
Expand Down
55 changes: 54 additions & 1 deletion crates/opy-rs/src/compiler/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -819,9 +819,28 @@ impl MacroExpander {
.collect::<Result<Vec<_>, IntegrationError>>()?,
span: *span,
},
Stmt::Delete { target, span } => Stmt::Delete {
target: Box::new(self.expand_expr(target, bindings)?),
span: *span,
},
Stmt::Goto {
label,
offset,
rule_start,
span,
} => Stmt::Goto {
label: label.clone(),
offset: offset
.as_ref()
.map(|offset| self.expand_expr(offset, bindings).map(Box::new))
.transpose()?,
rule_start: *rule_start,
span: *span,
},
Stmt::Break { .. } | Stmt::CallSubroutine { .. } | Stmt::Pass { .. } => {
statement.clone()
}
Stmt::Continue { .. } | Stmt::Label { .. } => statement.clone(),
})
}

Expand Down Expand Up @@ -2005,6 +2024,22 @@ impl<'a> Lowering<'a> {
arms,
span,
} => self.lower_switch(value, arms, *span).map(|action| vec![action]),
Stmt::Delete { span, .. } => Err(self.unsupported(
"delete statements are not representable in canonical WIR",
*span,
)),
Stmt::Continue { span } => Err(self.unsupported(
"continue statements are not representable in canonical WIR",
*span,
)),
Stmt::Goto { span, .. } => Err(self.unsupported(
"goto statements are not representable in canonical WIR",
*span,
)),
Stmt::Label { span, .. } => Err(self.unsupported(
"labels are not representable in canonical WIR",
*span,
)),
Stmt::Break { span } => match break_target {
Some(BreakTarget::Loop) => Ok(vec![self.wir.actions.push(Action::Call {
name: "break".to_string(),
Expand Down Expand Up @@ -3830,6 +3865,9 @@ fn collect_implicit_stmts(
collect_implicit_expr(target, declared_globals, declared_players, globals, players);
collect_implicit_expr(value, declared_globals, declared_players, globals, players);
}
Stmt::Delete { target, .. } => {
collect_implicit_expr(target, declared_globals, declared_players, globals, players);
}
Stmt::If {
branches, r#else, ..
} => {
Expand Down Expand Up @@ -3928,7 +3966,22 @@ fn collect_implicit_stmts(
}
}
}
Stmt::Break { .. } | Stmt::CallSubroutine { .. } | Stmt::Pass { .. } => {}
Stmt::Goto { offset, .. } => {
if let Some(offset) = offset {
collect_implicit_expr(
offset,
declared_globals,
declared_players,
globals,
players,
);
}
}
Stmt::Break { .. }
| Stmt::Continue { .. }
| Stmt::Label { .. }
| Stmt::CallSubroutine { .. }
| Stmt::Pass { .. } => {}
}
}
}
Expand Down
21 changes: 21 additions & 0 deletions crates/opy-rs/src/cst.rs
Original file line number Diff line number Diff line change
Expand Up @@ -197,9 +197,26 @@ pub enum Stmt {
arms: Vec<SwitchArm>,
span: Span,
},
Delete {
target: Expr,
span: Span,
},
Break {
span: Span,
},
Continue {
span: Span,
},
Goto {
label: Option<String>,
offset: Option<Expr>,
rule_start: bool,
span: Span,
},
Label {
name: String,
span: Span,
},
Pass {
span: Span,
},
Expand Down Expand Up @@ -393,7 +410,11 @@ impl Stmt {
| Stmt::While { span, .. }
| Stmt::DoWhile { span, .. }
| Stmt::Switch { span, .. }
| Stmt::Delete { span, .. }
| Stmt::Break { span }
| Stmt::Continue { span }
| Stmt::Goto { span, .. }
| Stmt::Label { span, .. }
| Stmt::Pass { span } => *span,
}
}
Expand Down
37 changes: 37 additions & 0 deletions crates/opy-rs/src/hir/dump.rs
Original file line number Diff line number Diff line change
Expand Up @@ -311,13 +311,50 @@ fn dump_stmt(statement: &Stmt, out: &mut String, level: usize) {
}
}
}
Stmt::Delete { target, span } => {
out.push_str(&format!("{}delete ", indent(level)));
render_expr(target, out);
out.push_str(&format!("{}\n", span_suffix(span.as_ref())));
}
Stmt::Break { span } => {
out.push_str(&format!(
"{}break{}\n",
indent(level),
span_suffix(span.as_ref())
));
}
Stmt::Continue { span } => {
out.push_str(&format!(
"{}continue{}\n",
indent(level),
span_suffix(span.as_ref())
));
}
Stmt::Goto {
label,
offset,
rule_start,
span,
} => {
out.push_str(&format!("{}goto ", indent(level)));
if *rule_start {
out.push_str("RULE_START");
} else if let Some(label) = label {
out.push_str(&format!("label {label}"));
} else if let Some(offset) = offset {
out.push_str("loc+");
render_expr(offset, out);
}
out.push_str(&format!("{}\n", span_suffix(span.as_ref())));
}
Stmt::Label { name, span } => {
out.push_str(&format!(
"{}label {}{}\n",
indent(level),
name,
span_suffix(span.as_ref())
));
}
Stmt::CallSubroutine { name, span } => {
out.push_str(&format!(
"{}callSubroutine {}{}\n",
Expand Down
28 changes: 28 additions & 0 deletions crates/opy-rs/src/hir/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -457,10 +457,34 @@ pub enum Stmt {
#[serde(skip_serializing_if = "Option::is_none")]
span: Option<Span>,
},
Delete {
target: Box<Expr>,
#[serde(skip_serializing_if = "Option::is_none")]
span: Option<Span>,
},
Break {
#[serde(skip_serializing_if = "Option::is_none")]
span: Option<Span>,
},
Continue {
#[serde(skip_serializing_if = "Option::is_none")]
span: Option<Span>,
},
Goto {
#[serde(default, skip_serializing_if = "Option::is_none")]
label: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
offset: Option<Box<Expr>>,
#[serde(default)]
rule_start: bool,
#[serde(skip_serializing_if = "Option::is_none")]
span: Option<Span>,
},
Label {
name: String,
#[serde(skip_serializing_if = "Option::is_none")]
span: Option<Span>,
},
CallSubroutine {
name: String,
#[serde(skip_serializing_if = "Option::is_none")]
Expand Down Expand Up @@ -491,7 +515,11 @@ impl Stmt {
| Stmt::While { span, .. }
| Stmt::DoWhile { span, .. }
| Stmt::Switch { span, .. }
| Stmt::Delete { span, .. }
| Stmt::Break { span }
| Stmt::Continue { span }
| Stmt::Goto { span, .. }
| Stmt::Label { span, .. }
| Stmt::CallSubroutine { span, .. }
| Stmt::Pass { span } => span.as_ref(),
}
Expand Down
57 changes: 56 additions & 1 deletion crates/opy-rs/src/hir/validate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,11 @@ const STMT_KINDS: &[&str] = &[
"while",
"doWhile",
"switch",
"delete",
"break",
"continue",
"goto",
"label",
"callSubroutine",
"pass",
];
Expand Down Expand Up @@ -467,6 +471,42 @@ fn validate_stmts(
}
}
}
Stmt::Delete { target, span } => {
if !matches!(target.as_ref(), Expr::Index { .. }) {
errors.push(invalid(
"invalid-structure",
"a delete statement must target an array index",
*span,
));
}
}
Stmt::Goto {
label,
offset,
rule_start,
span,
..
} => {
let target_count =
u8::from(label.is_some()) + u8::from(offset.is_some()) + u8::from(*rule_start);
if target_count != 1 {
errors.push(invalid(
"invalid-structure",
"a goto must contain exactly one label, offset, or RULE_START target",
*span,
));
}
if let Some(label) = label {
if let Err(error) = check_name(label, "label", *span) {
errors.push(error);
}
}
}
Stmt::Label { name, span } => {
if let Err(error) = check_name(name, "label", *span) {
errors.push(error);
}
}
_ => {}
}
});
Expand Down Expand Up @@ -542,6 +582,7 @@ fn statement_exprs(statements: &[Stmt]) -> Vec<&Expr> {
exprs.push(target.as_ref());
exprs.push(value.as_ref());
}
Stmt::Delete { target, .. } => exprs.push(target.as_ref()),
Stmt::If {
branches, r#else, ..
} => {
Expand Down Expand Up @@ -589,7 +630,16 @@ fn statement_exprs(statements: &[Stmt]) -> Vec<&Expr> {
}
}
}
Stmt::Break { .. } | Stmt::CallSubroutine { .. } | Stmt::Pass { .. } => {}
Stmt::Goto { offset, .. } => {
if let Some(offset) = offset {
exprs.push(offset.as_ref());
}
}
Stmt::Break { .. }
| Stmt::Continue { .. }
| Stmt::Label { .. }
| Stmt::CallSubroutine { .. }
| Stmt::Pass { .. } => {}
}
}
exprs
Expand Down Expand Up @@ -685,7 +735,11 @@ fn for_each_stmt<'a>(statements: &'a [Stmt], f: &mut impl FnMut(&'a Stmt)) {
}
Stmt::Expr { .. }
| Stmt::Assign { .. }
| Stmt::Delete { .. }
| Stmt::Break { .. }
| Stmt::Continue { .. }
| Stmt::Goto { .. }
| Stmt::Label { .. }
| Stmt::CallSubroutine { .. }
| Stmt::Pass { .. } => {}
}
Expand Down Expand Up @@ -851,6 +905,7 @@ fn check_stmt(value: &Value) -> Result<(), HirError> {
"condition",
"variable",
"iterable",
"offset",
] {
if let Some(child) = object.get(field) {
check_expr(child)?;
Expand Down
2 changes: 2 additions & 0 deletions crates/opy-rs/src/lexer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ pub enum TokenKind {
RBrace,
Comma,
Colon,
Semicolon,
Dot,
Assign,
Plus,
Expand Down Expand Up @@ -157,6 +158,7 @@ impl Lexer {
'}' => self.single(TokenKind::RBrace),
',' => self.single(TokenKind::Comma),
':' => self.single(TokenKind::Colon),
';' => self.single(TokenKind::Semicolon),
'.' => self.single(TokenKind::Dot),
'@' => self.single(TokenKind::At),
'=' => self.two(TokenKind::Assign, TokenKind::Eq, '='),
Expand Down
Loading
Loading