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
11 changes: 8 additions & 3 deletions compatibility/support-matrix.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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. 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",
Expand Down Expand Up @@ -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",
Expand Down
162 changes: 158 additions & 4 deletions crates/opy-compiler/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -1665,6 +1673,93 @@ impl<'a> Lowering<'a> {
}))
}

fn lower_receiver_action_call(
&mut self,
receiver: &Expr,
name: &str,
args: &[Expr],
span: Option<HirSpan>,
) -> Result<wir::ActionId, IntegrationError> {
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::<Result<Vec<_>, _>>()?,
);
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<wir::ValueId, IntegrationError> {
let span = expr.span().copied();
let value = match expr {
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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::<Result<Vec<_>, _>>()?,
);
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!(
Expand Down
Loading