Skip to main content

vm/compiler/
mod.rs

1use std::collections::HashMap;
2use std::fmt;
3use std::path::{Path, PathBuf};
4
5use crate::Program;
6use crate::assembler::AssemblerError;
7#[cfg(feature = "runtime")]
8use crate::vm::Vm;
9
10mod codegen;
11pub mod diagnostics;
12mod format;
13mod frontends;
14pub mod ir;
15mod lifetime;
16mod linker;
17mod parser;
18mod pipeline;
19mod source_loader;
20pub mod source_map;
21mod typing;
22
23use self::source_map::{SourceMap, Span};
24
25pub use self::codegen::Compiler;
26pub use self::format::{
27    FormatError, format_source, format_source_with_flavor, format_source_with_flavor_and_options,
28};
29pub use self::frontends::parse_source_with_dialect;
30pub use self::ir::{
31    AssignmentKind, ClosureExpr, Expr, FrontendIr, FunctionDecl, FunctionImpl, FunctionParam,
32    LocalIrBuilder, LocalSlot, MatchPattern, MatchTypePattern, Stmt, StructDecl, TypeSchema,
33};
34pub use self::parser::ParserDialect;
35pub use self::pipeline::{
36    InferredLocalTypeHint, UnknownInferredLocal, collect_inferred_local_type_hints,
37    collect_inferred_local_type_hints_at_path_with_options,
38    collect_inferred_local_type_hints_with_options, compile_source,
39    compile_source_at_path_with_flavor_and_options, compile_source_file,
40    compile_source_file_with_options, compile_source_for_repl, compile_source_for_repl_with_locals,
41    compile_source_with_flavor, compile_source_with_flavor_and_options,
42    lint_trailing_function_return_semicolons, lint_unknown_inferred_local_types,
43    lint_unknown_inferred_local_types_at_path_with_options,
44    lint_unknown_inferred_local_types_with_options, lint_unknown_type_annotations,
45};
46pub use self::source_loader::{FrontendImportSyntax, ImportClause, ModuleImport, NamedImport};
47
48#[derive(Debug)]
49pub enum CompileError {
50    Assembler(AssemblerError),
51    CallArityOverflow,
52    ClosureUsedAsValue,
53    CallableUsedAsValue,
54    NonCallableLocal(LocalSlot),
55    LocalSlotOverflow(LocalSlot),
56    CallableArityMismatch {
57        expected: usize,
58        got: usize,
59    },
60    BreakOutsideLoop,
61    ContinueOutsideLoop,
62    InlineFunctionRecursion(String),
63    IfElseBranchTypeMismatch {
64        line: Option<u32>,
65        source_name: Option<String>,
66        detail: String,
67    },
68    CallableArgumentTypeMismatch {
69        line: Option<u32>,
70        source_name: Option<String>,
71        detail: String,
72    },
73    BinaryOperandTypeMismatch {
74        line: Option<u32>,
75        source_name: Option<String>,
76        detail: String,
77    },
78    InvalidFieldAccess {
79        line: Option<u32>,
80        source_name: Option<String>,
81        detail: String,
82    },
83    FunctionParameterTypeConflict {
84        line: Option<u32>,
85        source_name: Option<String>,
86        detail: String,
87    },
88    StrictTypingRequired {
89        line: Option<u32>,
90        source_name: Option<String>,
91        detail: String,
92    },
93}
94
95impl CompileError {
96    pub fn line(&self) -> Option<usize> {
97        match self {
98            CompileError::IfElseBranchTypeMismatch { line, .. } => {
99                line.and_then(|value| usize::try_from(value).ok())
100            }
101            CompileError::CallableArgumentTypeMismatch { line, .. } => {
102                line.and_then(|value| usize::try_from(value).ok())
103            }
104            CompileError::BinaryOperandTypeMismatch { line, .. } => {
105                line.and_then(|value| usize::try_from(value).ok())
106            }
107            CompileError::InvalidFieldAccess { line, .. } => {
108                line.and_then(|value| usize::try_from(value).ok())
109            }
110            CompileError::FunctionParameterTypeConflict { line, .. } => {
111                line.and_then(|value| usize::try_from(value).ok())
112            }
113            CompileError::StrictTypingRequired { line, .. } => {
114                line.and_then(|value| usize::try_from(value).ok())
115            }
116            _ => None,
117        }
118    }
119
120    pub fn source_name(&self) -> Option<&str> {
121        match self {
122            CompileError::IfElseBranchTypeMismatch { source_name, .. }
123            | CompileError::CallableArgumentTypeMismatch { source_name, .. }
124            | CompileError::BinaryOperandTypeMismatch { source_name, .. }
125            | CompileError::InvalidFieldAccess { source_name, .. }
126            | CompileError::FunctionParameterTypeConflict { source_name, .. }
127            | CompileError::StrictTypingRequired { source_name, .. } => source_name.as_deref(),
128            _ => None,
129        }
130    }
131
132    pub fn diagnostic_message(&self) -> String {
133        match self {
134            CompileError::Assembler(err) => err.to_string(),
135            CompileError::CallArityOverflow => {
136                "call arity exceeds the supported bytecode encoding".to_string()
137            }
138            CompileError::ClosureUsedAsValue => {
139                "closures cannot be used as plain values".to_string()
140            }
141            CompileError::CallableUsedAsValue => {
142                "callables cannot be used as plain values".to_string()
143            }
144            CompileError::NonCallableLocal(slot) => format!("local slot {slot} is not callable"),
145            CompileError::LocalSlotOverflow(slot) => {
146                format!("local slot {slot} exceeds the supported bytecode encoding")
147            }
148            CompileError::CallableArityMismatch { expected, got } => {
149                format!("callable arity mismatch: expected {expected}, got {got}")
150            }
151            CompileError::BreakOutsideLoop => "break used outside of a loop".to_string(),
152            CompileError::ContinueOutsideLoop => "continue used outside of a loop".to_string(),
153            CompileError::InlineFunctionRecursion(name) => {
154                format!("inline function recursion detected in '{name}'")
155            }
156            CompileError::IfElseBranchTypeMismatch { detail, .. } => detail.clone(),
157            CompileError::CallableArgumentTypeMismatch { detail, .. } => detail.clone(),
158            CompileError::BinaryOperandTypeMismatch { detail, .. } => detail.clone(),
159            CompileError::InvalidFieldAccess { detail, .. } => detail.clone(),
160            CompileError::FunctionParameterTypeConflict { detail, .. } => detail.clone(),
161            CompileError::StrictTypingRequired { detail, .. } => detail.clone(),
162        }
163    }
164}
165
166impl fmt::Display for CompileError {
167    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
168        write!(f, "{}", self.diagnostic_message())
169    }
170}
171
172impl std::error::Error for CompileError {}
173
174#[derive(Debug, Clone, PartialEq, Eq)]
175pub struct ParseError {
176    pub line: usize,
177    pub message: String,
178    pub span: Option<Span>,
179    pub code: Option<String>,
180}
181
182impl ParseError {
183    pub fn new(message: impl Into<String>) -> Self {
184        Self {
185            line: 1,
186            message: message.into(),
187            span: None,
188            code: None,
189        }
190    }
191
192    pub fn at_line(line: usize, message: impl Into<String>) -> Self {
193        Self {
194            line,
195            message: message.into(),
196            span: None,
197            code: None,
198        }
199    }
200
201    pub fn at_span(span: Span, message: impl Into<String>) -> Self {
202        Self {
203            line: 1,
204            message: message.into(),
205            span: Some(span),
206            code: None,
207        }
208    }
209
210    pub fn with_code(mut self, code: impl Into<String>) -> Self {
211        self.code = Some(code.into());
212        self
213    }
214
215    pub fn with_line_span_from_source(mut self, source_map: &SourceMap, source_id: u32) -> Self {
216        if self.span.is_some() {
217            return self;
218        }
219        if let Some(span) = source_map.line_span(source_id, self.line) {
220            self.span = Some(span);
221        }
222        self
223    }
224}
225
226impl fmt::Display for ParseError {
227    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
228        if let Some(span) = self.span {
229            write!(
230                f,
231                "{} (source {} [{}..{}])",
232                self.message, span.source_id, span.lo, span.hi
233            )
234        } else {
235            write!(f, "line {}: {}", self.line, self.message)
236        }
237    }
238}
239
240impl std::error::Error for ParseError {}
241
242#[derive(Debug)]
243pub enum SourceError {
244    Parse(ParseError),
245    Compile(CompileError),
246}
247
248impl fmt::Display for SourceError {
249    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
250        match self {
251            SourceError::Parse(err) => write!(f, "{err}"),
252            SourceError::Compile(err) => write!(f, "compile error: {err}"),
253        }
254    }
255}
256
257impl std::error::Error for SourceError {}
258
259#[derive(Debug)]
260pub enum SourcePathError {
261    Io(std::io::Error),
262    MissingExtension,
263    UnsupportedExtension(String),
264    MissingFrontendPlugin(SourceFlavor),
265    ImportCycle(PathBuf),
266    NonRustScriptModule(PathBuf),
267    ImportWithoutParent(PathBuf),
268    InvalidImportSyntax {
269        path: PathBuf,
270        line: usize,
271        message: String,
272    },
273    Source(SourceError),
274}
275
276impl fmt::Display for SourcePathError {
277    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
278        match self {
279            SourcePathError::Io(err) => write!(f, "{err}"),
280            SourcePathError::MissingExtension => write!(f, "source file must have an extension"),
281            SourcePathError::UnsupportedExtension(ext) => write!(
282                f,
283                "unsupported source extension '.{ext}', expected .rss, .js, or .lua"
284            ),
285            SourcePathError::MissingFrontendPlugin(flavor) => {
286                write!(f, "no frontend plugin registered for {flavor:?} source")
287            }
288            SourcePathError::ImportCycle(path) => {
289                write!(f, "import cycle detected at '{}'", path.display())
290            }
291            SourcePathError::NonRustScriptModule(path) => {
292                write!(f, "module '{}' must use .rss extension", path.display())
293            }
294            SourcePathError::ImportWithoutParent(path) => write!(
295                f,
296                "cannot resolve import from '{}': missing parent directory",
297                path.display()
298            ),
299            SourcePathError::InvalidImportSyntax {
300                path,
301                line,
302                message,
303            } => write!(
304                f,
305                "invalid import syntax in '{}' at line {}: {}",
306                path.display(),
307                line,
308                message
309            ),
310            SourcePathError::Source(err) => write!(f, "{err}"),
311        }
312    }
313}
314
315impl std::error::Error for SourcePathError {}
316
317impl From<std::io::Error> for SourcePathError {
318    fn from(value: std::io::Error) -> Self {
319        SourcePathError::Io(value)
320    }
321}
322
323impl From<SourceError> for SourcePathError {
324    fn from(value: SourceError) -> Self {
325        SourcePathError::Source(value)
326    }
327}
328
329#[derive(Copy, Clone, Debug, PartialEq, Eq)]
330pub enum SourceFlavor {
331    RustScript,
332    JavaScript,
333    Lua,
334}
335
336pub trait SourcePlugin: Sync {
337    fn flavor(&self) -> SourceFlavor;
338
339    fn extensions(&self) -> &'static [&'static str];
340
341    fn import_syntax(&self) -> FrontendImportSyntax;
342
343    fn parse_source(&self, source: &str) -> Result<FrontendIr, ParseError>;
344
345    fn parser_dialect(&self) -> Option<&'static dyn ParserDialect> {
346        None
347    }
348
349    fn parse_module_imports(
350        &self,
351        _source: &str,
352        _path: &Path,
353    ) -> Result<Vec<ModuleImport>, SourcePathError> {
354        Ok(Vec::new())
355    }
356
357    fn strip_import_directives(&self, source: &str) -> String {
358        source.to_string()
359    }
360}
361
362#[derive(Clone, Copy, Debug, Default)]
363pub struct SharedParserOptions {
364    pub source_id: u32,
365    pub allow_implicit_externs: bool,
366    pub allow_implicit_semicolons: bool,
367    pub enforce_mutable_bindings: bool,
368}
369
370#[derive(Clone, Copy, Debug, PartialEq, Eq)]
371pub(crate) enum TypingMode {
372    DynamicHints,
373    StrictRustScript,
374}
375
376impl TypingMode {
377    pub(crate) fn for_flavor(flavor: SourceFlavor) -> Self {
378        match flavor {
379            SourceFlavor::RustScript => Self::StrictRustScript,
380            SourceFlavor::JavaScript | SourceFlavor::Lua => Self::DynamicHints,
381        }
382    }
383
384    pub(crate) fn is_strict(self) -> bool {
385        matches!(self, Self::StrictRustScript)
386    }
387}
388
389impl SourceFlavor {
390    pub fn from_extension(ext: &str) -> Option<Self> {
391        match ext.to_ascii_lowercase().as_str() {
392            "rss" => Some(Self::RustScript),
393            "js" => Some(Self::JavaScript),
394            "lua" => Some(Self::Lua),
395            _ => None,
396        }
397    }
398
399    pub fn from_path(path: &Path) -> Result<Self, SourcePathError> {
400        let ext = path
401            .extension()
402            .and_then(|value| value.to_str())
403            .ok_or(SourcePathError::MissingExtension)?;
404        SourceFlavor::from_extension(ext)
405            .ok_or_else(|| SourcePathError::UnsupportedExtension(ext.to_string()))
406    }
407
408    pub(crate) fn from_path_with_options(
409        path: &Path,
410        options: &CompileSourceFileOptions,
411    ) -> Result<Self, SourcePathError> {
412        let ext = path
413            .extension()
414            .and_then(|value| value.to_str())
415            .ok_or(SourcePathError::MissingExtension)?;
416        if let Some(plugin) = options.source_plugin_for_extension(ext) {
417            return Ok(plugin.flavor());
418        }
419        SourceFlavor::from_extension(ext)
420            .ok_or_else(|| SourcePathError::UnsupportedExtension(ext.to_string()))
421    }
422}
423
424#[derive(Clone, Debug, PartialEq, Eq)]
425pub struct ReplLocalBinding {
426    pub name: String,
427    pub mutable: bool,
428    pub schema: Option<TypeSchema>,
429    pub optional: bool,
430}
431
432pub struct CompiledProgram {
433    pub program: Program,
434    pub locals: usize,
435    pub functions: Vec<FunctionDecl>,
436}
437
438impl CompiledProgram {
439    #[cfg(feature = "runtime")]
440    pub fn into_vm(self) -> Vm {
441        Vm::new(self.program)
442    }
443}
444
445pub struct CompiledReplProgram {
446    pub compiled: CompiledProgram,
447    pub bindings: Vec<ReplLocalBinding>,
448}
449
450#[derive(Clone, Default)]
451pub struct CompileSourceFileOptions {
452    module_path_overrides: HashMap<String, PathBuf>,
453    module_source_overrides: HashMap<String, String>,
454    source_plugins: Vec<&'static dyn SourcePlugin>,
455}
456
457impl fmt::Debug for CompileSourceFileOptions {
458    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
459        f.debug_struct("CompileSourceFileOptions")
460            .field("module_path_overrides", &self.module_path_overrides)
461            .field("module_source_overrides", &self.module_source_overrides)
462            .field("source_plugin_count", &self.source_plugins.len())
463            .finish()
464    }
465}
466
467impl CompileSourceFileOptions {
468    pub fn new() -> Self {
469        Self::default()
470    }
471
472    pub fn with_module_override_path(
473        mut self,
474        import_spec: impl Into<String>,
475        module_path: impl Into<PathBuf>,
476    ) -> Self {
477        self.set_module_override_path(import_spec, module_path);
478        self
479    }
480
481    pub fn set_module_override_path(
482        &mut self,
483        import_spec: impl Into<String>,
484        module_path: impl Into<PathBuf>,
485    ) {
486        let key = normalize_import_spec(import_spec.into());
487        self.module_path_overrides.insert(key, module_path.into());
488    }
489
490    pub fn with_module_override_source(
491        mut self,
492        import_spec: impl Into<String>,
493        module_source: impl Into<String>,
494    ) -> Self {
495        self.set_module_override_source(import_spec, module_source);
496        self
497    }
498
499    pub fn set_module_override_source(
500        &mut self,
501        import_spec: impl Into<String>,
502        module_source: impl Into<String>,
503    ) {
504        let key = normalize_import_spec(import_spec.into());
505        self.module_source_overrides
506            .insert(key, module_source.into());
507    }
508
509    pub fn with_source_plugin(mut self, plugin: &'static dyn SourcePlugin) -> Self {
510        self.add_source_plugin(plugin);
511        self
512    }
513
514    pub fn add_source_plugin(&mut self, plugin: &'static dyn SourcePlugin) {
515        self.source_plugins.push(plugin);
516    }
517
518    pub fn module_override_path(&self, import_spec: &str) -> Option<&Path> {
519        let key = normalize_import_spec(import_spec.to_string());
520        self.module_path_overrides.get(&key).map(PathBuf::as_path)
521    }
522
523    pub fn module_override_source(&self, import_spec: &str) -> Option<&str> {
524        let key = normalize_import_spec(import_spec.to_string());
525        self.module_source_overrides.get(&key).map(String::as_str)
526    }
527
528    pub(crate) fn has_module_overrides(&self) -> bool {
529        !self.module_path_overrides.is_empty() || !self.module_source_overrides.is_empty()
530    }
531
532    pub(crate) fn has_source_plugins(&self) -> bool {
533        !self.source_plugins.is_empty()
534    }
535
536    pub(crate) fn source_plugin_for_flavor(
537        &self,
538        flavor: SourceFlavor,
539    ) -> Option<&'static dyn SourcePlugin> {
540        self.source_plugins
541            .iter()
542            .copied()
543            .find(|plugin| plugin.flavor() == flavor)
544    }
545
546    pub(crate) fn source_plugin_for_extension(
547        &self,
548        ext: &str,
549    ) -> Option<&'static dyn SourcePlugin> {
550        self.source_plugins.iter().copied().find(|plugin| {
551            plugin
552                .extensions()
553                .iter()
554                .any(|candidate| candidate.eq_ignore_ascii_case(ext))
555        })
556    }
557}
558
559const STDLIB_PRINT_NAME: &str = "print";
560const STDLIB_PRINT_ARITY: u8 = 1;
561
562fn normalize_import_spec(spec: String) -> String {
563    normalize_import_key(spec.trim())
564}
565
566fn normalize_import_key(spec: &str) -> String {
567    let normalized = spec.replace('\\', "/");
568    let (prefix, remainder) = split_windows_prefix(&normalized);
569    let absolute = remainder.starts_with('/');
570    let mut segments = Vec::<&str>::new();
571
572    for segment in remainder.split('/') {
573        if segment.is_empty() || segment == "." {
574            continue;
575        }
576        if segment == ".." {
577            match segments.last().copied() {
578                Some(existing) if existing != ".." => {
579                    segments.pop();
580                }
581                _ if !absolute => segments.push(".."),
582                _ => {}
583            }
584            continue;
585        }
586        segments.push(segment);
587    }
588
589    let mut out = String::new();
590    out.push_str(prefix);
591    if absolute {
592        out.push('/');
593    }
594    out.push_str(&segments.join("/"));
595    out
596}
597
598fn split_windows_prefix(input: &str) -> (&str, &str) {
599    let bytes = input.as_bytes();
600    if bytes.len() >= 2 && bytes[0].is_ascii_alphabetic() && bytes[1] == b':' {
601        (&input[..2], &input[2..])
602    } else {
603        ("", input)
604    }
605}