Skip to main content

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}