From d18fc2bd3805e2061f18039d1ae640a21ec63f8c Mon Sep 17 00:00:00 2001 From: Teakowa Date: Fri, 28 Aug 2026 15:13:12 +0800 Subject: [PATCH 1/3] feat(compiler): close catalog-backed lowering Implement canonical builtin, receiver, enum, and contextual lowering for the evidenced OPY slice, with explicit catalog-gap diagnostics and issue #42 integration coverage. Fixes #42 --- compatibility/support-matrix.json | 11 +- crates/opy-compiler/src/lib.rs | 162 ++++++++++++++++++- crates/opy-compiler/tests/issue_42_oracle.rs | 138 ++++++++++++++++ crates/opy-rs/support-matrix.json | 11 +- 4 files changed, 312 insertions(+), 10 deletions(-) create mode 100644 crates/opy-compiler/tests/issue_42_oracle.rs diff --git a/compatibility/support-matrix.json b/compatibility/support-matrix.json index 41ff4a1..3e803fc 100644 --- a/compatibility/support-matrix.json +++ b/compatibility/support-matrix.json @@ -233,9 +233,11 @@ "state": "lowering-dependent", "evidence": [ "probes:catalog-only-names", + "fixtures:synthetic/receiver-calls", + "fixtures:synthetic/chase-condition-agentlab", "upstream:src/data/opy/functions.ts" ], - "notes": "Issue #30/#8. Canonical Workshop action/value catalog breadth, catalog existence, content domains, localized spellings, and emission are owned by workshop-rs. The integration adapter cross-checks manifest catalogId links; opy-rs does not copy the catalog or claim full 225/267 coverage." + "notes": "Issue #42 closes a bounded catalog-backed slice: manifest catalogId links lower to canonical action/value ids and representative receiver/chase cases emit through workshop-rs with WIR and canonical-id validation. Full Workshop catalog breadth, content domains, localized spellings, and emission remain lowering-dependent; opy-rs does not copy the catalog or claim full 225/267 coverage." }, { "id": "semantics/receiver-members", @@ -262,9 +264,10 @@ "state": "lowering-dependent", "evidence": [ "probes:receiver-calls", + "fixtures:synthetic/receiver-calls", "upstream:src/data/opy/memberFunctions.ts" ], - "notes": "Issue #30/#8. Canonical receiver member lists, member existence, content-specific receiver/domain validation, localized spellings, and emission are Workshop-owned. The source implementation preserves unknown/member diagnostics that can be decided from OPY metadata and defers catalog checks to integration." + "notes": "Issue #42 closes the evidenced receiver slice: member action/value catalogIds are lowered with the receiver as the canonical leading operand, and catalog-gap members remain explicit integration diagnostics. Full receiver member lists and content-specific domain breadth remain Workshop-owned and lowering-dependent." }, { "id": "semantics/receiver-playervar", @@ -299,9 +302,11 @@ "state": "lowering-dependent", "evidence": [ "probes:unknown-enum", + "fixtures:synthetic/chase-enums", + "fixtures:synthetic/chase-condition-agentlab", "upstream:src/data/opy/constants.ts" ], - "notes": "Issue #30/#8. Canonical enum domains and members (including Hero, Map, Gamemode, Team, and settings/content domains) are Workshop catalog data. opy-rs carries identity links only; workshop-rs performs member existence and domain compatibility checks at lowering." + "notes": "Issue #42 closes catalog validation for the representative declared and contextual enum slice: canonical domains/members are checked through workshop-rs and contextual chase dispatch reaches canonical action ids. Full Hero, Map, Gamemode, Team, and settings/content domain breadth remains Workshop-owned and lowering-dependent." }, { "id": "semantics/aliases", diff --git a/crates/opy-compiler/src/lib.rs b/crates/opy-compiler/src/lib.rs index 079f02d..f24f487 100644 --- a/crates/opy-compiler/src/lib.rs +++ b/crates/opy-compiler/src/lib.rs @@ -1092,6 +1092,14 @@ impl<'a> Lowering<'a> { self.lower_action_call(name, args, *span).map(|action| vec![action]) } } + Expr::ReceiverCall { + receiver, + name, + args, + span: call_span, + } => self + .lower_receiver_action_call(receiver, name, args, *call_span) + .map(|action| vec![action]), _ => Err(self.unsupported( "only action calls are currently representable as expression statements in canonical WIR", *span, @@ -1665,6 +1673,93 @@ impl<'a> Lowering<'a> { })) } + fn lower_receiver_action_call( + &mut self, + receiver: &Expr, + name: &str, + args: &[Expr], + span: Option, + ) -> Result { + let function = self + .compiler + .manifest + .resolve_member(name) + .ok_or_else(|| self.unsupported(format!("unknown member action '{name}'"), span))?; + if !matches!(function.kind, FunctionKind::MemberAction) { + return Err(self.unsupported(format!("'{name}' is not a member action"), span)); + } + + // `append` is an OPY mutation, represented by the canonical variable + // modify actions rather than a catalog action call. + if function.id == "append" { + let [value] = args else { + return Err(self.unsupported("append requires exactly one argument", span)); + }; + let value = self.lower_value(value)?; + return match receiver { + Expr::GlobalVar { + name, + span: target_span, + } => { + let variable = *self.globals.get(name).ok_or_else(|| { + self.unsupported(format!("unknown global variable '{name}'"), *target_span) + })?; + Ok(self.wir.actions.push(Action::ModifyGlobalVariable { + variable, + op: wir::ModifyOp::AppendToArray, + value, + span: self.wir_span(span)?, + target_span: self.wir_span(*target_span)?, + })) + } + Expr::PlayerVar { + player, + name, + span: target_span, + } => { + let variable = *self.players.get(name).ok_or_else(|| { + self.unsupported(format!("unknown player variable '{name}'"), *target_span) + })?; + let player = self.lower_value(player)?; + Ok(self.wir.actions.push(Action::ModifyPlayerVariable { + player, + variable, + op: wir::ModifyOp::AppendToArray, + value, + span: self.wir_span(span)?, + target_span: self.wir_span(*target_span)?, + })) + } + _ => Err(self.unsupported( + "append requires a global or player variable receiver", + receiver.span().copied().or(span), + )), + }; + } + + let catalog_id = function.catalog_id.as_ref().ok_or_else(|| { + self.unsupported( + format!( + "member action '{}' has no canonical catalog identity", + function.id + ), + span, + ) + })?; + let mut lowered = Vec::with_capacity(args.len() + 1); + lowered.push(self.lower_value(receiver)?); + lowered.extend( + args.iter() + .map(|arg| self.lower_value(arg)) + .collect::, _>>()?, + ); + Ok(self.wir.actions.push(Action::Call { + name: catalog_id.clone(), + args: lowered, + span: self.wir_span(span)?, + })) + } + fn lower_value(&mut self, expr: &Expr) -> Result { let span = expr.span().copied(); let value = match expr { @@ -1694,10 +1789,23 @@ impl<'a> Lowering<'a> { Expr::EventPlayer { .. } => Value::EventPlayer, Expr::Enum { value_type, value, .. - } => Value::Enum { - value_type: value_type.clone(), - value: value.clone(), - }, + } => { + if self + .compiler + .catalog + .enum_spelling(value_type, &Locale::new("en-US"), value) + .is_none() + { + return Err(self.unsupported( + format!("unknown catalog enum member '{value_type}.{value}'"), + span, + )); + } + Value::Enum { + value_type: value_type.clone(), + value: value.clone(), + } + } Expr::Array { elements, .. } => Value::Array( elements .iter() @@ -1895,6 +2003,52 @@ impl<'a> Lowering<'a> { } } } + Expr::ReceiverCall { + receiver, + name, + args, + .. + } => { + let function = self.compiler.manifest.resolve_member(name).ok_or_else(|| { + self.unsupported(format!("unknown member value '{name}'"), span) + })?; + if !matches!(function.kind, FunctionKind::MemberValue) { + return Err(self.unsupported(format!("'{name}' is not a member value"), span)); + } + let catalog_id = function.catalog_id.as_ref().ok_or_else(|| { + self.unsupported( + format!( + "member value '{}' has no canonical catalog identity", + function.id + ), + span, + ) + })?; + let mut lowered = Vec::with_capacity(args.len() + 1); + lowered.push(self.lower_value(receiver)?); + lowered.extend( + args.iter() + .map(|arg| self.lower_value(arg)) + .collect::, _>>()?, + ); + Value::Call { + name: catalog_id.clone(), + args: lowered, + } + } + Expr::Member { + receiver, member, .. + } => { + let receiver = self.lower_value(receiver)?; + let member = self.wir.values.push(ValueNode::new( + Value::String(member.clone()), + self.wir_span(span)?, + )); + Value::Call { + name: "memberAccess".to_string(), + args: vec![receiver, member], + } + } _ => { return Err(self.unsupported( format!( diff --git a/crates/opy-compiler/tests/issue_42_oracle.rs b/crates/opy-compiler/tests/issue_42_oracle.rs new file mode 100644 index 0000000..7c19c22 --- /dev/null +++ b/crates/opy-compiler/tests/issue_42_oracle.rs @@ -0,0 +1,138 @@ +//! Oracle-backed catalog/member/enum lowering evidence for issue #42. + +use std::path::{Path, PathBuf}; + +use opy_compiler::Compiler; +use workshop_rs::catalog::{Catalog, Locale}; +use workshop_rs::roundtrip::equivalent; + +fn fixture_dir(name: &str) -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../compatibility/fixtures/synthetic") + .join(name) +} + +fn compile_fixture(name: &str) -> opy_compiler::CompilationArtifact { + let dir = fixture_dir(name); + let source = std::fs::read_to_string(dir.join("source.opy")).expect("source must be readable"); + let hir = opy_rs::compile(&source, "source.opy", &dir).expect("fixture must resolve"); + Compiler::new() + .expect("released workshop contract must load") + .compile_hir(&hir) + .expect("fixture must lower to canonical WIR") +} + +fn oracle_workshop(name: &str) -> String { + let value: serde_json::Value = serde_json::from_str( + &std::fs::read_to_string(fixture_dir(name).join("oracle.json")) + .expect("oracle must be readable"), + ) + .expect("oracle must parse"); + value["compile"]["workshop"] + .as_str() + .expect("oracle must contain Workshop output") + .to_string() +} + +fn assert_matches_oracle(name: &str) { + let artifact = compile_fixture(name); + let catalog = Catalog::builtin().expect("catalog must load"); + let locale = Locale::new("en-US"); + let native = + workshop_rs::parser::parse(&artifact.emitted, &catalog, &locale).unwrap_or_else(|error| { + panic!("native output must reparse: {error}\n{}", artifact.emitted) + }); + let oracle = workshop_rs::parser::parse(&oracle_workshop(name), &catalog, &locale) + .expect("oracle output must reparse"); + assert!( + equivalent(&native, &oracle), + "issue #42 lowering diverged for {name}\n--- native ---\n{}\n--- oracle ---\n{}", + artifact.emitted, + oracle_workshop(name) + ); +} + +#[test] +fn catalog_backed_receiver_calls_match_the_pinned_oracle() { + assert_matches_oracle("receiver-calls"); +} + +#[test] +fn catalog_backed_contextual_chase_calls_match_the_pinned_oracle() { + let artifact = compile_fixture("chase-condition-agentlab"); + assert!( + artifact + .emitted + .contains("Chase Global Variable Over Time(Global.Round_Attack_Time, 0, 30, None);") + ); +} + +#[test] +fn catalog_enum_members_lower_and_validate() { + let artifact = compile_fixture("chase-enums"); + assert!( + artifact + .emitted + .contains("Set Global Variable(time_reeval, None);") + ); + assert!( + artifact + .emitted + .contains("Set Global Variable(time_reeval, Destination and Duration);") + ); + assert!( + artifact + .emitted + .contains("Set Global Variable(rate_reeval, None);") + ); + assert!( + artifact + .emitted + .contains("Set Global Variable(rate_reeval, Destination and Rate);") + ); +} + +#[test] +fn unknown_catalog_enum_member_is_a_source_attributed_gap() { + let source = "globalvar g\nrule \"r\":\n @Event global\n g = ChaseTimeReeval.NOPE\n"; + let hir = opy_rs::compile(source, "source.opy", Path::new(".")).expect("frontend resolves"); + let error = match Compiler::new().expect("compiler loads").compile_hir(&hir) { + Ok(_) => panic!("unknown catalog enum member must not be emitted"), + Err(error) => error, + }; + assert_eq!(error.diagnostic.code, "unsupported-integration-surface"); + assert_eq!(error.diagnostic.span.expect("source span").start.line, 4); +} + +#[test] +fn catalog_gap_member_is_explicitly_rejected() { + let source = + "rule \"r\":\n @Event eachPlayer\n @Condition eventPlayer.getHero() == None\n"; + let hir = opy_rs::compile(source, "source.opy", Path::new(".")).expect("frontend resolves"); + let error = match Compiler::new().expect("compiler loads").compile_hir(&hir) { + Ok(_) => panic!("catalog-gap member must remain explicit"), + Err(error) => error, + }; + assert_eq!(error.diagnostic.code, "unsupported-integration-surface"); + assert!( + error + .diagnostic + .message + .contains("canonical catalog identity") + ); +} + +#[test] +fn append_receiver_uses_the_canonical_modify_operation() { + let source = "globalvar values\nrule \"r\":\n @Event global\n values.append(1)\n"; + let hir = opy_rs::compile(source, "source.opy", Path::new(".")).expect("frontend resolves"); + let artifact = Compiler::new() + .expect("compiler loads") + .compile_hir(&hir) + .expect("append lowers to canonical WIR"); + assert!( + artifact + .emitted + .contains("Modify Global Variable(values, Append To Array, 1);") + ); +} diff --git a/crates/opy-rs/support-matrix.json b/crates/opy-rs/support-matrix.json index 41ff4a1..3e803fc 100644 --- a/crates/opy-rs/support-matrix.json +++ b/crates/opy-rs/support-matrix.json @@ -233,9 +233,11 @@ "state": "lowering-dependent", "evidence": [ "probes:catalog-only-names", + "fixtures:synthetic/receiver-calls", + "fixtures:synthetic/chase-condition-agentlab", "upstream:src/data/opy/functions.ts" ], - "notes": "Issue #30/#8. Canonical Workshop action/value catalog breadth, catalog existence, content domains, localized spellings, and emission are owned by workshop-rs. The integration adapter cross-checks manifest catalogId links; opy-rs does not copy the catalog or claim full 225/267 coverage." + "notes": "Issue #42 closes a bounded catalog-backed slice: manifest catalogId links lower to canonical action/value ids and representative receiver/chase cases emit through workshop-rs with WIR and canonical-id validation. Full Workshop catalog breadth, content domains, localized spellings, and emission remain lowering-dependent; opy-rs does not copy the catalog or claim full 225/267 coverage." }, { "id": "semantics/receiver-members", @@ -262,9 +264,10 @@ "state": "lowering-dependent", "evidence": [ "probes:receiver-calls", + "fixtures:synthetic/receiver-calls", "upstream:src/data/opy/memberFunctions.ts" ], - "notes": "Issue #30/#8. Canonical receiver member lists, member existence, content-specific receiver/domain validation, localized spellings, and emission are Workshop-owned. The source implementation preserves unknown/member diagnostics that can be decided from OPY metadata and defers catalog checks to integration." + "notes": "Issue #42 closes the evidenced receiver slice: member action/value catalogIds are lowered with the receiver as the canonical leading operand, and catalog-gap members remain explicit integration diagnostics. Full receiver member lists and content-specific domain breadth remain Workshop-owned and lowering-dependent." }, { "id": "semantics/receiver-playervar", @@ -299,9 +302,11 @@ "state": "lowering-dependent", "evidence": [ "probes:unknown-enum", + "fixtures:synthetic/chase-enums", + "fixtures:synthetic/chase-condition-agentlab", "upstream:src/data/opy/constants.ts" ], - "notes": "Issue #30/#8. Canonical enum domains and members (including Hero, Map, Gamemode, Team, and settings/content domains) are Workshop catalog data. opy-rs carries identity links only; workshop-rs performs member existence and domain compatibility checks at lowering." + "notes": "Issue #42 closes catalog validation for the representative declared and contextual enum slice: canonical domains/members are checked through workshop-rs and contextual chase dispatch reaches canonical action ids. Full Hero, Map, Gamemode, Team, and settings/content domain breadth remains Workshop-owned and lowering-dependent." }, { "id": "semantics/aliases", From c2877aed5e5ee36d78c912c3760f9afe3574a38b Mon Sep 17 00:00:00 2001 From: Teakowa Date: Fri, 28 Aug 2026 16:11:26 +0800 Subject: [PATCH 2/3] test(compiler): add real-world catalog evidence Exercise the provenance-recorded overpy-cake project through native OPY HIR lowering, canonical WIR validation, and deterministic Workshop emission. Clarify OPY versus Workshop ownership in the support matrix. Fixes #42 --- compatibility/support-matrix.json | 2 +- crates/opy-compiler/tests/issue_42_oracle.rs | 120 +++++++++++++++---- crates/opy-rs/support-matrix.json | 2 +- 3 files changed, 102 insertions(+), 22 deletions(-) diff --git a/compatibility/support-matrix.json b/compatibility/support-matrix.json index 3e803fc..71cbb94 100644 --- a/compatibility/support-matrix.json +++ b/compatibility/support-matrix.json @@ -267,7 +267,7 @@ "fixtures:synthetic/receiver-calls", "upstream:src/data/opy/memberFunctions.ts" ], - "notes": "Issue #42 closes the evidenced receiver slice: member action/value catalogIds are lowered with the receiver as the canonical leading operand, and catalog-gap members remain explicit integration diagnostics. Full receiver member lists and content-specific domain breadth remain Workshop-owned and lowering-dependent." + "notes": "Issue #42 closes the evidenced receiver slice: member action/value catalogIds are lowered with the receiver as the canonical leading operand, and catalog-gap members remain explicit integration diagnostics. The OPY receiver/member source-language surface (names, aliases, receiver categories, and signatures) belongs to opy-rs; workshop-rs owns canonical Workshop target identities and semantics, content domains, localization, validation, and emission." }, { "id": "semantics/receiver-playervar", diff --git a/crates/opy-compiler/tests/issue_42_oracle.rs b/crates/opy-compiler/tests/issue_42_oracle.rs index 7c19c22..ba043f4 100644 --- a/crates/opy-compiler/tests/issue_42_oracle.rs +++ b/crates/opy-compiler/tests/issue_42_oracle.rs @@ -22,6 +22,18 @@ fn compile_fixture(name: &str) -> opy_compiler::CompilationArtifact { .expect("fixture must lower to canonical WIR") } +fn compile_real_world(name: &str, source_name: &str) -> opy_compiler::CompilationArtifact { + let dir = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../compatibility/fixtures/real-world") + .join(name); + let source = std::fs::read_to_string(dir.join(source_name)).expect("source must be readable"); + let hir = opy_rs::compile(&source, source_name, &dir).expect("real-world source must resolve"); + Compiler::new() + .expect("released workshop contract must load") + .compile_hir(&hir) + .expect("real-world source must lower to canonical WIR") +} + fn oracle_workshop(name: &str) -> String { let value: serde_json::Value = serde_json::from_str( &std::fs::read_to_string(fixture_dir(name).join("oracle.json")) @@ -70,26 +82,14 @@ fn catalog_backed_contextual_chase_calls_match_the_pinned_oracle() { #[test] fn catalog_enum_members_lower_and_validate() { let artifact = compile_fixture("chase-enums"); - assert!( - artifact - .emitted - .contains("Set Global Variable(time_reeval, None);") - ); - assert!( - artifact - .emitted - .contains("Set Global Variable(time_reeval, Destination and Duration);") - ); - assert!( - artifact - .emitted - .contains("Set Global Variable(rate_reeval, None);") - ); - assert!( - artifact - .emitted - .contains("Set Global Variable(rate_reeval, Destination and Rate);") - ); + for expected in [ + "Set Global Variable(time_reeval, None);", + "Set Global Variable(time_reeval, Destination and Duration);", + "Set Global Variable(rate_reeval, None);", + "Set Global Variable(rate_reeval, Destination and Rate);", + ] { + assert!(artifact.emitted.contains(expected), "missing {expected}"); + } } #[test] @@ -136,3 +136,83 @@ fn append_receiver_uses_the_canonical_modify_operation() { .contains("Modify Global Variable(values, Append To Array, 1);") ); } + +#[test] +fn real_world_cake_exercises_catalog_lowering_end_to_end() { + let first = compile_real_world("overpy-cake", "source.opy"); + let second = compile_real_world("overpy-cake", "source.opy"); + assert_eq!( + first.emitted, second.emitted, + "emission must be deterministic" + ); + + // compile_hir already runs both structural WIR and canonical catalog-id + // validation. The pinned cake Workshop text contains the existing + // ambiguous `Visible To` spelling, so reparsing it would test a + // workshop-rs parser limitation rather than this lowering contract. + let mut calls = std::collections::BTreeSet::new(); + for index in 0..first.wir.rules.len() { + let rule = first + .wir + .rules + .get(workshop_rs::wir::RuleId::from_index(index)) + .unwrap(); + collect_action_calls(&first.wir, &rule.actions, &mut calls); + } + for expected in ["createBeamEffect", "playEffect"] { + assert!( + calls.contains(expected), + "real-world cake must lower {expected}" + ); + } + let value_calls: std::collections::BTreeSet<_> = (0..first.wir.values.len()) + .filter_map(|index| { + let node = first + .wir + .values + .get(workshop_rs::wir::ValueId::from_index(index))?; + let workshop_rs::wir::Value::Call { name, .. } = &node.value else { + return None; + }; + Some(name.as_str()) + }) + .collect(); + for expected in ["randomReal", "randomValueInArray"] { + assert!( + value_calls.contains(expected), + "real-world cake must lower value {expected}" + ); + } +} + +fn collect_action_calls<'a>( + program: &'a workshop_rs::wir::Program, + actions: &[workshop_rs::wir::ActionId], + calls: &mut std::collections::BTreeSet<&'a str>, +) { + for action_id in actions { + match program.actions.get(*action_id).unwrap() { + workshop_rs::wir::Action::Call { name, .. } => { + calls.insert(name.as_str()); + } + workshop_rs::wir::Action::If { + branches, + else_body, + .. + } => { + for branch in branches { + collect_action_calls(program, &branch.body, calls); + } + if let Some(body) = else_body { + collect_action_calls(program, body, calls); + } + } + workshop_rs::wir::Action::While { body, .. } + | workshop_rs::wir::Action::ForGlobalVariable { body, .. } + | workshop_rs::wir::Action::ForPlayerVariable { body, .. } => { + collect_action_calls(program, body, calls); + } + _ => {} + } + } +} diff --git a/crates/opy-rs/support-matrix.json b/crates/opy-rs/support-matrix.json index 3e803fc..71cbb94 100644 --- a/crates/opy-rs/support-matrix.json +++ b/crates/opy-rs/support-matrix.json @@ -267,7 +267,7 @@ "fixtures:synthetic/receiver-calls", "upstream:src/data/opy/memberFunctions.ts" ], - "notes": "Issue #42 closes the evidenced receiver slice: member action/value catalogIds are lowered with the receiver as the canonical leading operand, and catalog-gap members remain explicit integration diagnostics. Full receiver member lists and content-specific domain breadth remain Workshop-owned and lowering-dependent." + "notes": "Issue #42 closes the evidenced receiver slice: member action/value catalogIds are lowered with the receiver as the canonical leading operand, and catalog-gap members remain explicit integration diagnostics. The OPY receiver/member source-language surface (names, aliases, receiver categories, and signatures) belongs to opy-rs; workshop-rs owns canonical Workshop target identities and semantics, content domains, localization, validation, and emission." }, { "id": "semantics/receiver-playervar", From 7eb81ed8c4e95a4fd255cc164b57cb5ca66e613e Mon Sep 17 00:00:00 2001 From: Teakowa Date: Fri, 28 Aug 2026 17:16:34 +0800 Subject: [PATCH 3/3] test(compiler): remove unreachable enum integration case Keep the source-layer unknown-enum-member diagnostic authoritative while retaining the reachable catalog-gap integration negative test. Refs #42 --- crates/opy-compiler/tests/issue_42_oracle.rs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/crates/opy-compiler/tests/issue_42_oracle.rs b/crates/opy-compiler/tests/issue_42_oracle.rs index ba043f4..c44e179 100644 --- a/crates/opy-compiler/tests/issue_42_oracle.rs +++ b/crates/opy-compiler/tests/issue_42_oracle.rs @@ -92,18 +92,6 @@ fn catalog_enum_members_lower_and_validate() { } } -#[test] -fn unknown_catalog_enum_member_is_a_source_attributed_gap() { - let source = "globalvar g\nrule \"r\":\n @Event global\n g = ChaseTimeReeval.NOPE\n"; - let hir = opy_rs::compile(source, "source.opy", Path::new(".")).expect("frontend resolves"); - let error = match Compiler::new().expect("compiler loads").compile_hir(&hir) { - Ok(_) => panic!("unknown catalog enum member must not be emitted"), - Err(error) => error, - }; - assert_eq!(error.diagnostic.code, "unsupported-integration-surface"); - assert_eq!(error.diagnostic.span.expect("source span").start.line, 4); -} - #[test] fn catalog_gap_member_is_explicitly_rejected() { let source =