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