use std::{error::Error, fmt};
use crate::{
CodegenError, CompileArtifacts, CompileError, CompileOptions, DEFAULT_LANGSPEC_SCRIPT_NAME,
LangSpec, LangSpecError, OptimizationLevel, PreprocessError, Script, ScriptResolver,
SourceBundle, SourceError, SourceLoadOptions, compile_script, compile_script_with_source_map,
graphviz::render_script_graphviz, load_langspec, load_source_bundle, parse_source_bundle,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CompilerSessionOptions {
pub langspec_script_name: String,
pub source_load: SourceLoadOptions,
pub compile: CompileOptions,
pub emit_debug: bool,
}
impl Default for CompilerSessionOptions {
fn default() -> Self {
Self {
langspec_script_name: DEFAULT_LANGSPEC_SCRIPT_NAME.to_string(),
source_load: SourceLoadOptions::default(),
compile: CompileOptions::default(),
emit_debug: true,
}
}
}
#[derive(Debug)]
pub enum CompilerSessionError {
LangSpec(LangSpecError),
Preprocess(PreprocessError),
Source(SourceError),
Compile(CompileError),
}
impl fmt::Display for CompilerSessionError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::LangSpec(error) => error.fmt(f),
Self::Preprocess(error) => error.fmt(f),
Self::Source(error) => error.fmt(f),
Self::Compile(error) => error.fmt(f),
}
}
}
impl Error for CompilerSessionError {}
impl From<LangSpecError> for CompilerSessionError {
fn from(value: LangSpecError) -> Self {
Self::LangSpec(value)
}
}
impl From<SourceError> for CompilerSessionError {
fn from(value: SourceError) -> Self {
Self::Source(value)
}
}
impl From<PreprocessError> for CompilerSessionError {
fn from(value: PreprocessError) -> Self {
Self::Preprocess(value)
}
}
impl From<CompileError> for CompilerSessionError {
fn from(value: CompileError) -> Self {
Self::Compile(value)
}
}
pub struct CompilerSession<'a> {
resolver: &'a dyn ScriptResolver,
options: CompilerSessionOptions,
cached_langspec: Option<LangSpec>,
}
#[derive(Debug, Clone)]
pub(crate) struct PreparedScript {
pub(crate) langspec: LangSpec,
pub(crate) bundle: SourceBundle,
pub(crate) script: Script,
}
impl fmt::Debug for CompilerSession<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("CompilerSession")
.field("options", &self.options)
.field("has_cached_langspec", &self.cached_langspec.is_some())
.finish()
}
}
impl<'a> CompilerSession<'a> {
#[must_use]
pub fn new(resolver: &'a dyn ScriptResolver) -> Self {
Self::with_options(resolver, CompilerSessionOptions::default())
}
#[must_use]
pub fn with_options(resolver: &'a dyn ScriptResolver, options: CompilerSessionOptions) -> Self {
Self {
resolver,
options,
cached_langspec: None,
}
}
#[must_use]
pub fn options(&self) -> &CompilerSessionOptions {
&self.options
}
#[must_use]
pub fn generate_debugger_output(&self) -> bool {
self.options.emit_debug
}
pub fn set_generate_debugger_output(&mut self, state: bool) {
self.options.emit_debug = state;
}
#[must_use]
pub fn optimization_level(&self) -> OptimizationLevel {
self.options.compile.optimization
}
pub fn set_optimization_level(&mut self, optimization: OptimizationLevel) {
self.options.compile.optimization = optimization;
}
#[must_use]
pub fn source_load_options(&self) -> SourceLoadOptions {
self.options.source_load
}
pub fn set_source_load_options(&mut self, options: SourceLoadOptions) {
self.options.source_load = options;
self.cached_langspec = None;
}
#[must_use]
pub fn langspec_script_name(&self) -> &str {
&self.options.langspec_script_name
}
pub fn set_langspec_script_name(&mut self, script_name: impl Into<String>) {
self.options.langspec_script_name = script_name.into();
self.cached_langspec = None;
}
pub fn compile_script_name(
&mut self,
script_name: &str,
) -> Result<CompileArtifacts, CompilerSessionError> {
let prepared = self.prepare_script_name(script_name)?;
self.compile_prepared(&prepared)
.map_err(CompilerSessionError::from)
}
pub fn render_graphviz_for_script_name(
&mut self,
script_name: &str,
) -> Result<String, CompilerSessionError> {
let prepared = self.prepare_script_name(script_name)?;
Ok(render_script_graphviz(
&prepared.script,
Some(&prepared.bundle.source_map),
))
}
fn ensure_langspec_loaded(&mut self) -> Result<&LangSpec, CompilerSessionError> {
if self.cached_langspec.is_none() {
let langspec = load_langspec(
self.resolver,
&self.options.langspec_script_name,
self.options.source_load,
)?;
self.cached_langspec = Some(langspec);
}
self.cached_langspec.as_ref().ok_or_else(|| {
CompilerSessionError::Source(SourceError::resolver(
"failed to cache langspec after successful load",
))
})
}
pub(crate) fn prepare_script_name(
&mut self,
script_name: &str,
) -> Result<PreparedScript, CompilerSessionError> {
let langspec = self.ensure_langspec_loaded()?.clone();
let bundle = load_source_bundle(self.resolver, script_name, self.options.source_load)?;
let script = parse_source_bundle(&bundle, Some(&langspec)).map_err(|error| {
CompilerSessionError::Compile(CompileError::Codegen(CodegenError {
span: None,
message: format!("failed to parse source bundle during compile: {error}"),
}))
})?;
Ok(PreparedScript {
langspec,
bundle,
script,
})
}
pub(crate) fn compile_prepared(
&self,
prepared: &PreparedScript,
) -> Result<CompileArtifacts, CompileError> {
if self.options.emit_debug {
compile_script_with_source_map(
&prepared.script,
&prepared.bundle.source_map,
prepared.bundle.root_id,
Some(&prepared.langspec),
self.options.compile,
)
} else {
compile_script(
&prepared.script,
Some(&prepared.langspec),
self.options.compile,
)
}
}
}
#[cfg(test)]
mod tests {
use super::CompilerSession;
use crate::{InMemoryScriptResolver, OptimizationLevel};
#[test]
fn compiler_session_reuses_langspec_and_toggles_debug_output()
-> Result<(), Box<dyn std::error::Error>> {
let mut resolver = InMemoryScriptResolver::new();
resolver.insert_source("nwscript", "void PrintInteger(int n);");
resolver.insert_source("main", "void main() { PrintInteger(42); }");
let mut session = CompilerSession::new(&resolver);
let first = session.compile_script_name("main")?;
assert!(!first.ncs.is_empty());
assert!(first.ndb.is_some());
session.set_generate_debugger_output(false);
let second = session.compile_script_name("main")?;
assert!(!second.ncs.is_empty());
assert!(second.ndb.is_none());
Ok(())
}
#[test]
fn compiler_session_updates_optimization_without_recreation()
-> Result<(), Box<dyn std::error::Error>> {
let mut resolver = InMemoryScriptResolver::new();
resolver.insert_source("nwscript", "void PrintInteger(int n);");
resolver.insert_source("main", "void main() { PrintInteger(42); }");
let mut session = CompilerSession::new(&resolver);
session.set_optimization_level(OptimizationLevel::O1);
let artifacts = session.compile_script_name("main")?;
assert!(!artifacts.ncs.is_empty());
assert!(artifacts.ndb.is_none());
Ok(())
}
}