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}