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
29 changes: 14 additions & 15 deletions compatibility/compiler-expectations.json
Original file line number Diff line number Diff line change
Expand Up @@ -441,9 +441,9 @@
"provenance:real-world/overpy-broken-weapons/fixture.json"
],
"owner": "opy-rs#88",
"note": "The full real-world project now passes the #99 numeric-range setting parser boundary and remains outside the #38 compiler baseline at the next unknown-value diagnostic.",
"note": "The full real-world project now passes the #99 numeric-range setting parser boundary and the audited builtin surface; it remains outside the #38 compiler baseline at the OverPy internal RULE_CONDITION identifier boundary.",
"failureClass": "frontend",
"diagnosticCode": "unknown-value"
"diagnosticCode": "unknown-identifier"
},
{
"fixture": "real-world/overpy-cake",
Expand All @@ -468,9 +468,9 @@
"provenance:real-world/overpy-client-to-server/fixture.json"
],
"owner": "opy-rs#88",
"note": "The chained ternary, isDummy, and getHorizontalFacingAngle regressions now lower; the full real-world project remains outside the #38 compiler baseline and currently stops at the next unknown value (`horizontalAngleOfDirection`).",
"note": "The chained ternary, isDummy, getHorizontalFacingAngle, horizontalAngleOfDirection, and getFacingDirection boundaries now lower; the full real-world project remains outside the #38 compiler baseline at the next unknown member (`startFacing`).",
"failureClass": "frontend",
"diagnosticCode": "unknown-value"
"diagnosticCode": "unknown-member"
},
{
"fixture": "real-world/overpy-cronch",
Expand All @@ -482,9 +482,9 @@
"provenance:real-world/overpy-cronch/fixture.json"
],
"owner": "opy-rs#88",
"note": "The postfix increment regression now lowers; the full real-world project remains outside the #38 compiler baseline and currently stops at the unsupported createDummy action at cronch.opy:103.",
"note": "The postfix increment regression and createDummy action now lower; the full real-world project remains outside the #38 compiler baseline at the existing SpecVisibility enum boundary.",
"failureClass": "frontend",
"diagnosticCode": "unknown-action"
"diagnosticCode": "unknown-enum-member"
},
{
"fixture": "real-world/overpy-crosshair",
Expand All @@ -496,9 +496,9 @@
"provenance:real-world/overpy-crosshair/fixture.json"
],
"owner": "opy-rs#88",
"note": "The full real-world project now passes the implicit-concatenation, hudSubheader, and SpecVisibility.NEVER boundaries; the remaining native gap is the separate unsupported hudSubtext action.",
"failureClass": "frontend",
"diagnosticCode": "unknown-action"
"note": "The full real-world project now passes the implicit-concatenation, hudSubheader, SpecVisibility.NEVER, hudSubtext, and disableHeroHUD member boundaries; the remaining native gap is the separate stringModifier integration surface.",
"failureClass": "integration",
"diagnosticCode": "unsupported-integration-surface"
},
{
"fixture": "real-world/overpy-inputhud",
Expand Down Expand Up @@ -612,17 +612,16 @@
},
{
"fixture": "synthetic/chase-keywords",
"nativeStatus": "failure",
"classification": "known-gap",
"comparison": "diagnostic-code",
"nativeStatus": "success",
"classification": "unsupported",
"comparison": "semantic-wir",
"evidence": [
"oracle:synthetic/chase-keywords/oracle.json",
"provenance:synthetic/chase-keywords/fixture.json"
],
"owner": "opy-rs#88",
"note": "The source semantic form is known, while the current canonical integration surface lacks a complete compiler contract.",
"failureClass": "integration",
"diagnosticCode": "unsupported-integration-surface"
"note": "The complete chase keyword source now compiles through the audited builtin surface, while canonical WIR equivalence remains an explicit follow-up for the native compiler's variable presentation and constant-folding differences.",
"semanticEquivalent": false
},
{
"fixture": "synthetic/declarations-rules",
Expand Down
2 changes: 1 addition & 1 deletion compatibility/differential-expectations.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
{"fixture": "real-world/overpy-cronch", "nativeStatus": "failure", "classification": "known-gap", "ruleNames": false, "evidence": ["oracle:real-world/overpy-cronch/oracle.json", "regression:real-world/overpy-cronch/fixture.json"], "note": "The postfix increment regression now resolves; the full project reaches the next unsupported createDummy action at cronch.opy:103."},
{"fixture": "real-world/overpy-broken-weapons", "nativeStatus": "failure", "classification": "known-gap", "ruleNames": false, "evidence": ["oracle:real-world/overpy-broken-weapons/oracle.json", "regression:real-world/overpy-broken-weapons/fixture.json"], "note": "The numeric-range setting regression now resolves; the full project advances to the next known native gap at `isAssemblingHeroes`."},
{"fixture": "real-world/overpy-client-to-server", "nativeStatus": "failure", "classification": "known-gap", "ruleNames": false, "evidence": ["oracle:real-world/overpy-client-to-server/oracle.json", "regression:real-world/overpy-client-to-server/fixture.json"], "note": "The chained ternary, isDummy, and getHorizontalFacingAngle regressions now resolve; the full project remains an explicit known gap at its next unsupported construct."},
{"fixture": "real-world/overpy-crosshair", "nativeStatus": "failure", "classification": "known-gap", "ruleNames": false, "evidence": ["oracle:real-world/overpy-crosshair/oracle.json", "regression:real-world/overpy-crosshair/fixture.json"], "note": "The SpecVisibility.NEVER and hudSubheader boundaries now lower; the full project reaches the separate unsupported hudSubtext action."},
{"fixture": "real-world/overpy-crosshair", "nativeStatus": "success", "classification": "match", "ruleNames": false, "evidence": ["oracle:real-world/overpy-crosshair/oracle.json", "regression:real-world/overpy-crosshair/fixture.json"], "note": "The frontend resolves the full project through the audited HUD builtin surface; compiler-only lowering retains the separate stringModifier integration boundary."},
{"fixture": "real-world/overpy-inputhud", "nativeStatus": "failure", "classification": "known-gap", "ruleNames": false, "evidence": ["oracle:real-world/overpy-inputhud/oracle.json", "regression:real-world/overpy-inputhud/fixture.json"], "note": "Implicit string concatenation now parses; the full project advances to the separate unsupported conditional-value expression gap."},
{"fixture": "real-world/overpy-parabola", "nativeStatus": "failure", "classification": "known-gap", "ruleNames": false, "evidence": ["oracle:real-world/overpy-parabola/oracle.json", "regression:real-world/overpy-parabola/fixture.json"], "note": "The numeric enum member regression now resolves; the full project reaches the next multiline parenthesized expression gap at parabola.opy:50."},
{"fixture": "real-world/overpy-meipocalypse", "nativeStatus": "failure", "classification": "match", "ruleNames": false, "evidence": ["oracle:real-world/overpy-meipocalypse/oracle.json", "provenance:real-world/overpy-meipocalypse/fixture.json"], "note": "The reference rejects the missing script hook; native rejection is retained with a different earlier diagnostic."},
Expand Down
2 changes: 2 additions & 0 deletions crates/opy-rs/src/compiler/integration_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ mod issue_130_horizontal_facing_angle;
mod issue_131_spec_visibility;
#[path = "tests/issue_142_preprocessing.rs"]
mod issue_142_preprocessing;
#[path = "tests/issue_144_builtin_surface.rs"]
mod issue_144_builtin_surface;
#[path = "tests/issue_42_oracle.rs"]
mod issue_42_oracle;
#[path = "tests/issue_46_oracle.rs"]
Expand Down
135 changes: 120 additions & 15 deletions crates/opy-rs/src/compiler/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3047,8 +3047,36 @@ impl<'a> Lowering<'a> {
if !matches!(function.kind, FunctionKind::Action) {
return Err(self.unsupported(format!("'{name}' is not a generic OPY action"), span));
}
if function.id == "hudSubheader" {
return self.lower_hud_subheader(args, span);
if matches!(
function.id.as_str(),
"hudHeader" | "hudSubheader" | "hudSubtext"
) {
let text_slot = match function.id.as_str() {
"hudHeader" => 1,
"hudSubheader" => 2,
"hudSubtext" => 3,
_ => unreachable!(),
};
return self.lower_hud_text(args, span, text_slot, &function.id);
}
if function.id == "createDummy" && args.len() == 4 {
let mut lowered = args
.iter()
.map(|expr| self.lower_value(expr))
.collect::<Result<Vec<_>, _>>()?;
let mut zero_vector = Vec::with_capacity(3);
for value in [0.0, 0.0, 0.0] {
zero_vector.push(self.push_value(Value::Number {
value,
text: "0".to_string(),
}));
}
lowered.push(self.push_call("vector", zero_vector));
return Ok(self.wir.actions.push(Action::Call {
name: "createDummyBot".to_string(),
args: lowered,
span: self.wir_span(span)?,
}));
}
let catalog_id = function.catalog_id.as_ref().ok_or_else(|| {
self.unsupported(
Expand All @@ -3070,10 +3098,12 @@ impl<'a> Lowering<'a> {
}))
}

fn lower_hud_subheader(
fn lower_hud_text(
&mut self,
args: &[Expr],
span: Option<HirSpan>,
text_slot: usize,
function_name: &str,
) -> Result<wir::ActionId, IntegrationError> {
let [
visible_to,
Expand All @@ -3085,25 +3115,35 @@ impl<'a> Lowering<'a> {
spectators,
] = args
else {
return Err(
self.unsupported("hudSubheader requires exactly seven bound arguments", span)
);
return Err(self.unsupported(
format!("{function_name} requires exactly seven bound arguments"),
span,
));
};
let visible_to = self.lower_hud_visible_to(visible_to)?;
let null_header = self.push_value(Value::Null);
let null_text = self.push_value(Value::Null);
let mut text_slots = [
self.push_value(Value::Null),
self.push_value(Value::Null),
self.push_value(Value::Null),
];
let text_value = self.lower_value(text)?;
let text = self.push_call("customString", vec![text_value]);
text_slots[text_slot - 1] = self.push_call("customString", vec![text_value]);
let mut colors = [
self.push_value(Value::Null),
self.push_value(Value::Null),
self.push_value(Value::Null),
];
colors[text_slot - 1] = self.lower_value(color)?;
let args = vec![
visible_to,
null_header,
text,
null_text,
text_slots[0],
text_slots[1],
text_slots[2],
self.lower_value(position)?,
self.lower_value(sort_order)?,
self.push_value(Value::Null),
self.lower_value(color)?,
self.push_value(Value::Null),
colors[0],
colors[1],
colors[2],
self.lower_value(reevaluation)?,
self.lower_value(spectators)?,
];
Expand Down Expand Up @@ -3485,6 +3525,71 @@ impl<'a> Lowering<'a> {
y: self.lower_value(&args[1])?,
z: self.lower_value(&args[2])?,
}
} else if matches!(name.as_str(), "all" | "any") {
let call_name = if name == "all" {
"isTrueForAll"
} else {
"isTrueForAny"
};
let [array] = args.as_slice() else {
return Err(self.unsupported(
format!("{name} requires exactly one array argument"),
span,
));
};
let (array, condition) = match array {
Expr::Comprehension {
element,
variable,
index,
iterable,
..
} => {
if index.is_some() {
return Err(self.unsupported(
format!("{name} does not support an index binder"),
span,
));
}
let iterable = self.lower_value(iterable)?;
self.array_bindings.push(ArrayBinding {
element: variable.clone(),
index: None,
});
let condition = self.lower_value(element);
self.array_bindings.pop();
(iterable, condition?)
}
array => (
self.lower_value(array)?,
self.push_call("currentArrayElement", Vec::new()),
),
};
Value::Call {
name: call_name.to_string(),
args: vec![array, condition],
}
} else if matches!(name.as_str(), "ceil" | "floor" | "round") {
let [value] = args.as_slice() else {
return Err(self.unsupported(
format!("{name} requires exactly one numeric argument"),
span,
));
};
let rounding = match name.as_str() {
"ceil" => "UP",
"floor" => "DOWN",
"round" => "NEAREST",
_ => unreachable!(),
};
let rounding = self.push_value(Value::Enum {
value_type: "Rounding".to_string(),
value: rounding.to_string(),
});
Value::Call {
name: "roundToInteger".to_string(),
args: vec![self.lower_value(value)?, rounding],
}
} else if name == "sorted" {
let (array, key) = match args.as_slice() {
[array] => (
Expand Down
30 changes: 22 additions & 8 deletions crates/opy-rs/src/compiler/tests/issue_114_hud_subheader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,17 +125,31 @@ fn hud_subheader_omitted_spectators_use_default_visibility() {
}

#[test]
fn other_hud_helpers_keep_explicit_unknown_action_diagnostics() {
fn other_hud_helpers_lower_to_their_canonical_text_slots() {
for helper in ["hudHeader", "hudSubtext"] {
let source = format!(
"rule \"r\":\n @Event global\n {helper}(getAllPlayers(), \"text\", HudPosition.TOP, 0, Color.WHITE, HudReeval.VISIBILITY, SpecVisibility.DEFAULT)\n"
);
let error = crate::compile(&source, "source.opy", Path::new("."))
.expect_err("out-of-scope HUD helper must remain unsupported");
assert_eq!(error.code, "unknown-action");
assert!(
error.message.contains(helper),
"diagnostic must name {helper}"
);
let hir =
crate::compile(&source, "source.opy", Path::new(".")).expect("source must resolve");
let artifact = Compiler::new()
.expect("released workshop contract must load")
.compile_hir(&hir)
.expect("HUD helper must lower");
let rule = artifact
.wir
.rules
.get(workshop_rs::wir::RuleId::from_index(0))
.expect("rule must exist");
let Action::Call { name, args, .. } = artifact.wir.actions.get(rule.actions[0]).unwrap()
else {
panic!("HUD helper must lower to a canonical action call");
};
assert_eq!(name, "createHudText");
let text_slot = if helper == "hudHeader" { 1 } else { 3 };
assert!(matches!(
&artifact.wir.values.get(args[text_slot]).unwrap().value,
Value::Call { name, args } if name == "customString" && args.len() == 1
));
}
}
Loading
Loading