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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

78 changes: 67 additions & 11 deletions crates/opy-compiler/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,6 @@ impl Compiler {
pub fn compile_hir(&self, hir: &hir::Program) -> Result<CompilationArtifact, IntegrationError> {
let mut lowering = Lowering::new(self, hir)?;
lowering.copy_files()?;
lowering.reject_unsupported_metadata()?;
lowering.lower_declarations()?;
lowering.lower_rules()?;

Expand Down Expand Up @@ -228,6 +227,72 @@ pub struct CompilationArtifact {
pub catalog_identity: CatalogIdentity,
}

fn convert_settings(settings: opy_rs::hir::Settings) -> workshop_rs::settings::Settings {
workshop_rs::settings::Settings {
span: settings.span.map(convert_settings_span),
children: settings
.children
.into_iter()
.map(convert_settings_node)
.collect(),
}
}

fn convert_settings_node(node: opy_rs::hir::SettingsNode) -> workshop_rs::settings::SettingsNode {
use opy_rs::hir::SettingsNode as SourceNode;
use workshop_rs::settings::{SettingsListElement, SettingsNode as TargetNode};

match node {
SourceNode::Group {
name,
children,
span,
} => TargetNode::Group {
name,
children: children.into_iter().map(convert_settings_node).collect(),
span: span.map(convert_settings_span),
},
SourceNode::Number { name, value, span } => TargetNode::Number {
name,
value,
span: span.map(convert_settings_span),
},
SourceNode::Bool { name, value, span } => TargetNode::Bool {
name,
value,
span: span.map(convert_settings_span),
},
SourceNode::String { name, value, span } => TargetNode::String {
name,
value,
span: span.map(convert_settings_span),
},
SourceNode::List {
name,
elements,
span,
} => TargetNode::List {
name,
elements: elements
.into_iter()
.map(|element| SettingsListElement {
value: element.value,
span: element.span.map(convert_settings_span),
})
.collect(),
span: span.map(convert_settings_span),
},
}
}

fn convert_settings_span(span: HirSpan) -> WorkshopSpan {
WorkshopSpan::new(
workshop_rs::source::FileId::from_index(span.file as usize),
WorkshopPosition::new(span.start.line, span.start.col),
WorkshopPosition::new(span.end.line, span.end.col),
)
}

struct Lowering<'a> {
compiler: &'a Compiler,
hir: &'a hir::Program,
Expand Down Expand Up @@ -268,6 +333,7 @@ impl<'a> Lowering<'a> {
}

fn copy_files(&mut self) -> Result<(), IntegrationError> {
self.wir.settings = self.hir.settings.clone().map(convert_settings);
for file in &self.hir.files {
if self.files.contains_key(&file.id) {
return Err(IntegrationError::new(
Expand All @@ -283,16 +349,6 @@ impl<'a> Lowering<'a> {
Ok(())
}

fn reject_unsupported_metadata(&self) -> Result<(), IntegrationError> {
if let Some(settings) = &self.hir.settings {
return Err(self.unsupported(
"custom-game settings lowering is outside #46",
settings.span,
));
}
Ok(())
}

fn lower_declarations(&mut self) -> Result<(), IntegrationError> {
let (implicit_globals, implicit_players) = implicit_default_variables(self.hir);
for declaration in &self.hir.declarations {
Expand Down
1 change: 1 addition & 0 deletions crates/opy-rs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ workspace = true
opy-macro-js = { path = "../opy-macro-js", version = "0.1.1" }
serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true
workshop-rs.workspace = true
64 changes: 43 additions & 21 deletions crates/opy-rs/src/lower.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ use crate::diag::{OpyError, OpyResult, Span};
use crate::manifest::{
Function, FunctionContext, FunctionKind, Manifest, Param, ParamDefault, ReceiverCategory,
};
use workshop_rs::catalog::Catalog;

/// The protocol envelope this frontend produces.
const PROTOCOL_NAME: &str = "wright/opy-hir";
Expand Down Expand Up @@ -69,6 +70,8 @@ struct Lowerer {
allow_dict_literal: bool,
/// The authoritative builtin semantic table (issue #109).
manifest: &'static Manifest,
/// The canonical Workshop catalog linked by the manifest.
catalog: Catalog,
errors: Vec<OpyError>,
}

Expand Down Expand Up @@ -96,6 +99,15 @@ pub fn lower_with_preprocessing(
));
}
};
let catalog = match Catalog::builtin() {
Ok(catalog) => catalog,
Err(error) => {
return Err(OpyError::new(
"catalog-error",
format!("cannot load the Workshop catalog: {error}"),
));
}
};
let mut lowerer = Lowerer {
globals: HashSet::new(),
players: HashSet::new(),
Expand All @@ -105,6 +117,7 @@ pub fn lower_with_preprocessing(
locals: Vec::new(),
allow_dict_literal: false,
manifest,
catalog,
errors: Vec::new(),
};
lowerer.collect_symbols(program);
Expand Down Expand Up @@ -1099,9 +1112,22 @@ impl Lowerer {
// Builtin Workshop enum: the domain name is a declared OPY
// signature identity (manifest `param.domain`); the member list
// is Workshop-owned catalog content, so the member access
// resolves as an opaque identity without member validation.
// Member-existence checks are lowering-dependent (issue #8).
// resolves as an opaque identity after validating the member
// against the canonical Workshop catalog.
if self.manifest.domain_identity(name) {
if let Some(domain) = self.catalog.enum_domain(name)
&& !domain
.members
.iter()
.any(|candidate| candidate.member == *member)
{
self.error_at(
"unknown-enum-member",
format!("enum '{name}' has no member '{member}'"),
span,
);
return HirExpr::Null { span: None };
}
return HirExpr::Enum {
value_type: name.clone(),
value: member.to_string(),
Expand Down Expand Up @@ -2165,14 +2191,14 @@ mod tests {
}

#[test]
fn unknown_chase_time_reeval_member_resolves_as_an_opaque_enum_identity() {
// Member spellings are Workshop catalog content: a member access on
// a declared domain identity resolves as an opaque Enum node without
// member validation (lowering-dependent, #8).
let value = lowered_value(
fn unknown_chase_time_reeval_member_is_rejected_by_the_catalog() {
let error = crate::compile(
"globalvar g\nrule \"r\":\n @Event global\n g = ChaseTimeReeval.NOPE\n",
);
assert_enum(&value, "ChaseTimeReeval", "NOPE");
"test.opy",
std::path::Path::new(""),
)
.expect_err("unknown catalog member must be rejected");
assert_eq!(error.code, "unknown-enum-member");
}

#[test]
Expand Down Expand Up @@ -2770,19 +2796,15 @@ mod tests {
}

#[test]
fn previously_rejected_enum_member_spellings_resolve_as_opaque_identities() {
// The KNOWN_ENUMS-era member allowlist is gone: Color.CYAN and
// DynamicEffect.SPARKLES are member spellings whose validity is
// Workshop-owned knowledge, so they resolve as opaque enum
// identities instead of failing (lowering-dependent, #8).
let value =
lowered_value("globalvar g\nrule \"r\":\n @Event global\n g = Color.CYAN\n");
assert_enum(&value, "Color", "CYAN");

let value = lowered_value(
fn unknown_catalog_enum_members_are_rejected() {
for source in [
"globalvar g\nrule \"r\":\n @Event global\n g = Color.CYAN\n",
"globalvar g\nrule \"r\":\n @Event global\n g = DynamicEffect.SPARKLES\n",
);
assert_enum(&value, "DynamicEffect", "SPARKLES");
] {
let error = crate::compile(source, "test.opy", std::path::Path::new(""))
.expect_err("unknown catalog member must be rejected");
assert_eq!(error.code, "unknown-enum-member");
}
}

#[test]
Expand Down