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