Skip to main content

nwnrs_nwscript/
session.rs

1use std::{error::Error, fmt};
2
3use crate::{
4    CodegenError, CompileArtifacts, CompileError, CompileOptions, DEFAULT_LANGSPEC_SCRIPT_NAME,
5    LangSpec, LangSpecError, OptimizationLevel, PreprocessError, Script, ScriptResolver,
6    SourceBundle, SourceError, SourceLoadOptions, compile_script, compile_script_with_source_map,
7    graphviz::render_script_graphviz, load_langspec, load_source_bundle, parse_source_bundle,
8};
9
10/// Configuration for one reusable NWScript compiler session.
11#[derive(#[automatically_derived]
impl ::core::fmt::Debug for CompilerSessionOptions {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        ::core::fmt::Formatter::debug_struct_field4_finish(f,
            "CompilerSessionOptions", "langspec_script_name",
            &self.langspec_script_name, "source_load", &self.source_load,
            "compile", &self.compile, "emit_debug", &&self.emit_debug)
    }
}Debug, #[automatically_derived]
impl ::core::clone::Clone for CompilerSessionOptions {
    #[inline]
    fn clone(&self) -> CompilerSessionOptions {
        CompilerSessionOptions {
            langspec_script_name: ::core::clone::Clone::clone(&self.langspec_script_name),
            source_load: ::core::clone::Clone::clone(&self.source_load),
            compile: ::core::clone::Clone::clone(&self.compile),
            emit_debug: ::core::clone::Clone::clone(&self.emit_debug),
        }
    }
}Clone, #[automatically_derived]
impl ::core::cmp::PartialEq for CompilerSessionOptions {
    #[inline]
    fn eq(&self, other: &CompilerSessionOptions) -> bool {
        self.emit_debug == other.emit_debug &&
                    self.langspec_script_name == other.langspec_script_name &&
                self.source_load == other.source_load &&
            self.compile == other.compile
    }
}PartialEq, #[automatically_derived]
impl ::core::cmp::Eq for CompilerSessionOptions {
    #[inline]
    #[doc(hidden)]
    #[coverage(off)]
    fn assert_fields_are_eq(&self) {
        let _: ::core::cmp::AssertParamIsEq<String>;
        let _: ::core::cmp::AssertParamIsEq<SourceLoadOptions>;
        let _: ::core::cmp::AssertParamIsEq<CompileOptions>;
        let _: ::core::cmp::AssertParamIsEq<bool>;
    }
}Eq)]
12pub struct CompilerSessionOptions {
13    /// Logical script name used to load the builtin language specification.
14    pub langspec_script_name: String,
15    /// Source loading configuration used for the langspec and all compilations.
16    pub source_load:          SourceLoadOptions,
17    /// Code generation options applied to each compile request.
18    pub compile:              CompileOptions,
19    /// Whether compilations should emit `NDB` debugger output when available.
20    pub emit_debug:           bool,
21}
22
23impl Default for CompilerSessionOptions {
24    fn default() -> Self {
25        Self {
26            langspec_script_name: DEFAULT_LANGSPEC_SCRIPT_NAME.to_string(),
27            source_load:          SourceLoadOptions::default(),
28            compile:              CompileOptions::default(),
29            emit_debug:           true,
30        }
31    }
32}
33
34/// Errors returned while using one reusable compiler session.
35#[derive(#[automatically_derived]
impl ::core::fmt::Debug for CompilerSessionError {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        match self {
            CompilerSessionError::LangSpec(__self_0) =>
                ::core::fmt::Formatter::debug_tuple_field1_finish(f,
                    "LangSpec", &__self_0),
            CompilerSessionError::Preprocess(__self_0) =>
                ::core::fmt::Formatter::debug_tuple_field1_finish(f,
                    "Preprocess", &__self_0),
            CompilerSessionError::Source(__self_0) =>
                ::core::fmt::Formatter::debug_tuple_field1_finish(f, "Source",
                    &__self_0),
            CompilerSessionError::Compile(__self_0) =>
                ::core::fmt::Formatter::debug_tuple_field1_finish(f,
                    "Compile", &__self_0),
        }
    }
}Debug)]
36pub enum CompilerSessionError {
37    /// Loading or parsing the builtin language specification failed.
38    LangSpec(LangSpecError),
39    /// Loading and preprocessing the requested source bundle failed.
40    Preprocess(PreprocessError),
41    /// Loading the requested source bundle failed.
42    Source(SourceError),
43    /// Parsing or code generation failed.
44    Compile(CompileError),
45}
46
47impl fmt::Display for CompilerSessionError {
48    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49        match self {
50            Self::LangSpec(error) => error.fmt(f),
51            Self::Preprocess(error) => error.fmt(f),
52            Self::Source(error) => error.fmt(f),
53            Self::Compile(error) => error.fmt(f),
54        }
55    }
56}
57
58impl Error for CompilerSessionError {}
59
60impl From<LangSpecError> for CompilerSessionError {
61    fn from(value: LangSpecError) -> Self {
62        Self::LangSpec(value)
63    }
64}
65
66impl From<SourceError> for CompilerSessionError {
67    fn from(value: SourceError) -> Self {
68        Self::Source(value)
69    }
70}
71
72impl From<PreprocessError> for CompilerSessionError {
73    fn from(value: PreprocessError) -> Self {
74        Self::Preprocess(value)
75    }
76}
77
78impl From<CompileError> for CompilerSessionError {
79    fn from(value: CompileError) -> Self {
80        Self::Compile(value)
81    }
82}
83
84/// One reusable pure-Rust compiler session backed by a script resolver.
85pub struct CompilerSession<'a> {
86    resolver:        &'a dyn ScriptResolver,
87    options:         CompilerSessionOptions,
88    cached_langspec: Option<LangSpec>,
89}
90
91#[derive(#[automatically_derived]
impl ::core::fmt::Debug for PreparedScript {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        ::core::fmt::Formatter::debug_struct_field3_finish(f,
            "PreparedScript", "langspec", &self.langspec, "bundle",
            &self.bundle, "script", &&self.script)
    }
}Debug, #[automatically_derived]
impl ::core::clone::Clone for PreparedScript {
    #[inline]
    fn clone(&self) -> PreparedScript {
        PreparedScript {
            langspec: ::core::clone::Clone::clone(&self.langspec),
            bundle: ::core::clone::Clone::clone(&self.bundle),
            script: ::core::clone::Clone::clone(&self.script),
        }
    }
}Clone)]
92pub(crate) struct PreparedScript {
93    pub(crate) langspec: LangSpec,
94    pub(crate) bundle:   SourceBundle,
95    pub(crate) script:   Script,
96}
97
98impl fmt::Debug for CompilerSession<'_> {
99    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
100        f.debug_struct("CompilerSession")
101            .field("options", &self.options)
102            .field("has_cached_langspec", &self.cached_langspec.is_some())
103            .finish()
104    }
105}
106
107impl<'a> CompilerSession<'a> {
108    /// Creates one compiler session with default options.
109    #[must_use]
110    pub fn new(resolver: &'a dyn ScriptResolver) -> Self {
111        Self::with_options(resolver, CompilerSessionOptions::default())
112    }
113
114    /// Creates one compiler session with explicit options.
115    #[must_use]
116    pub fn with_options(resolver: &'a dyn ScriptResolver, options: CompilerSessionOptions) -> Self {
117        Self {
118            resolver,
119            options,
120            cached_langspec: None,
121        }
122    }
123
124    /// Returns the current immutable session options.
125    #[must_use]
126    pub fn options(&self) -> &CompilerSessionOptions {
127        &self.options
128    }
129
130    /// Returns whether this session emits `NDB` debugger output.
131    #[must_use]
132    pub fn generate_debugger_output(&self) -> bool {
133        self.options.emit_debug
134    }
135
136    /// Toggles `NDB` debugger output without recreating the session.
137    pub fn set_generate_debugger_output(&mut self, state: bool) {
138        self.options.emit_debug = state;
139    }
140
141    /// Returns the current optimization level.
142    #[must_use]
143    pub fn optimization_level(&self) -> OptimizationLevel {
144        self.options.compile.optimization
145    }
146
147    /// Updates the optimization level without recreating the session.
148    pub fn set_optimization_level(&mut self, optimization: OptimizationLevel) {
149        self.options.compile.optimization = optimization;
150    }
151
152    /// Returns the current source-load options.
153    #[must_use]
154    pub fn source_load_options(&self) -> SourceLoadOptions {
155        self.options.source_load
156    }
157
158    /// Updates source-loading options and invalidates any cached langspec.
159    pub fn set_source_load_options(&mut self, options: SourceLoadOptions) {
160        self.options.source_load = options;
161        self.cached_langspec = None;
162    }
163
164    /// Returns the logical langspec script name.
165    #[must_use]
166    pub fn langspec_script_name(&self) -> &str {
167        &self.options.langspec_script_name
168    }
169
170    /// Updates the langspec script name and invalidates any cached langspec.
171    pub fn set_langspec_script_name(&mut self, script_name: impl Into<String>) {
172        self.options.langspec_script_name = script_name.into();
173        self.cached_langspec = None;
174    }
175
176    /// Compiles one logical script name through the configured resolver.
177    ///
178    /// # Errors
179    ///
180    /// Returns [`CompilerSessionError`] if source loading, langspec loading,
181    /// parsing, or code generation fails.
182    pub fn compile_script_name(
183        &mut self,
184        script_name: &str,
185    ) -> Result<CompileArtifacts, CompilerSessionError> {
186        let prepared = self.prepare_script_name(script_name)?;
187        self.compile_prepared(&prepared)
188            .map_err(CompilerSessionError::from)
189    }
190
191    /// Renders one logical script name to Graphviz DOT using the cached
192    /// langspec and loaded source bundle.
193    ///
194    /// # Errors
195    ///
196    /// Returns [`CompilerSessionError`] if source loading or parsing fails.
197    pub fn render_graphviz_for_script_name(
198        &mut self,
199        script_name: &str,
200    ) -> Result<String, CompilerSessionError> {
201        let prepared = self.prepare_script_name(script_name)?;
202        Ok(render_script_graphviz(
203            &prepared.script,
204            Some(&prepared.bundle.source_map),
205        ))
206    }
207
208    fn ensure_langspec_loaded(&mut self) -> Result<&LangSpec, CompilerSessionError> {
209        if self.cached_langspec.is_none() {
210            let langspec = load_langspec(
211                self.resolver,
212                &self.options.langspec_script_name,
213                self.options.source_load,
214            )?;
215            self.cached_langspec = Some(langspec);
216        }
217        self.cached_langspec.as_ref().ok_or_else(|| {
218            CompilerSessionError::Source(SourceError::resolver(
219                "failed to cache langspec after successful load",
220            ))
221        })
222    }
223
224    pub(crate) fn prepare_script_name(
225        &mut self,
226        script_name: &str,
227    ) -> Result<PreparedScript, CompilerSessionError> {
228        let langspec = self.ensure_langspec_loaded()?.clone();
229        let bundle = load_source_bundle(self.resolver, script_name, self.options.source_load)?;
230        let script = parse_source_bundle(&bundle, Some(&langspec)).map_err(|error| {
231            CompilerSessionError::Compile(CompileError::Codegen(CodegenError {
232                span:    None,
233                message: ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("failed to parse source bundle during compile: {0}",
                error))
    })format!("failed to parse source bundle during compile: {error}"),
234            }))
235        })?;
236        Ok(PreparedScript {
237            langspec,
238            bundle,
239            script,
240        })
241    }
242
243    pub(crate) fn compile_prepared(
244        &self,
245        prepared: &PreparedScript,
246    ) -> Result<CompileArtifacts, CompileError> {
247        if self.options.emit_debug {
248            compile_script_with_source_map(
249                &prepared.script,
250                &prepared.bundle.source_map,
251                prepared.bundle.root_id,
252                Some(&prepared.langspec),
253                self.options.compile,
254            )
255        } else {
256            compile_script(
257                &prepared.script,
258                Some(&prepared.langspec),
259                self.options.compile,
260            )
261        }
262    }
263}
264
265#[cfg(test)]
266mod tests {
267    use super::CompilerSession;
268    use crate::{InMemoryScriptResolver, OptimizationLevel};
269
270    #[test]
271    fn compiler_session_reuses_langspec_and_toggles_debug_output()
272    -> Result<(), Box<dyn std::error::Error>> {
273        let mut resolver = InMemoryScriptResolver::new();
274        resolver.insert_source("nwscript", "void PrintInteger(int n);");
275        resolver.insert_source("main", "void main() { PrintInteger(42); }");
276
277        let mut session = CompilerSession::new(&resolver);
278        let first = session.compile_script_name("main")?;
279        assert!(!first.ncs.is_empty());
280        assert!(first.ndb.is_some());
281
282        session.set_generate_debugger_output(false);
283        let second = session.compile_script_name("main")?;
284        assert!(!second.ncs.is_empty());
285        assert!(second.ndb.is_none());
286        Ok(())
287    }
288
289    #[test]
290    fn compiler_session_updates_optimization_without_recreation()
291    -> Result<(), Box<dyn std::error::Error>> {
292        let mut resolver = InMemoryScriptResolver::new();
293        resolver.insert_source("nwscript", "void PrintInteger(int n);");
294        resolver.insert_source("main", "void main() { PrintInteger(42); }");
295
296        let mut session = CompilerSession::new(&resolver);
297        session.set_optimization_level(OptimizationLevel::O1);
298        let artifacts = session.compile_script_name("main")?;
299        assert!(!artifacts.ncs.is_empty());
300        assert!(artifacts.ndb.is_none());
301        Ok(())
302    }
303}