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