opy_rs/lib.rs
1//! The standalone OverPy-compatible `.opy` implementation (opy-rs).
2//!
3//! Owns the OPY source-language surface of the `opy-rs` repository: a lexer,
4//! an indentation-aware CST/parser with structured diagnostics and recovery,
5//! token-level preprocessing (includes and `#!define` macros), semantic
6//! resolution, and lowering into the opy-rs-owned Opy HIR contract
7//! ([`hir::Program`]). Everything from source through the Opy HIR semantic
8//! model is Workshop-independent: source analysis never depends on `workshop-rs`,
9//! OverPy, or Node. The bounded source-to-Workshop compiler is exposed from
10//! this same crate behind the explicit [`Compiler`] API.
11//!
12//! Pipeline: [`lexer::lex`] → [`preprocess::preprocess`] →
13//! [`parser::parse`] → [`lower::lower`] → Opy HIR ([`hir`]).
14//!
15//! OverPy-compatible `__script__("…")` macros execute at compile time through
16//! the bounded embedded macro runtime: script macros expand
17//! during preprocessing with the reference's argument-injection ABI, and
18//! resource limits mirror the pinned reference constants
19//! (`macro_js::Limits::default()`). Script-macro expansion is
20//! compile-time behavior and is source-supported.
21//!
22//! `#!postCompileHook` is recognized, parsed, validated, and recorded only
23//! (see [`preprocess`] and [`CompileOutcome::post_compile_hook`]): the
24//! The source implementation never executes the hook. Real hook execution
25//! receives the final Workshop text produced by lowering and is
26//! lowering-dependent (workshop-rs emission, issue #8); source analysis never
27//! fabricates a Workshop payload.
28//!
29//! This crate owns the OverPy source-language implementation, its bounded
30//! compiler, Workshop→OPY reconstruction, and the isolated differential
31//! harness entry points.
32
33mod compiler;
34pub mod cst;
35pub mod diag;
36pub mod hir;
37pub mod lexer;
38pub mod lower;
39mod macro_js;
40pub mod manifest;
41pub mod parser;
42pub mod preprocess;
43pub mod settings;
44pub mod support;
45pub mod tooling;
46
47use std::path::Path;
48
49pub use compiler::reconstruct;
50pub use compiler::{
51 COMPILE_SCHEMA_VERSION, CompilationArtifact, CompileDiagnostic, CompileFailureClass,
52 CompileOutput, CompileReport, CompileResult, CompileStatus, Compiler, CompilerIdentity,
53 IntegrationDiagnostic, IntegrationError, LinkReport, ScriptDiagnostic, WORKSHOP_RS_VERSION,
54};
55use diag::Span;
56pub use diag::{OpyError, OpyResult};
57pub use lower::lower;
58pub use parser::parse;
59pub use preprocess::{preprocess, preprocess_with_overlay};
60
61#[cfg(test)]
62mod tests {
63 use super::compile;
64 use std::path::Path;
65
66 #[test]
67 fn unsupported_operator_aliases_fail_at_the_source_boundary() {
68 for expression in ["a // 2", "a //= 2", "a ^ 2", "a && 2", "a || 2", "a = !2"] {
69 let source = format!(
70 "globalvar a\nrule \"unsupported operator\":\n @Event global\n {expression}\n"
71 );
72 let error = compile(&source, "unsupported-operator.opy", Path::new("."))
73 .expect_err("unsupported operator alias unexpectedly compiled");
74 assert!(matches!(error.code.as_str(), "lex-error" | "parse-error"));
75 assert!(error.span.is_some(), "{expression}: missing source span");
76 }
77 }
78
79 #[test]
80 fn implicit_event_player_defaults_satisfy_hir_reference_validation() {
81 let hir = compile(
82 "rule \"implicit player\":\n @Event eachPlayer\n eventPlayer.A = 1\n",
83 "implicit-player.opy",
84 Path::new("."),
85 )
86 .expect("implicit event-player default must resolve");
87 hir.validate()
88 .expect("implicit event-player default must satisfy HIR invariants");
89 }
90}
91
92/// The producer identity for generated HIR.
93///
94/// The producer identity and the Opy HIR protocol envelope (`wright/opy-hir`
95/// v2) is emitted for the ordered switch-arm wire grammar; v1 consumers must
96/// reject it until they migrate to the v2 contract.
97pub const LANGUAGE_NAME: &str = "opy-rs";
98pub const LANGUAGE_VERSION: &str = env!("CARGO_PKG_VERSION");
99
100/// Compile one `.opy` source end-to-end into the Opy HIR contract:
101/// preprocess (includes/defines) → parse (CST) → lower (HIR).
102///
103/// `main_path` is the file's display path recorded in the HIR file registry;
104/// `root` is the include base. `compile` never requires Node or OverPy.
105pub fn compile(source: &str, main_path: &str, root: &Path) -> OpyResult<hir::Program> {
106 compile_with_overlay(source, main_path, root, &std::collections::BTreeMap::new())
107}
108
109/// Compile with open-document overlays: includes resolve to overlay text
110/// (keyed by the include string or the resolved canonical path) before the
111/// filesystem, so unsaved editor buffers participate in include resolution.
112pub fn compile_with_overlay(
113 source: &str,
114 main_path: &str,
115 root: &Path,
116 overlay: &std::collections::BTreeMap<String, String>,
117) -> OpyResult<hir::Program> {
118 let outcome = compile_with_overlay_outcome(source, main_path, root, overlay);
119 match outcome.hir {
120 Some(hir) => Ok(hir),
121 None => Err(outcome
122 .error
123 .expect("a failed compile outcome always carries an error")),
124 }
125}
126
127/// The outcome of a compile with overlays.
128///
129/// Unlike [`compile_with_overlay`], this retains the source file registry
130/// even when parsing or lowering fails, so language tooling can map span file
131/// ids to their actual source identities without building a diagnostics-only
132/// project model.
133pub struct CompileOutcome {
134 pub hir: Option<hir::Program>,
135 pub error: Option<OpyError>,
136 pub files: Vec<preprocess::FileRecord>,
137 /// The declared `#!postCompileHook` script, when the source declared one
138 /// and compilation succeeded.
139 ///
140 /// This is the declaration record, not an execution result: the OPY
141 /// implementation
142 /// recognizes, parses, validates, and records the directive, but never
143 /// executes the hook. Execution against the final Workshop text is
144 /// lowering-dependent (workshop-rs emission, issue #8); source analysis
145 /// never fabricates a Workshop payload.
146 pub post_compile_hook: Option<PostCompileHookRecord>,
147}
148
149/// The recorded declaration of a `#!postCompileHook` script.
150///
151/// The declared `#!postCompileHook` script; execution against the final
152/// Workshop text is lowering-dependent (workshop-rs emission, issue #8). The
153/// frontend never fabricates a Workshop payload.
154#[derive(Debug, Clone, PartialEq, Eq)]
155pub struct PostCompileHookRecord {
156 /// The script path as declared (root-relative).
157 pub script: String,
158 /// The resolved script source, retained for the backend hook ABI.
159 pub source: String,
160 /// The directive's source span, when known.
161 pub span: Option<Span>,
162}
163
164/// Compile with open-document overlays while retaining the source file registry
165/// on parse/lower failure.
166///
167/// This is the compile contract view of [`tooling::check_with_overlay`]: the
168/// two share one pipeline, so `check` and `compile` never disagree about
169/// whether a project is clean.
170pub fn compile_with_overlay_outcome(
171 source: &str,
172 main_path: &str,
173 root: &Path,
174 overlay: &std::collections::BTreeMap<String, String>,
175) -> CompileOutcome {
176 let outcome = tooling::check_with_overlay(source, main_path, root, overlay);
177 // Every failed check carries at least one diagnostic, so a None model
178 // always yields an error (the compile outcome invariant).
179 let error = outcome.diagnostics.first().map(|diagnostic| OpyError {
180 code: diagnostic.code.clone(),
181 message: diagnostic.message.clone(),
182 span: diagnostic
183 .span
184 .as_ref()
185 .map(tooling::SourceLocation::to_span),
186 });
187 // The directive was parsed, validated, and recorded by preprocessing; the
188 // source implementation never executes the hook (real hook execution receives the
189 // final Workshop text and is lowering-dependent, issue #8 — see
190 // `PostCompileHookRecord`).
191 let post_compile_hook = outcome.post_compile_hook.map(|hook| PostCompileHookRecord {
192 script: hook.path,
193 source: hook.source,
194 span: Some(hook.span),
195 });
196 CompileOutcome {
197 hir: outcome.model.map(|model| model.hir),
198 error,
199 files: outcome.files,
200 post_compile_hook,
201 }
202}