From 06083d26f6168879aa334c776444f02727ce0532 Mon Sep 17 00:00:00 2001 From: Teakowa Date: Tue, 1 Sep 2026 01:55:03 +0800 Subject: [PATCH 1/2] feat(opy): expand audited OverPy builtin surface Refs #144 --- compatibility/compiler-expectations.json | 29 +- .../opy-rs/src/compiler/integration_tests.rs | 2 + crates/opy-rs/src/compiler/mod.rs | 135 +++++- .../compiler/tests/issue_114_hud_subheader.rs | 30 +- .../tests/issue_144_builtin_surface.rs | 119 +++++ .../compiler/tests/issue_99_numeric_range.rs | 4 +- crates/opy-rs/src/manifest/data/manifest.json | 439 ++++++++++++++++++ .../src/manifest/probes/builtin-surface.opy | 27 ++ crates/opy-rs/src/manifest/probes/probes.json | 7 + docs/language-support/registries.md | 20 +- docs/overpy-support/callables-and-domains.md | 9 +- 11 files changed, 767 insertions(+), 54 deletions(-) create mode 100644 crates/opy-rs/src/compiler/tests/issue_144_builtin_surface.rs create mode 100644 crates/opy-rs/src/manifest/probes/builtin-surface.opy diff --git a/compatibility/compiler-expectations.json b/compatibility/compiler-expectations.json index cc53c09..93a2b61 100644 --- a/compatibility/compiler-expectations.json +++ b/compatibility/compiler-expectations.json @@ -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", @@ -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", @@ -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", @@ -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", @@ -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", diff --git a/crates/opy-rs/src/compiler/integration_tests.rs b/crates/opy-rs/src/compiler/integration_tests.rs index bc7d115..781d61f 100644 --- a/crates/opy-rs/src/compiler/integration_tests.rs +++ b/crates/opy-rs/src/compiler/integration_tests.rs @@ -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"] diff --git a/crates/opy-rs/src/compiler/mod.rs b/crates/opy-rs/src/compiler/mod.rs index ca5e8f7..71663fa 100644 --- a/crates/opy-rs/src/compiler/mod.rs +++ b/crates/opy-rs/src/compiler/mod.rs @@ -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::, _>>()?; + 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( @@ -3070,10 +3098,12 @@ impl<'a> Lowering<'a> { })) } - fn lower_hud_subheader( + fn lower_hud_text( &mut self, args: &[Expr], span: Option, + text_slot: usize, + function_name: &str, ) -> Result { let [ visible_to, @@ -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)?, ]; @@ -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] => ( diff --git a/crates/opy-rs/src/compiler/tests/issue_114_hud_subheader.rs b/crates/opy-rs/src/compiler/tests/issue_114_hud_subheader.rs index 7df2d55..b3e001d 100644 --- a/crates/opy-rs/src/compiler/tests/issue_114_hud_subheader.rs +++ b/crates/opy-rs/src/compiler/tests/issue_114_hud_subheader.rs @@ -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 + )); } } diff --git a/crates/opy-rs/src/compiler/tests/issue_144_builtin_surface.rs b/crates/opy-rs/src/compiler/tests/issue_144_builtin_surface.rs new file mode 100644 index 0000000..46004af --- /dev/null +++ b/crates/opy-rs/src/compiler/tests/issue_144_builtin_surface.rs @@ -0,0 +1,119 @@ +//! Inventory-backed builtin and alias coverage for issue #144. + +use std::path::Path; + +use crate::Compiler; +use workshop_rs::wir::Value; + +fn value_call_names(artifact: &crate::CompilationArtifact) -> Vec { + (0..artifact.wir.values.len()) + .filter_map(|index| { + let node = artifact + .wir + .values + .get(workshop_rs::wir::ValueId::from_index(index))?; + match &node.value { + Value::Call { name, .. } => Some(name.clone()), + _ => None, + } + }) + .collect() +} + +#[test] +fn catalog_backed_builtin_inventory_lowers_to_canonical_calls() { + let source = r#"globalvar g + +rule "builtin surface": + @Event global + @Condition isAssemblingHeroes() == false + @Condition any([g > 0 for g in [1, 2]]) + @Condition all([g > 0 for g in [1, 2]]) + @Condition ceil(1.2) == 2 + @Condition floor(1.8) == 1 + @Condition round(1.5) == 2 + g = getPlayers(Team.ALL) + g = random.randint(1, 3) + g = random.shuffle([1, 2]) + chaseAtRate(g, 1, 1) + createDummy(Hero.ANA, Team.ALL, -1, vect(0, 0, 0), vect(0, 0, 0)) + hudHeader(getAllPlayers(), "header", HudPosition.TOP, 0, Color.WHITE, HudReeval.VISIBILITY, SpecVisibility.DEFAULT) + hudSubtext(getAllPlayers(), "text", HudPosition.TOP, 1, Color.WHITE, HudReeval.VISIBILITY, SpecVisibility.DEFAULT) + waitUntil(true, 1) +"#; + let hir = crate::compile(source, "builtin-surface.opy", Path::new(".")) + .expect("the audited builtin source must resolve"); + let artifact = Compiler::new() + .expect("released Workshop contract must load") + .compile_hir(&hir) + .expect("the audited builtin source must lower"); + let names = value_call_names(&artifact); + for expected in [ + "isAssemblingHeroes", + "isTrueForAny", + "isTrueForAll", + "allPlayers", + "randomInteger", + "randomizedArray", + "roundToInteger", + "customString", + ] { + assert!( + names.iter().any(|name| name == expected), + "missing {expected} call" + ); + } + assert!(artifact.emitted.contains("Chase Global Variable At Rate")); + assert!(artifact.emitted.contains("Create Dummy Bot")); + assert!(artifact.emitted.contains("Wait Until")); + assert_eq!(artifact.emitted.matches("Create HUD Text").count(), 2); +} + +#[test] +fn builtin_alias_preserves_source_identity_and_canonical_target() { + let source = "rule \"r\":\n @Event global\n @Condition horizontalAngleFromDirection(vect(1, 0, 0)) == 90\n"; + let hir = crate::compile(source, "source.opy", Path::new(".")).expect("alias must resolve"); + let artifact = Compiler::new() + .expect("released Workshop contract must load") + .compile_hir(&hir) + .expect("alias must lower through the canonical catalog"); + assert!( + value_call_names(&artifact) + .iter() + .any(|name| name == "horizontalAngleFromDirection") + ); +} + +#[test] +fn builtin_surface_rejects_invalid_arity_and_keyword_with_source_diagnostics() { + let error = crate::compile( + "rule \"r\":\n @Event global\n createDummy(Hero.ANA, Team.ALL)\n", + "source.opy", + Path::new("."), + ) + .expect_err("createDummy must require its source signature"); + assert_eq!(error.code, "missing-argument"); + assert_eq!(error.span.expect("arity provenance").start.line, 3); + + let error = crate::compile( + "rule \"r\":\n @Event global\n hudHeader(getAllPlayers(), \"header\", bad=HudPosition.TOP, 0, Color.WHITE, HudReeval.VISIBILITY)\n", + "source.opy", + Path::new("."), + ) + .expect_err("unknown HUD keyword must be rejected"); + assert_eq!(error.code, "unknown-keyword"); + assert!(error.message.contains("bad")); +} + +#[test] +fn create_dummy_uses_the_reference_facing_default() { + let source = + "rule \"r\":\n @Event global\n createDummy(Hero.ANA, Team.ALL, -1, vect(0, 0, 0))\n"; + 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("createDummy's facing default must lower"); + assert!(artifact.emitted.contains("Create Dummy Bot")); + assert!(artifact.emitted.contains("Vector(0, 0, 0)")); +} diff --git a/crates/opy-rs/src/compiler/tests/issue_99_numeric_range.rs b/crates/opy-rs/src/compiler/tests/issue_99_numeric_range.rs index 513686d..eee7a67 100644 --- a/crates/opy-rs/src/compiler/tests/issue_99_numeric_range.rs +++ b/crates/opy-rs/src/compiler/tests/issue_99_numeric_range.rs @@ -121,11 +121,11 @@ fn motivating_project_advances_past_numeric_range_frontend_gap() { ); assert_eq!(report.compile.status, CompileStatus::Failure); - assert_eq!(report.compile.diagnostics[0].code, "unknown-value"); + assert_eq!(report.compile.diagnostics[0].code, "unknown-identifier"); assert!( report.compile.diagnostics[0] .message - .contains("isAssemblingHeroes") + .contains("RULE_CONDITION") ); } diff --git a/crates/opy-rs/src/manifest/data/manifest.json b/crates/opy-rs/src/manifest/data/manifest.json index 9e40bbc..440902f 100644 --- a/crates/opy-rs/src/manifest/data/manifest.json +++ b/crates/opy-rs/src/manifest/data/manifest.json @@ -23,6 +23,46 @@ "kind": "memberAlias", "source": "hasStatusEffect", "target": "hasStatus" + }, + { + "evidence": [ + "builtin-surface" + ], + "kind": "functionAlias", + "source": "horizontalAngleFromDirection", + "target": "horizontalAngleOfDirection" + }, + { + "evidence": [ + "builtin-surface" + ], + "kind": "memberAlias", + "source": "isHoldingButton", + "target": "isButtonHeld" + }, + { + "evidence": [ + "builtin-surface" + ], + "kind": "functionAlias", + "source": "angleToDirection", + "target": "directionFromAngles" + }, + { + "evidence": [ + "builtin-surface" + ], + "kind": "functionAlias", + "source": "buttonString", + "target": "inputBindingString" + }, + { + "evidence": [ + "builtin-surface" + ], + "kind": "memberAlias", + "source": "disableHeroHUD", + "target": "disableHeroHud" } ], "functions": [ @@ -45,6 +85,22 @@ } ] }, + { + "catalogId": "waitUntil", + "evidence": [ + "builtin-surface" + ], + "id": "waitUntil", + "kind": "action", + "params": [ + { + "name": "condition" + }, + { + "name": "timeout" + } + ] + }, { "catalogId": "disableInspector", "evidence": [ @@ -116,6 +172,90 @@ } ] }, + { + "catalogLink": "special-lowering", + "evidence": [ + "builtin-surface" + ], + "id": "hudHeader", + "kind": "action", + "params": [ + { + "name": "visibleTo" + }, + { + "name": "text" + }, + { + "alternateNames": [ + "position" + ], + "domain": "HudPosition", + "name": "location" + }, + { + "name": "sortOrder" + }, + { + "domain": "Color", + "name": "color" + }, + { + "domain": "HudReeval", + "name": "reevaluation" + }, + { + "alternateNames": [ + "spectators" + ], + "default": "DEFAULT", + "domain": "SpecVisibility", + "name": "specVisibility" + } + ] + }, + { + "catalogLink": "special-lowering", + "evidence": [ + "builtin-surface" + ], + "id": "hudSubtext", + "kind": "action", + "params": [ + { + "name": "visibleTo" + }, + { + "name": "text" + }, + { + "alternateNames": [ + "position" + ], + "domain": "HudPosition", + "name": "location" + }, + { + "name": "sortOrder" + }, + { + "domain": "Color", + "name": "color" + }, + { + "domain": "HudReeval", + "name": "reevaluation" + }, + { + "alternateNames": [ + "spectators" + ], + "default": "DEFAULT", + "domain": "SpecVisibility", + "name": "specVisibility" + } + ] + }, { "catalogId": "createBeamEffect", "evidence": [ @@ -220,6 +360,31 @@ } ] }, + { + "catalogId": "chaseAtRate", + "evidence": [ + "builtin-surface" + ], + "id": "chaseAtRate", + "kind": "action", + "params": [ + { + "name": "variable", + "variable": true + }, + { + "name": "destination" + }, + { + "name": "rate" + }, + { + "default": "DESTINATION_AND_RATE", + "domain": "ChaseRateReeval", + "name": "reevaluation" + } + ] + }, { "catalogId": "chaseOverTime", "evidence": [ @@ -307,6 +472,36 @@ "kind": "value", "params": [] }, + { + "catalogId": "directionFromAngles", + "evidence": [ + "builtin-surface" + ], + "id": "directionFromAngles", + "kind": "value", + "params": [ + { + "name": "horizontalAngle" + }, + { + "name": "verticalAngle" + } + ] + }, + { + "catalogId": "inputBindingString", + "evidence": [ + "builtin-surface" + ], + "id": "inputBindingString", + "kind": "value", + "params": [ + { + "domain": "Button", + "name": "button" + } + ] + }, { "catalogId": "randomReal", "evidence": [ @@ -338,6 +533,117 @@ } ] }, + { + "catalogId": "randomInteger", + "evidence": [ + "builtin-surface" + ], + "id": "random.randint", + "keywordArgs": false, + "kind": "value", + "params": [ + { + "name": "min" + }, + { + "name": "max" + } + ] + }, + { + "catalogId": "randomizedArray", + "evidence": [ + "builtin-surface" + ], + "id": "random.shuffle", + "keywordArgs": false, + "kind": "value", + "params": [ + { + "name": "array" + } + ] + }, + { + "catalogId": "allPlayers", + "evidence": [ + "builtin-surface" + ], + "id": "getPlayers", + "kind": "value", + "params": [ + { + "default": "ALL", + "domain": "Team", + "name": "team" + } + ] + }, + { + "catalogLink": "special-lowering", + "evidence": [ + "builtin-surface" + ], + "id": "all", + "kind": "value", + "params": [ + { + "name": "array" + } + ] + }, + { + "catalogLink": "special-lowering", + "evidence": [ + "builtin-surface" + ], + "id": "any", + "kind": "value", + "params": [ + { + "name": "array" + } + ] + }, + { + "catalogLink": "special-lowering", + "evidence": [ + "builtin-surface" + ], + "id": "ceil", + "kind": "value", + "params": [ + { + "name": "value" + } + ] + }, + { + "catalogLink": "special-lowering", + "evidence": [ + "builtin-surface" + ], + "id": "floor", + "kind": "value", + "params": [ + { + "name": "value" + } + ] + }, + { + "catalogLink": "special-lowering", + "evidence": [ + "builtin-surface" + ], + "id": "round", + "kind": "value", + "params": [ + { + "name": "value" + } + ] + }, { "catalogLink": "special-lowering", "context": "forIterable", @@ -389,6 +695,69 @@ "kind": "value", "params": [] }, + { + "catalogId": "isAssemblingHeroes", + "evidence": [ + "builtin-surface" + ], + "id": "isAssemblingHeroes", + "kind": "value", + "params": [] + }, + { + "catalogId": "setMatchTime", + "evidence": [ + "builtin-surface" + ], + "id": "setMatchTime", + "kind": "action", + "params": [ + { + "name": "time" + } + ] + }, + { + "catalogId": "horizontalAngleFromDirection", + "evidence": [ + "builtin-surface" + ], + "id": "horizontalAngleOfDirection", + "kind": "value", + "params": [ + { + "name": "direction" + } + ] + }, + { + "catalogId": "createDummyBot", + "evidence": [ + "builtin-surface" + ], + "id": "createDummy", + "kind": "action", + "params": [ + { + "domain": "Hero", + "name": "hero" + }, + { + "domain": "Team", + "name": "team" + }, + { + "name": "slot" + }, + { + "name": "position" + }, + { + "name": "facing", + "optional": true + } + ] + }, { "catalogId": "getPlayersInRadius", "evidence": [ @@ -681,6 +1050,76 @@ "params": [], "receiver": "Player" }, + { + "catalogId": "getFacingDirection", + "evidence": [ + "builtin-surface" + ], + "id": "getFacingDirection", + "kind": "memberValue", + "params": [], + "receiver": "Player" + }, + { + "catalogId": "isButtonHeld", + "evidence": [ + "builtin-surface" + ], + "id": "isButtonHeld", + "kind": "memberValue", + "params": [ + { + "domain": "Button", + "name": "button" + } + ], + "receiver": "Player" + }, + { + "catalogId": "startFacing", + "evidence": [ + "builtin-surface" + ], + "id": "startFacing", + "kind": "memberAction", + "params": [ + { + "name": "direction" + }, + { + "name": "turnRate" + }, + { + "domain": "Relativity", + "name": "relativity" + }, + { + "domain": "FacingReeval", + "name": "reevaluation" + } + ], + "receiver": "Player" + }, + { + "catalogId": "disableHeroHud", + "evidence": [ + "builtin-surface" + ], + "id": "disableHeroHud", + "kind": "memberAction", + "params": [], + "receiver": "Player" + }, + { + "catalogId": "enableHeroHud", + "evidence": [ + "builtin-surface" + ], + "id": "enableHeroHud", + "kind": "memberAction", + "params": [], + "receiver": "Player" + }, { "catalogLink": "catalog-gap", "evidence": [ diff --git a/crates/opy-rs/src/manifest/probes/builtin-surface.opy b/crates/opy-rs/src/manifest/probes/builtin-surface.opy new file mode 100644 index 0000000..3ece92f --- /dev/null +++ b/crates/opy-rs/src/manifest/probes/builtin-surface.opy @@ -0,0 +1,27 @@ +globalvar g + +rule "builtin surface": + @Event eachPlayer + @Condition isAssemblingHeroes() == false + @Condition horizontalAngleFromDirection(vect(1, 0, 0)) == 90 + @Condition eventPlayer.isHoldingButton(Button.INTERACT) + @Condition horizontalAngleFromDirection(eventPlayer.getFacingDirection()) == 90 + @Condition any([g > 0 for g in [1, 2]]) + @Condition all([g > 0 for g in [1, 2]]) + @Condition ceil(1.2) == 2 + @Condition floor(1.8) == 1 + @Condition round(1.5) == 2 + g = getPlayers(Team.ALL) + g = angleToDirection(30, 0) + g = buttonString(Button.INTERACT) + g = random.randint(1, 3) + g = random.shuffle([1, 2]) + chaseAtRate(g, 1, 1) + createDummy(Hero.ANA, Team.ALL, -1, vect(0, 0, 0), vect(0, 0, 0)) + hudHeader(getAllPlayers(), "header", HudPosition.TOP, 0, Color.WHITE, HudReeval.VISIBILITY, SpecVisibility.DEFAULT) + hudSubtext(getAllPlayers(), "text", HudPosition.TOP, 1, Color.WHITE, HudReeval.VISIBILITY, SpecVisibility.DEFAULT) + setMatchTime(0) + eventPlayer.startFacing(vect(0, 0, 1), 1, Relativity.TO_WORLD, FacingReeval.DIRECTION_AND_TURN_RATE) + eventPlayer.disableHeroHUD() + eventPlayer.enableHeroHud() + waitUntil(true, 1) diff --git a/crates/opy-rs/src/manifest/probes/probes.json b/crates/opy-rs/src/manifest/probes/probes.json index 8ec40e5..128c0af 100644 --- a/crates/opy-rs/src/manifest/probes/probes.json +++ b/crates/opy-rs/src/manifest/probes/probes.json @@ -1,6 +1,13 @@ { "schemaVersion": 1, "probes": [ + { + "expect": "success", + "id": "builtin-surface", + "outputSha256": "a39f1063b02efe65e96584872ab48c147cc5b2a0cb88acdd67e015c59a347124", + "sha256": "4912631d54d26e9fb5c242de1c30ba2fa75cb6e6a47cb6b9333e2af1c19c694b", + "source": "builtin-surface.opy" + }, { "diagnosticContains": "Expected a value, but got function 'wait' which is an action", "expect": "failure", diff --git a/docs/language-support/registries.md b/docs/language-support/registries.md index d2e7725..e5c41d3 100644 --- a/docs/language-support/registries.md +++ b/docs/language-support/registries.md @@ -106,10 +106,10 @@ Reference: `889d9749d1def17f146548cbddb94ea1ab015847`. | Upstream key | Status | Contract | | --- | --- | --- | -| `random.randint(min, max)` | 🚧 Coming soon | Two inclusive integer bounds; integer result. | -| `random.uniform(min, max)` | 🚧 Coming soon | Two float bounds; float result. | -| `random.choice(array)` | 🚧 Coming soon | One array; element result. | -| `random.shuffle(array)` | 🚧 Coming soon | One array; copied array result. | +| `random.randint(min, max)` | ✅ Supported | Two inclusive integer bounds; integer result. | +| `random.uniform(min, max)` | ✅ Supported | Two float bounds; float result. | +| `random.choice(array)` | ✅ Supported | One array; element result. | +| `random.shuffle(array)` | ✅ Supported | One array; copied array result. | ## Built-in macros (`src/data/opy/macros.ts`) @@ -122,7 +122,7 @@ Reference: `889d9749d1def17f146548cbddb94ea1ab015847`. | `.getRealPlayerClosestToReticle`, `.getRealPlayersClosestToReticle` | 🚧 Coming soon | | `getRealPlayersInRadius`, `.getRealPlayersInViewAngle` | 🚧 Coming soon | | `getSign`, `getAllPlayers` | 🚧 Coming soon | -| `hudHeader`, `hudSubtext` | 🚧 Coming soon | +| `hudHeader`, `hudSubtext` | ✅ Supported | | `hudSubheader` | ✅ Bounded compiler slice | | `lerp`, `lineIntersectsSphere` | 🚧 Coming soon | | `print`, `.reverse`, `timeToString`, `.unique` | 🚧 Coming soon | @@ -144,12 +144,12 @@ beginning with `.` are receiver dispatch entries. The separate | `.filter` | ✅ Supported | | `.map` | ✅ Supported | | `arrayToString` | 🚧 Coming soon | -| `ceil` | 🚧 Coming soon | -| `floor` | 🚧 Coming soon | -| `round` | 🚧 Coming soon | +| `ceil` | ✅ Supported | +| `floor` | ✅ Supported | +| `round` | ✅ Supported | | `hsl` | 🚧 Coming soon | -| `chaseAtRate` | 🚧 Coming soon | -| `chaseOverTime` | 🚧 Coming soon | +| `chaseAtRate` | ✅ Supported | +| `chaseOverTime` | ✅ Supported | | `compress` | 🚧 Coming soon | | `compressed` | 🚧 Coming soon | | `decompressNumbers` | 🚧 Coming soon | diff --git a/docs/overpy-support/callables-and-domains.md b/docs/overpy-support/callables-and-domains.md index 2c76525..a598293 100644 --- a/docs/overpy-support/callables-and-domains.md +++ b/docs/overpy-support/callables-and-domains.md @@ -23,10 +23,11 @@ behavior, and dispatch rule. | `raiseToPower(base, exponent)` | ✅ Supported | Two numeric arguments in order; value operation. | | `sorted(array[, key])` | ✅ Supported | Optional lambda key; element/index binder is contextual. | | `all(array)` / `any(array)` | ✅ Supported | One boolean-array value. | -| `random.randint(min, max)` | 🚧 Coming soon | Two inclusive integer bounds; integer result. | -| `random.uniform(min, max)` | 🚧 Coming soon | Two float bounds; float result. | -| `random.choice(array)` | 🚧 Coming soon | One array; returns an element or supplied non-array value. | -| `random.shuffle(array)` | 🚧 Coming soon | One array; returns a copied array. | +| `ceil(value)` / `floor(value)` / `round(value)` | ✅ Supported | Numeric rounding maps to the canonical `Rounding` domain. | +| `random.randint(min, max)` | ✅ Supported | Two inclusive integer bounds; integer result. | +| `random.uniform(min, max)` | ✅ Supported | Two float bounds; float result. | +| `random.choice(array)` | ✅ Supported | One array; returns an element or supplied non-array value. | +| `random.shuffle(array)` | ✅ Supported | One array; returns a copied array. | | `_(contextOrString[, string])` | 🚧 Coming soon | One-argument and two-argument modes differ. | ## Receiver/member functions From e1c0a53cb3188fabfed69c39a4a1e3b2e9da28d6 Mon Sep 17 00:00:00 2001 From: Teakowa Date: Tue, 1 Sep 2026 03:41:57 +0800 Subject: [PATCH 2/2] fix(opy): address builtin surface review Refs #144 --- compatibility/differential-expectations.json | 2 +- .../compiler/tests/issue_144_builtin_surface.rs | 4 +++- crates/opy-rs/src/lower.rs | 7 +++++++ crates/opy-rs/src/manifest/data/manifest.json | 14 ++++++++++++++ crates/opy-rs/src/manifest/mod.rs | 7 +++---- .../opy-rs/src/manifest/probes/builtin-surface.opy | 2 ++ crates/opy-rs/src/manifest/probes/probes.json | 4 ++-- crates/opy-rs/tests/differential.rs | 6 +++--- 8 files changed, 35 insertions(+), 11 deletions(-) diff --git a/compatibility/differential-expectations.json b/compatibility/differential-expectations.json index 26f9067..3c09431 100644 --- a/compatibility/differential-expectations.json +++ b/compatibility/differential-expectations.json @@ -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."}, diff --git a/crates/opy-rs/src/compiler/tests/issue_144_builtin_surface.rs b/crates/opy-rs/src/compiler/tests/issue_144_builtin_surface.rs index 46004af..3f35882 100644 --- a/crates/opy-rs/src/compiler/tests/issue_144_builtin_surface.rs +++ b/crates/opy-rs/src/compiler/tests/issue_144_builtin_surface.rs @@ -39,6 +39,8 @@ rule "builtin surface": createDummy(Hero.ANA, Team.ALL, -1, vect(0, 0, 0), vect(0, 0, 0)) hudHeader(getAllPlayers(), "header", HudPosition.TOP, 0, Color.WHITE, HudReeval.VISIBILITY, SpecVisibility.DEFAULT) hudSubtext(getAllPlayers(), "text", HudPosition.TOP, 1, Color.WHITE, HudReeval.VISIBILITY, SpecVisibility.DEFAULT) + hudHeader(text="default header") + hudSubtext(text="default text") waitUntil(true, 1) "#; let hir = crate::compile(source, "builtin-surface.opy", Path::new(".")) @@ -66,7 +68,7 @@ rule "builtin surface": assert!(artifact.emitted.contains("Chase Global Variable At Rate")); assert!(artifact.emitted.contains("Create Dummy Bot")); assert!(artifact.emitted.contains("Wait Until")); - assert_eq!(artifact.emitted.matches("Create HUD Text").count(), 2); + assert_eq!(artifact.emitted.matches("Create HUD Text").count(), 4); } #[test] diff --git a/crates/opy-rs/src/lower.rs b/crates/opy-rs/src/lower.rs index 901f8a7..936692f 100644 --- a/crates/opy-rs/src/lower.rs +++ b/crates/opy-rs/src/lower.rs @@ -1810,6 +1810,13 @@ impl Lowerer { match &slots[index] { Some(value) => bound.push(value.clone()), None => match ¶m.default { + Some(ParamDefault::Call { call }) => { + bound.push(HirExpr::Call { + name: call.clone(), + args: Vec::new(), + span: None, + }); + } Some(ParamDefault::EnumMember(member)) => { let domain = param.domain.clone().unwrap_or_default(); bound.push(HirExpr::Enum { diff --git a/crates/opy-rs/src/manifest/data/manifest.json b/crates/opy-rs/src/manifest/data/manifest.json index 440902f..a3f88c1 100644 --- a/crates/opy-rs/src/manifest/data/manifest.json +++ b/crates/opy-rs/src/manifest/data/manifest.json @@ -181,6 +181,9 @@ "kind": "action", "params": [ { + "default": { + "call": "getAllPlayers" + }, "name": "visibleTo" }, { @@ -190,17 +193,21 @@ "alternateNames": [ "position" ], + "default": "LEFT", "domain": "HudPosition", "name": "location" }, { + "default": 0, "name": "sortOrder" }, { + "default": "WHITE", "domain": "Color", "name": "color" }, { + "default": "VISIBILITY_SORT_ORDER_STRING_AND_COLOR", "domain": "HudReeval", "name": "reevaluation" }, @@ -223,6 +230,9 @@ "kind": "action", "params": [ { + "default": { + "call": "getAllPlayers" + }, "name": "visibleTo" }, { @@ -232,17 +242,21 @@ "alternateNames": [ "position" ], + "default": "LEFT", "domain": "HudPosition", "name": "location" }, { + "default": 0, "name": "sortOrder" }, { + "default": "WHITE", "domain": "Color", "name": "color" }, { + "default": "VISIBILITY_SORT_ORDER_STRING_AND_COLOR", "domain": "HudReeval", "name": "reevaluation" }, diff --git a/crates/opy-rs/src/manifest/mod.rs b/crates/opy-rs/src/manifest/mod.rs index 42971e1..2d73d21 100644 --- a/crates/opy-rs/src/manifest/mod.rs +++ b/crates/opy-rs/src/manifest/mod.rs @@ -137,13 +137,12 @@ impl ReceiverCategory { } } -/// A parameter default that the frontend expands: an enum member -/// (`"MEMBER"`) or a scalar (`0.016`). Only enum-member defaults are -/// expanded at lowering (matching the reference emission); scalar defaults -/// are declared data (the `wait` special form fills its own). +/// A parameter default that the frontend expands: a function call, enum +/// member (`"MEMBER"`), or scalar (`0.016`). #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(untagged)] pub enum ParamDefault { + Call { call: String }, EnumMember(String), Number(f64), } diff --git a/crates/opy-rs/src/manifest/probes/builtin-surface.opy b/crates/opy-rs/src/manifest/probes/builtin-surface.opy index 3ece92f..33c4259 100644 --- a/crates/opy-rs/src/manifest/probes/builtin-surface.opy +++ b/crates/opy-rs/src/manifest/probes/builtin-surface.opy @@ -20,6 +20,8 @@ rule "builtin surface": createDummy(Hero.ANA, Team.ALL, -1, vect(0, 0, 0), vect(0, 0, 0)) hudHeader(getAllPlayers(), "header", HudPosition.TOP, 0, Color.WHITE, HudReeval.VISIBILITY, SpecVisibility.DEFAULT) hudSubtext(getAllPlayers(), "text", HudPosition.TOP, 1, Color.WHITE, HudReeval.VISIBILITY, SpecVisibility.DEFAULT) + hudHeader(text="default header") + hudSubtext(text="default text") setMatchTime(0) eventPlayer.startFacing(vect(0, 0, 1), 1, Relativity.TO_WORLD, FacingReeval.DIRECTION_AND_TURN_RATE) eventPlayer.disableHeroHUD() diff --git a/crates/opy-rs/src/manifest/probes/probes.json b/crates/opy-rs/src/manifest/probes/probes.json index 128c0af..58136d7 100644 --- a/crates/opy-rs/src/manifest/probes/probes.json +++ b/crates/opy-rs/src/manifest/probes/probes.json @@ -4,8 +4,8 @@ { "expect": "success", "id": "builtin-surface", - "outputSha256": "a39f1063b02efe65e96584872ab48c147cc5b2a0cb88acdd67e015c59a347124", - "sha256": "4912631d54d26e9fb5c242de1c30ba2fa75cb6e6a47cb6b9333e2af1c19c694b", + "outputSha256": "ea2886dbfbb79173f75ce0f175b8cfa0eca3e4927dce6a6bd787569f1c194bb8", + "sha256": "6ef72b624ced1743001d0121767394e0b660ecb507092c2169fb373dcea79493", "source": "builtin-surface.opy" }, { diff --git a/crates/opy-rs/tests/differential.rs b/crates/opy-rs/tests/differential.rs index 6d765b5..a749ebd 100644 --- a/crates/opy-rs/tests/differential.rs +++ b/crates/opy-rs/tests/differential.rs @@ -478,11 +478,11 @@ fn declared_corpus() -> BTreeMap<&'static str, Case> { Some("parse-error"), "the chained ternary, isDummy, and getHorizontalFacingAngle regressions resolve; the full project reaches the next unknown value (`horizontalAngleOfDirection`). Gap: reference accepts, native rejects (documented).", ); - diagnostic( + resolve( &mut cases, "real-world/overpy-crosshair", - Some("unknown-action"), - "the SpecVisibility.NEVER and hudSubheader boundaries now resolve; the full project reaches the separate unsupported hudSubtext action. Gap: reference accepts, native rejects (documented).", + false, + "the frontend resolves the full project through the audited HUD builtin surface; compiler-only lowering retains the separate stringModifier integration boundary.", ); diagnostic( &mut cases,