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}