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}