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