Skip to main content

vm/compiler/
pipeline.rs

1use std::collections::HashMap;
2use std::path::{Path, PathBuf};
3
4use crate::HostImport;
5
6use super::ReplLocalState;
7use super::codegen::Compiler;
8use super::frontends;
9use super::ir::{Expr, FrontendIr, FunctionDecl, FunctionImpl, LocalSlot, Stmt, TypeSchema};
10use super::linker::merge_units;
11use super::source_loader::load_units_for_source_file;
12use super::source_map::SourceMap;
13use super::{
14    CompileError, CompileSourceFileOptions, CompiledProgram, CompiledReplProgram, ParseError,
15    ReplLocalBinding, SourceError, SourceFlavor, SourcePathError, TypingMode, lifetime, parser,
16    typing,
17};
18
19#[derive(Clone, Copy, Debug, Default)]
20pub(super) struct LocalDebugRange {
21    pub(super) declared_line: Option<u32>,
22    pub(super) last_line: Option<u32>,
23}
24
25#[derive(Clone, Debug, PartialEq, Eq)]
26pub struct UnknownInferredLocal {
27    pub name: String,
28    pub line: usize,
29    pub span: Option<crate::compiler::source_map::Span>,
30}
31
32#[derive(Clone, Debug, PartialEq, Eq)]
33pub struct InferredLocalTypeHint {
34    pub name: String,
35    pub inferred_type: String,
36    pub declared_line: Option<u32>,
37    pub last_line: Option<u32>,
38}
39
40#[derive(Clone, Copy, Debug)]
41struct CompileBehavior {
42    clear_dead_locals: bool,
43}
44
45impl CompileBehavior {
46    const DEFAULT: Self = Self {
47        clear_dead_locals: true,
48    };
49    const REPL: Self = Self {
50        clear_dead_locals: false,
51    };
52}
53
54fn collect_named_local_debug_ranges(parsed: &FrontendIr) -> HashMap<String, LocalDebugRange> {
55    let slot_ranges = collect_local_debug_ranges(&parsed.stmts, &parsed.function_impls);
56    let mut named_ranges = HashMap::<String, LocalDebugRange>::new();
57    for (name, slot) in &parsed.local_bindings {
58        let Some(range) = slot_ranges.get(slot).copied() else {
59            continue;
60        };
61        let entry = named_ranges.entry(name.clone()).or_default();
62        entry.declared_line = merge_min_debug_line(entry.declared_line, range.declared_line);
63        entry.last_line = merge_max_debug_line(entry.last_line, range.last_line);
64    }
65    named_ranges
66}
67
68fn collect_local_debug_ranges(
69    stmts: &[Stmt],
70    function_impls: &HashMap<u16, FunctionImpl>,
71) -> HashMap<LocalSlot, LocalDebugRange> {
72    let mut ranges = HashMap::<LocalSlot, LocalDebugRange>::new();
73    for stmt in stmts {
74        record_stmt_local_debug_ranges(stmt, &mut ranges);
75    }
76    for function_impl in function_impls.values() {
77        for stmt in &function_impl.body_stmts {
78            record_stmt_local_debug_ranges(stmt, &mut ranges);
79        }
80        let fallback_line = function_impl
81            .body_stmts
82            .last()
83            .map(stmt_source_line)
84            .unwrap_or(1);
85        let body_expr_line = if function_impl.body_expr_line > 0 {
86            function_impl.body_expr_line
87        } else {
88            fallback_line
89        };
90        record_expr_local_debug_ranges(&function_impl.body_expr, body_expr_line, &mut ranges);
91    }
92    ranges
93}
94
95fn record_stmt_local_debug_ranges(stmt: &Stmt, ranges: &mut HashMap<LocalSlot, LocalDebugRange>) {
96    match stmt {
97        Stmt::Noop { .. } | Stmt::FuncDecl { .. } | Stmt::Break { .. } | Stmt::Continue { .. } => {}
98        Stmt::Drop { index, line } => {
99            note_local_use(ranges, *index, *line);
100        }
101        Stmt::Let {
102            index, expr, line, ..
103        } => {
104            note_local_decl(ranges, *index, *line);
105            record_expr_local_debug_ranges(expr, *line, ranges);
106        }
107        Stmt::Assign {
108            index, expr, line, ..
109        } => {
110            note_local_use(ranges, *index, *line);
111            record_expr_local_debug_ranges(expr, *line, ranges);
112        }
113        Stmt::ClosureLet { line, closure } => {
114            for (source_slot, captured_slot) in &closure.capture_copies {
115                note_local_use(ranges, *source_slot, *line);
116                note_local_use(ranges, *captured_slot, *line);
117            }
118            record_expr_local_debug_ranges(&closure.body, *line, ranges);
119        }
120        Stmt::Expr { expr, line } => {
121            record_expr_local_debug_ranges(expr, *line, ranges);
122        }
123        Stmt::IfElse {
124            condition,
125            then_branch,
126            else_branch,
127            line,
128        } => {
129            record_expr_local_debug_ranges(condition, *line, ranges);
130            for nested in then_branch {
131                record_stmt_local_debug_ranges(nested, ranges);
132            }
133            for nested in else_branch {
134                record_stmt_local_debug_ranges(nested, ranges);
135            }
136        }
137        Stmt::For {
138            init,
139            condition,
140            post,
141            body,
142            line,
143        } => {
144            record_stmt_local_debug_ranges(init, ranges);
145            record_expr_local_debug_ranges(condition, *line, ranges);
146            record_stmt_local_debug_ranges(post, ranges);
147            for nested in body {
148                record_stmt_local_debug_ranges(nested, ranges);
149            }
150        }
151        Stmt::While {
152            condition,
153            body,
154            line,
155        } => {
156            record_expr_local_debug_ranges(condition, *line, ranges);
157            for nested in body {
158                record_stmt_local_debug_ranges(nested, ranges);
159            }
160        }
161    }
162}
163
164fn record_expr_local_debug_ranges(
165    expr: &Expr,
166    line: u32,
167    ranges: &mut HashMap<LocalSlot, LocalDebugRange>,
168) {
169    match expr {
170        Expr::Null
171        | Expr::Int(_)
172        | Expr::Float(_)
173        | Expr::Bool(_)
174        | Expr::Bytes(_)
175        | Expr::String(_)
176        | Expr::FunctionRef(_) => {}
177        Expr::Var(index) | Expr::MoveVar(index) => {
178            note_local_use(ranges, *index, line);
179        }
180        Expr::MoveField { root, .. } | Expr::MoveIndex { root, .. } => {
181            note_local_use(ranges, *root, line);
182        }
183        Expr::OptionalGet {
184            container,
185            key,
186            container_slot,
187            key_slot,
188        } => {
189            note_local_use(ranges, *container_slot, line);
190            note_local_use(ranges, *key_slot, line);
191            record_expr_local_debug_ranges(container, line, ranges);
192            record_expr_local_debug_ranges(key, line, ranges);
193        }
194        Expr::OptionUnwrapOr {
195            value,
196            value_slot,
197            fallback,
198        } => {
199            note_local_use(ranges, *value_slot, line);
200            record_expr_local_debug_ranges(value, line, ranges);
201            record_expr_local_debug_ranges(fallback, line, ranges);
202        }
203        Expr::Call(_, _, args) => {
204            for arg in args {
205                record_expr_local_debug_ranges(arg, line, ranges);
206            }
207        }
208        Expr::LocalCall(index, _, args) => {
209            note_local_use(ranges, *index, line);
210            for arg in args {
211                record_expr_local_debug_ranges(arg, line, ranges);
212            }
213        }
214        Expr::Closure(closure) => {
215            for (source_slot, captured_slot) in &closure.capture_copies {
216                note_local_use(ranges, *source_slot, line);
217                note_local_use(ranges, *captured_slot, line);
218            }
219            record_expr_local_debug_ranges(&closure.body, line, ranges);
220        }
221        Expr::ClosureCall(closure, args) => {
222            for arg in args {
223                record_expr_local_debug_ranges(arg, line, ranges);
224            }
225            for (source_slot, captured_slot) in &closure.capture_copies {
226                note_local_use(ranges, *source_slot, line);
227                note_local_use(ranges, *captured_slot, line);
228            }
229            record_expr_local_debug_ranges(&closure.body, line, ranges);
230        }
231        Expr::Add(lhs, rhs)
232        | Expr::Sub(lhs, rhs)
233        | Expr::Mul(lhs, rhs)
234        | Expr::Div(lhs, rhs)
235        | Expr::Mod(lhs, rhs)
236        | Expr::And(lhs, rhs)
237        | Expr::Or(lhs, rhs)
238        | Expr::Eq(lhs, rhs)
239        | Expr::Lt(lhs, rhs)
240        | Expr::Gt(lhs, rhs) => {
241            record_expr_local_debug_ranges(lhs, line, ranges);
242            record_expr_local_debug_ranges(rhs, line, ranges);
243        }
244        Expr::Neg(inner)
245        | Expr::Not(inner)
246        | Expr::ToOwned(inner)
247        | Expr::Borrow(inner)
248        | Expr::BorrowMut(inner) => {
249            record_expr_local_debug_ranges(inner, line, ranges);
250        }
251        Expr::IfElse {
252            condition,
253            then_expr,
254            else_expr,
255        } => {
256            record_expr_local_debug_ranges(condition, line, ranges);
257            record_expr_local_debug_ranges(then_expr, line, ranges);
258            record_expr_local_debug_ranges(else_expr, line, ranges);
259        }
260        Expr::Match {
261            value_slot,
262            result_slot,
263            value,
264            arms,
265            default,
266        } => {
267            note_local_use(ranges, *value_slot, line);
268            note_local_use(ranges, *result_slot, line);
269            record_expr_local_debug_ranges(value, line, ranges);
270            for (pattern, arm_expr) in arms {
271                if let Some(binding_slot) = pattern.binding_slot() {
272                    note_local_use(ranges, binding_slot, line);
273                }
274                record_expr_local_debug_ranges(arm_expr, line, ranges);
275            }
276            record_expr_local_debug_ranges(default, line, ranges);
277        }
278        Expr::Block { stmts, expr } => {
279            for stmt in stmts {
280                record_stmt_local_debug_ranges(stmt, ranges);
281            }
282            record_expr_local_debug_ranges(expr, line, ranges);
283        }
284    }
285}
286
287fn note_local_decl(ranges: &mut HashMap<LocalSlot, LocalDebugRange>, slot: LocalSlot, line: u32) {
288    let entry = ranges.entry(slot).or_default();
289    entry.declared_line = Some(
290        entry
291            .declared_line
292            .map_or(line, |current| current.min(line)),
293    );
294    entry.last_line = Some(entry.last_line.map_or(line, |current| current.max(line)));
295}
296
297fn note_local_use(ranges: &mut HashMap<LocalSlot, LocalDebugRange>, slot: LocalSlot, line: u32) {
298    let entry = ranges.entry(slot).or_default();
299    entry.last_line = Some(entry.last_line.map_or(line, |current| current.max(line)));
300}
301
302fn merge_min_debug_line(current: Option<u32>, incoming: Option<u32>) -> Option<u32> {
303    match (current, incoming) {
304        (Some(lhs), Some(rhs)) => Some(lhs.min(rhs)),
305        (Some(lhs), None) => Some(lhs),
306        (None, Some(rhs)) => Some(rhs),
307        (None, None) => None,
308    }
309}
310
311fn merge_max_debug_line(current: Option<u32>, incoming: Option<u32>) -> Option<u32> {
312    match (current, incoming) {
313        (Some(lhs), Some(rhs)) => Some(lhs.max(rhs)),
314        (Some(lhs), None) => Some(lhs),
315        (None, Some(rhs)) => Some(rhs),
316        (None, None) => None,
317    }
318}
319
320fn stmt_source_line(stmt: &Stmt) -> u32 {
321    match stmt {
322        Stmt::Noop { line }
323        | Stmt::Let { line, .. }
324        | Stmt::Assign { line, .. }
325        | Stmt::ClosureLet { line, .. }
326        | Stmt::FuncDecl { line, .. }
327        | Stmt::Expr { line, .. }
328        | Stmt::IfElse { line, .. }
329        | Stmt::For { line, .. }
330        | Stmt::While { line, .. }
331        | Stmt::Break { line }
332        | Stmt::Continue { line }
333        | Stmt::Drop { line, .. } => *line,
334    }
335}
336
337fn is_compiler_primitive_import(name: &str) -> bool {
338    name.starts_with("__prim_")
339}
340
341fn compile_parsed_output(
342    source: String,
343    parsed: FrontendIr,
344    behavior: CompileBehavior,
345    typing_mode: TypingMode,
346    enable_local_move_semantics: bool,
347) -> Result<CompiledProgram, SourceError> {
348    compile_parsed_output_with_entry_locals(
349        source,
350        parsed,
351        &[],
352        &[],
353        behavior,
354        typing_mode,
355        enable_local_move_semantics,
356    )
357}
358
359fn compile_parsed_output_with_entry_locals(
360    source: String,
361    parsed: FrontendIr,
362    entry_locals: &[lifetime::EntryLocalAvailability],
363    entry_local_types: &[typing::EntryLocalType],
364    behavior: CompileBehavior,
365    typing_mode: TypingMode,
366    enable_local_move_semantics: bool,
367) -> Result<CompiledProgram, SourceError> {
368    // Normal compilation passes no entry locals. The REPL uses this hook to treat
369    // carried-over locals from prior entries as already available at snippet start.
370    if typing_mode.is_strict() {
371        reject_strict_unknown_annotations(&parsed).map_err(SourceError::Parse)?;
372    }
373    let local_debug_ranges = collect_named_local_debug_ranges(&parsed);
374    let parsed = typing::legalize_builtins_and_bind_types(parsed, typing_mode, entry_local_types);
375    typing::validate_if_else_type_consistency(&parsed, typing_mode, entry_local_types)
376        .map_err(SourceError::Compile)?;
377    if typing_mode.is_strict() {
378        let strict_type_info = typing::infer_types(&parsed, typing_mode, entry_local_types);
379        enforce_strict_rustscript_type_resolution(&parsed, &strict_type_info)
380            .map_err(SourceError::Compile)?;
381    }
382    let parsed = lifetime::enforce_local_availability_with_entry_locals(
383        parsed,
384        entry_locals,
385        behavior.clear_dead_locals,
386        enable_local_move_semantics,
387    )
388    .map_err(SourceError::Parse)?;
389    let type_info = typing::infer_types(&parsed, typing_mode, entry_local_types);
390    let FrontendIr {
391        stmts,
392        locals,
393        local_bindings,
394        struct_schemas,
395        functions,
396        function_impls,
397        ..
398    } = parsed;
399    let function_decls = functions
400        .iter()
401        .cloned()
402        .map(|decl| (decl.index, decl))
403        .collect::<HashMap<_, _>>();
404
405    let mut runtime_import_functions: Vec<FunctionDecl> = functions
406        .iter()
407        .filter(|func| !function_impls.contains_key(&func.index))
408        .cloned()
409        .collect();
410    let mut call_index_remap = HashMap::<u16, u16>::new();
411    for (next_index, func) in runtime_import_functions.iter_mut().enumerate() {
412        let next_index = u16::try_from(next_index).map_err(|_| {
413            SourceError::Parse(ParseError {
414                span: None,
415                code: None,
416                line: 1,
417                message: "too many host imports after RSS function inlining".to_string(),
418            })
419        })?;
420        call_index_remap.insert(func.index, next_index);
421        func.index = next_index;
422    }
423    let visible_runtime_import_functions = runtime_import_functions
424        .iter()
425        .filter(|func| !is_compiler_primitive_import(&func.name))
426        .cloned()
427        .collect::<Vec<_>>();
428    let host_import_return_types = functions
429        .iter()
430        .filter(|func| !function_impls.contains_key(&func.index))
431        .map(|func| (func.index, typing::BoundType::from(func.return_type)))
432        .collect::<HashMap<_, _>>();
433    let host_import_signatures = typing::build_host_import_signatures(&functions, &function_impls);
434
435    let mut compiler = Compiler::new();
436    compiler.set_type_inference(type_info);
437    compiler.set_typing_mode(typing_mode);
438    compiler.set_source(source);
439    compiler.set_function_decls(function_decls);
440    compiler.set_function_impls(function_impls);
441    compiler.set_struct_schemas(struct_schemas);
442    compiler.set_host_import_return_types(host_import_return_types);
443    compiler.set_host_import_signatures(host_import_signatures);
444    compiler.set_call_index_remap(call_index_remap);
445    compiler.set_enable_local_move_semantics(enable_local_move_semantics);
446    for func in &functions {
447        compiler.add_function_debug(func);
448    }
449    for (name, index) in local_bindings {
450        let range = local_debug_ranges.get(&name).copied().unwrap_or_default();
451        compiler
452            .add_local_debug(name, index, range.declared_line, range.last_line)
453            .map_err(SourceError::Compile)?;
454    }
455    let mut program = compiler
456        .compile_program(&stmts)
457        .map_err(SourceError::Compile)?;
458    program.local_count = locals;
459    program.imports = runtime_import_functions
460        .iter()
461        .map(|func| HostImport {
462            name: func.name.clone(),
463            arity: func.arity,
464            return_type: func.return_type,
465        })
466        .collect();
467    Ok(CompiledProgram {
468        program,
469        locals,
470        functions: visible_runtime_import_functions,
471    })
472}
473
474#[derive(Clone, Debug)]
475struct StrictSlotSite {
476    name: String,
477    kind: &'static str,
478    line: Option<u32>,
479    source_name: Option<String>,
480}
481
482fn reject_strict_unknown_annotations(parsed: &FrontendIr) -> Result<(), ParseError> {
483    let Some(span) = parsed.unknown_type_spans.first().copied() else {
484        return Ok(());
485    };
486    Err(ParseError {
487        line: 1,
488        message:
489            "RustScript requires concrete compile-time types; 'unknown' annotations are not allowed"
490                .to_string(),
491        span: Some(span),
492        code: Some("E_STRICT_UNKNOWN_TYPE".to_string()),
493    })
494}
495
496fn enforce_strict_rustscript_type_resolution(
497    parsed: &FrontendIr,
498    type_info: &typing::TypeInferenceResult,
499) -> Result<(), CompileError> {
500    for schema in parsed.struct_schemas.values() {
501        if schema_is_fully_known(&schema.body_schema) {
502            continue;
503        }
504        return Err(CompileError::StrictTypingRequired {
505            line: None,
506            source_name: None,
507            detail: format!(
508                "struct '{}' contains non-concrete field types; RustScript requires concrete schemas",
509                schema.name
510            ),
511        });
512    }
513
514    let function_decl_lines = collect_function_decl_lines(&parsed.stmts);
515    for decl in &parsed.functions {
516        if let Some(schema) = decl.return_schema.as_ref()
517            && !schema_is_fully_known(schema)
518        {
519            return Err(CompileError::StrictTypingRequired {
520                line: function_decl_lines.get(&decl.index).copied(),
521                source_name: parsed.function_sources.get(&decl.index).cloned(),
522                detail: format!(
523                    "function '{}' uses a non-concrete return schema; RustScript requires concrete return types",
524                    decl.name
525                ),
526            });
527        }
528    }
529
530    for (slot, site) in collect_strict_slot_sites(parsed) {
531        if slot_is_fully_typed(slot, type_info) {
532            continue;
533        }
534        return Err(CompileError::StrictTypingRequired {
535            line: site.line,
536            source_name: site.source_name,
537            detail: format!(
538                "{} '{}' does not resolve to a concrete compile-time type in RustScript",
539                site.kind, site.name
540            ),
541        });
542    }
543
544    Ok(())
545}
546
547fn slot_is_fully_typed(slot: LocalSlot, type_info: &typing::TypeInferenceResult) -> bool {
548    let slot_index = usize::from(slot);
549    if type_info
550        .callable_slots
551        .get(slot_index)
552        .copied()
553        .unwrap_or(false)
554    {
555        return true;
556    }
557    if let Some(schema) = type_info
558        .local_schemas
559        .get(slot_index)
560        .and_then(|schema| schema.as_ref())
561    {
562        return schema_is_fully_known(schema);
563    }
564    type_info.local_types.get(slot_index).copied() != Some(crate::ValueType::Unknown)
565}
566
567fn schema_is_fully_known(schema: &TypeSchema) -> bool {
568    match schema {
569        TypeSchema::Unknown => false,
570        TypeSchema::Null
571        | TypeSchema::Int
572        | TypeSchema::Float
573        | TypeSchema::Number
574        | TypeSchema::Bool
575        | TypeSchema::String
576        | TypeSchema::Bytes
577        | TypeSchema::GenericParam(_) => true,
578        TypeSchema::Optional(inner) => schema_is_fully_known(inner),
579        TypeSchema::Named(_, type_args) => type_args.iter().all(schema_is_fully_known),
580        TypeSchema::Array(item) | TypeSchema::Map(item) => schema_is_fully_known(item),
581        TypeSchema::ArrayTuple(items) => items.iter().all(schema_is_fully_known),
582        TypeSchema::ArrayTupleRest { prefix, rest } => {
583            prefix.iter().all(schema_is_fully_known) && schema_is_fully_known(rest)
584        }
585        TypeSchema::Object(fields) => fields.values().all(schema_is_fully_known),
586        TypeSchema::Callable { params, result } => {
587            params.iter().all(schema_is_fully_known) && schema_is_fully_known(result)
588        }
589    }
590}
591
592fn collect_strict_slot_sites(parsed: &FrontendIr) -> Vec<(LocalSlot, StrictSlotSite)> {
593    let mut sites = Vec::new();
594    let local_debug_ranges = collect_local_debug_ranges(&parsed.stmts, &parsed.function_impls);
595    let local_source_names = collect_local_source_names(parsed);
596    for (name, slot) in &parsed.local_bindings {
597        let line = local_debug_ranges
598            .get(slot)
599            .and_then(|range| range.declared_line);
600        sites.push((
601            *slot,
602            StrictSlotSite {
603                name: name.clone(),
604                kind: "local",
605                line,
606                source_name: local_source_names.get(slot).cloned().flatten(),
607            },
608        ));
609    }
610
611    let function_decl_lines = collect_function_decl_lines(&parsed.stmts);
612    for decl in &parsed.functions {
613        let Some(function_impl) = parsed.function_impls.get(&decl.index) else {
614            continue;
615        };
616        for (name, slot) in decl.args.iter().zip(function_impl.param_slots.iter()) {
617            sites.push((
618                *slot,
619                StrictSlotSite {
620                    name: name.clone(),
621                    kind: "parameter",
622                    line: function_decl_lines.get(&decl.index).copied(),
623                    source_name: parsed.function_sources.get(&decl.index).cloned(),
624                },
625            ));
626        }
627    }
628
629    sites.sort_by_key(|(slot, _)| *slot);
630    sites
631}
632
633fn collect_local_source_names(parsed: &FrontendIr) -> HashMap<LocalSlot, Option<String>> {
634    let mut out = HashMap::new();
635    for (index, stmt) in parsed.stmts.iter().enumerate() {
636        let source_name = parsed
637            .stmt_sources
638            .get(index)
639            .and_then(|source| source.as_deref());
640        record_local_source_names(std::slice::from_ref(stmt), source_name, &mut out);
641    }
642    for decl in &parsed.functions {
643        let Some(function_impl) = parsed.function_impls.get(&decl.index) else {
644            continue;
645        };
646        let source_name = parsed.function_sources.get(&decl.index).map(String::as_str);
647        record_local_source_names(&function_impl.body_stmts, source_name, &mut out);
648    }
649    out
650}
651
652fn record_local_source_names(
653    stmts: &[Stmt],
654    source_name: Option<&str>,
655    out: &mut HashMap<LocalSlot, Option<String>>,
656) {
657    let source_name = source_name.map(str::to_string);
658    for stmt in stmts {
659        match stmt {
660            Stmt::Let { index, .. } => {
661                out.entry(*index).or_insert_with(|| source_name.clone());
662            }
663            Stmt::IfElse {
664                then_branch,
665                else_branch,
666                ..
667            } => {
668                record_local_source_names(then_branch, source_name.as_deref(), out);
669                record_local_source_names(else_branch, source_name.as_deref(), out);
670            }
671            Stmt::For {
672                init, post, body, ..
673            } => {
674                record_local_source_names(
675                    std::slice::from_ref(init.as_ref()),
676                    source_name.as_deref(),
677                    out,
678                );
679                record_local_source_names(
680                    std::slice::from_ref(post.as_ref()),
681                    source_name.as_deref(),
682                    out,
683                );
684                record_local_source_names(body, source_name.as_deref(), out);
685            }
686            Stmt::While { body, .. } => {
687                record_local_source_names(body, source_name.as_deref(), out);
688            }
689            Stmt::Noop { .. }
690            | Stmt::Assign { .. }
691            | Stmt::ClosureLet { .. }
692            | Stmt::FuncDecl { .. }
693            | Stmt::Expr { .. }
694            | Stmt::Break { .. }
695            | Stmt::Continue { .. }
696            | Stmt::Drop { .. } => {}
697        }
698    }
699}
700
701pub fn compile_source(source: &str) -> Result<CompiledProgram, SourceError> {
702    compile_source_with_flavor(source, SourceFlavor::RustScript)
703}
704
705pub fn lint_trailing_function_return_semicolons(
706    source: &str,
707    flavor: SourceFlavor,
708) -> Result<Vec<ParseError>, ParseError> {
709    let Some(dialect) =
710        frontends::parser_dialect_for_flavor(flavor, &CompileSourceFileOptions::default())
711    else {
712        return Ok(Vec::new());
713    };
714    parser::lint_trailing_function_return_semicolons(source, 0, dialect)
715}
716
717pub fn lint_unknown_type_annotations(
718    source: &str,
719    flavor: SourceFlavor,
720) -> Result<Vec<crate::compiler::source_map::Span>, SourceError> {
721    let mut source_map = SourceMap::new();
722    let source_id = source_map.add_source("<source>", source.to_string());
723    let parsed = frontends::parse_source(source, flavor, &CompileSourceFileOptions::default())
724        .map_err(|err| {
725            SourceError::Parse(err.with_line_span_from_source(&source_map, source_id))
726        })?;
727    Ok(parsed.unknown_type_spans)
728}
729
730pub fn lint_unknown_inferred_local_types(
731    source: &str,
732    flavor: SourceFlavor,
733) -> Result<Vec<UnknownInferredLocal>, SourceError> {
734    lint_unknown_inferred_local_types_impl(source, flavor)
735}
736
737pub fn collect_inferred_local_type_hints(
738    source: &str,
739    flavor: SourceFlavor,
740) -> Result<Vec<InferredLocalTypeHint>, SourceError> {
741    collect_inferred_local_type_hints_impl(source, flavor)
742}
743
744pub fn collect_inferred_local_type_hints_with_options(
745    source: &str,
746    flavor: SourceFlavor,
747    options: CompileSourceFileOptions,
748) -> Result<Vec<InferredLocalTypeHint>, SourcePathError> {
749    let source_owned = source.to_string();
750    run_with_compiler_stack(move || {
751        collect_inferred_local_type_hints_with_options_impl(&source_owned, flavor, &options)
752    })
753}
754
755pub fn collect_inferred_local_type_hints_at_path_with_options(
756    path: impl AsRef<Path>,
757    source: &str,
758    flavor: SourceFlavor,
759    options: CompileSourceFileOptions,
760) -> Result<Vec<InferredLocalTypeHint>, SourcePathError> {
761    let path = path.as_ref().to_path_buf();
762    let source_owned = source.to_string();
763    run_with_compiler_stack(move || {
764        collect_inferred_local_type_hints_at_path_with_options_impl(
765            &path,
766            &source_owned,
767            flavor,
768            &options,
769        )
770    })
771}
772
773pub fn lint_unknown_inferred_local_types_with_options(
774    source: &str,
775    flavor: SourceFlavor,
776    options: CompileSourceFileOptions,
777) -> Result<Vec<UnknownInferredLocal>, SourcePathError> {
778    let source_owned = source.to_string();
779    run_with_compiler_stack(move || {
780        lint_unknown_inferred_local_types_with_options_impl(&source_owned, flavor, &options)
781    })
782}
783
784pub fn lint_unknown_inferred_local_types_at_path_with_options(
785    path: impl AsRef<Path>,
786    source: &str,
787    flavor: SourceFlavor,
788    options: CompileSourceFileOptions,
789) -> Result<Vec<UnknownInferredLocal>, SourcePathError> {
790    let path = path.as_ref().to_path_buf();
791    let source_owned = source.to_string();
792    run_with_compiler_stack(move || {
793        lint_unknown_inferred_local_types_at_path_with_options_impl(
794            &path,
795            &source_owned,
796            flavor,
797            &options,
798        )
799    })
800}
801
802fn lint_unknown_inferred_local_types_impl(
803    source: &str,
804    flavor: SourceFlavor,
805) -> Result<Vec<UnknownInferredLocal>, SourceError> {
806    let mut source_map = SourceMap::new();
807    let source_id = source_map.add_source("<source>", source.to_string());
808    let parsed = frontends::parse_source(source, flavor, &CompileSourceFileOptions::default())
809        .map_err(|err| {
810            SourceError::Parse(err.with_line_span_from_source(&source_map, source_id))
811        })?;
812    Ok(collect_unknown_inferred_local_types(
813        &source_map,
814        source_id,
815        parsed,
816    ))
817}
818
819fn collect_inferred_local_type_hints_impl(
820    source: &str,
821    flavor: SourceFlavor,
822) -> Result<Vec<InferredLocalTypeHint>, SourceError> {
823    let mut source_map = SourceMap::new();
824    let source_id = source_map.add_source("<source>", source.to_string());
825    let parsed = frontends::parse_source(source, flavor, &CompileSourceFileOptions::default())
826        .map_err(|err| {
827            SourceError::Parse(err.with_line_span_from_source(&source_map, source_id))
828        })?;
829    Ok(collect_named_local_type_hints(parsed))
830}
831
832fn lint_unknown_inferred_local_types_with_options_impl(
833    source: &str,
834    flavor: SourceFlavor,
835    options: &CompileSourceFileOptions,
836) -> Result<Vec<UnknownInferredLocal>, SourcePathError> {
837    if !options.has_module_overrides() && !options.has_source_plugins() {
838        return lint_unknown_inferred_local_types_impl(source, flavor)
839            .map_err(SourcePathError::Source);
840    }
841
842    let path = virtual_inmemory_entry_path(flavor);
843    lint_unknown_inferred_local_types_at_path_with_options_impl(&path, source, flavor, options)
844}
845
846fn collect_inferred_local_type_hints_with_options_impl(
847    source: &str,
848    flavor: SourceFlavor,
849    options: &CompileSourceFileOptions,
850) -> Result<Vec<InferredLocalTypeHint>, SourcePathError> {
851    if !options.has_module_overrides() && !options.has_source_plugins() {
852        return collect_inferred_local_type_hints_impl(source, flavor)
853            .map_err(SourcePathError::Source);
854    }
855
856    let path = virtual_inmemory_entry_path(flavor);
857    collect_inferred_local_type_hints_at_path_with_options_impl(&path, source, flavor, options)
858}
859
860fn lint_unknown_inferred_local_types_at_path_with_options_impl(
861    path: &Path,
862    source: &str,
863    flavor: SourceFlavor,
864    options: &CompileSourceFileOptions,
865) -> Result<Vec<UnknownInferredLocal>, SourcePathError> {
866    let mut source_map = SourceMap::new();
867    let source_id = source_map.add_source(path.display().to_string(), source.to_string());
868    let (_root_parse_source, units) = load_units_for_source_file(path, flavor, source, options)?;
869    let parsed = units
870        .into_iter()
871        .last()
872        .map(|unit| unit.parsed)
873        .expect("root parsed unit should always be present");
874    Ok(collect_unknown_inferred_local_types(
875        &source_map,
876        source_id,
877        parsed,
878    ))
879}
880
881fn collect_inferred_local_type_hints_at_path_with_options_impl(
882    path: &Path,
883    source: &str,
884    flavor: SourceFlavor,
885    options: &CompileSourceFileOptions,
886) -> Result<Vec<InferredLocalTypeHint>, SourcePathError> {
887    let (_root_parse_source, units) = load_units_for_source_file(path, flavor, source, options)?;
888    let parsed = units
889        .into_iter()
890        .last()
891        .map(|unit| unit.parsed)
892        .expect("root parsed unit should always be present");
893    Ok(collect_named_local_type_hints(parsed))
894}
895
896fn collect_unknown_inferred_local_types(
897    source_map: &SourceMap,
898    source_id: u32,
899    parsed: FrontendIr,
900) -> Vec<UnknownInferredLocal> {
901    let local_debug_ranges = collect_local_debug_ranges(&parsed.stmts, &parsed.function_impls);
902    let parsed = typing::legalize_builtins_and_bind_types(parsed, TypingMode::DynamicHints, &[]);
903    let type_info = typing::infer_types(&parsed, TypingMode::DynamicHints, &[]);
904
905    let mut warnings = Vec::new();
906    for (name, slot) in &parsed.local_bindings {
907        let Some(range) = local_debug_ranges.get(slot) else {
908            continue;
909        };
910        let Some(line_u32) = range.declared_line else {
911            continue;
912        };
913        let slot_index = usize::from(*slot);
914        if type_info
915            .callable_slots
916            .get(slot_index)
917            .copied()
918            .unwrap_or(false)
919        {
920            continue;
921        }
922        if type_info
923            .local_schema_labels
924            .get(slot_index)
925            .and_then(|label| label.as_ref())
926            .is_some_and(|label| label != "unknown")
927        {
928            continue;
929        }
930        if type_info.local_types.get(slot_index) != Some(&crate::ValueType::Unknown) {
931            continue;
932        }
933        let line = usize::try_from(line_u32).unwrap_or(usize::MAX);
934        warnings.push(UnknownInferredLocal {
935            name: name.clone(),
936            line,
937            span: find_local_name_span(source_map, source_id, line, name)
938                .or_else(|| source_map.line_span(source_id, line)),
939        });
940    }
941    warnings
942}
943
944fn collect_named_local_type_hints(parsed: FrontendIr) -> Vec<InferredLocalTypeHint> {
945    let slot_ranges = collect_local_debug_ranges(&parsed.stmts, &parsed.function_impls);
946    let function_decl_lines = collect_function_decl_lines(&parsed.stmts);
947    let parsed = typing::legalize_builtins_and_bind_types(parsed, TypingMode::DynamicHints, &[]);
948    let type_info = typing::infer_types(&parsed, TypingMode::DynamicHints, &[]);
949
950    let mut hints = Vec::new();
951    for (name, slot) in &parsed.local_bindings {
952        hints.push(InferredLocalTypeHint {
953            name: name.clone(),
954            inferred_type: inferred_slot_type_name(&type_info, *slot),
955            declared_line: slot_ranges.get(slot).and_then(|range| range.declared_line),
956            last_line: slot_ranges.get(slot).and_then(|range| range.last_line),
957        });
958    }
959
960    for decl in &parsed.functions {
961        let Some(function_impl) = parsed.function_impls.get(&decl.index) else {
962            continue;
963        };
964        let declared_line = function_decl_lines.get(&decl.index).copied();
965        let last_line = function_scope_last_line(function_impl).or(declared_line);
966        for (name, slot) in decl.args.iter().zip(function_impl.param_slots.iter()) {
967            hints.push(InferredLocalTypeHint {
968                name: name.clone(),
969                inferred_type: inferred_slot_type_name(&type_info, *slot),
970                declared_line: slot_ranges
971                    .get(slot)
972                    .and_then(|range| range.declared_line)
973                    .or(declared_line),
974                last_line: slot_ranges
975                    .get(slot)
976                    .and_then(|range| range.last_line)
977                    .or(last_line),
978            });
979        }
980    }
981
982    hints
983}
984
985fn inferred_slot_type_name(type_info: &typing::TypeInferenceResult, slot: LocalSlot) -> String {
986    let slot_index = usize::from(slot);
987    if type_info
988        .callable_slots
989        .get(slot_index)
990        .copied()
991        .unwrap_or(false)
992    {
993        return "function".to_string();
994    }
995    if let Some(label) = type_info
996        .local_schema_labels
997        .get(slot_index)
998        .and_then(|label| label.as_ref())
999        .filter(|label| label.as_str() != "unknown")
1000    {
1001        return label.clone();
1002    }
1003    value_type_name(
1004        type_info
1005            .local_types
1006            .get(slot_index)
1007            .copied()
1008            .unwrap_or(crate::ValueType::Unknown),
1009    )
1010    .to_string()
1011}
1012
1013fn value_type_name(value: crate::ValueType) -> &'static str {
1014    match value {
1015        crate::ValueType::Unknown => "unknown",
1016        crate::ValueType::Null => "null",
1017        crate::ValueType::Int => "int",
1018        crate::ValueType::Float => "float",
1019        crate::ValueType::Bool => "bool",
1020        crate::ValueType::String => "string",
1021        crate::ValueType::Bytes => "bytes",
1022        crate::ValueType::Array => "array",
1023        crate::ValueType::Map => "map",
1024    }
1025}
1026
1027fn collect_function_decl_lines(stmts: &[Stmt]) -> HashMap<u16, u32> {
1028    let mut lines = HashMap::new();
1029    record_function_decl_lines(stmts, &mut lines);
1030    lines
1031}
1032
1033fn record_function_decl_lines(stmts: &[Stmt], lines: &mut HashMap<u16, u32>) {
1034    for stmt in stmts {
1035        match stmt {
1036            Stmt::FuncDecl { index, line, .. } => {
1037                lines.insert(*index, *line);
1038            }
1039            Stmt::IfElse {
1040                then_branch,
1041                else_branch,
1042                ..
1043            } => {
1044                record_function_decl_lines(then_branch, lines);
1045                record_function_decl_lines(else_branch, lines);
1046            }
1047            Stmt::For {
1048                init, post, body, ..
1049            } => {
1050                record_function_decl_lines(std::slice::from_ref(init.as_ref()), lines);
1051                record_function_decl_lines(std::slice::from_ref(post.as_ref()), lines);
1052                record_function_decl_lines(body, lines);
1053            }
1054            Stmt::While { body, .. } => {
1055                record_function_decl_lines(body, lines);
1056            }
1057            Stmt::Noop { .. }
1058            | Stmt::Let { .. }
1059            | Stmt::Assign { .. }
1060            | Stmt::ClosureLet { .. }
1061            | Stmt::Expr { .. }
1062            | Stmt::Break { .. }
1063            | Stmt::Continue { .. }
1064            | Stmt::Drop { .. } => {}
1065        }
1066    }
1067}
1068
1069fn function_scope_last_line(function_impl: &FunctionImpl) -> Option<u32> {
1070    let stmt_last_line = function_impl.body_stmts.last().map(stmt_source_line);
1071    match stmt_last_line {
1072        Some(line) => Some(line.max(function_impl.body_expr_line)),
1073        None if function_impl.body_expr_line > 0 => Some(function_impl.body_expr_line),
1074        None => None,
1075    }
1076}
1077
1078fn find_local_name_span(
1079    source_map: &SourceMap,
1080    source_id: u32,
1081    line: usize,
1082    name: &str,
1083) -> Option<crate::compiler::source_map::Span> {
1084    let file = source_map.file(source_id)?;
1085    let line_range = file.line_span(line)?;
1086    let line_text = file.line_text(line)?;
1087    let mut search_start = 0usize;
1088    while let Some(relative) = line_text[search_start..].find(name) {
1089        let start = search_start + relative;
1090        let end = start + name.len();
1091        let prev_ok = start == 0
1092            || !line_text[..start]
1093                .chars()
1094                .next_back()
1095                .is_some_and(is_ident_char);
1096        let next_ok =
1097            end == line_text.len() || !line_text[end..].chars().next().is_some_and(is_ident_char);
1098        if prev_ok && next_ok {
1099            return Some(crate::compiler::source_map::Span::new(
1100                source_id,
1101                line_range.start + start,
1102                line_range.start + end,
1103            ));
1104        }
1105        search_start = end;
1106    }
1107    None
1108}
1109
1110fn is_ident_char(ch: char) -> bool {
1111    ch.is_ascii_alphanumeric() || ch == '_'
1112}
1113
1114pub fn compile_source_for_repl(source: &str) -> Result<CompiledProgram, SourceError> {
1115    compile_source_for_repl_with_locals(source, &[]).map(|compiled| compiled.compiled)
1116}
1117
1118pub fn compile_source_for_repl_with_locals(
1119    source: &str,
1120    predefined_locals: &[ReplLocalBinding],
1121) -> Result<CompiledReplProgram, SourceError> {
1122    let source_owned = source.to_string();
1123    let predefined_locals = predefined_locals.to_vec();
1124    run_with_compiler_stack(move || {
1125        compile_source_for_repl_with_locals_impl(&source_owned, &predefined_locals, &[])
1126    })
1127}
1128
1129pub fn compile_source_for_repl_with_state(
1130    source: &str,
1131    predefined_locals: &[ReplLocalState],
1132) -> Result<CompiledReplProgram, SourceError> {
1133    let source_owned = source.to_string();
1134    let predefined_locals = predefined_locals.to_vec();
1135    run_with_compiler_stack(move || {
1136        let bindings = predefined_locals
1137            .iter()
1138            .map(|state| state.binding.clone())
1139            .collect::<Vec<_>>();
1140        let moved_names = predefined_locals
1141            .iter()
1142            .filter(|state| state.moved)
1143            .map(|state| state.binding.name.clone())
1144            .collect::<Vec<_>>();
1145        compile_source_for_repl_with_locals_impl(&source_owned, &bindings, &moved_names)
1146    })
1147}
1148
1149pub fn compile_source_with_flavor(
1150    source: &str,
1151    flavor: SourceFlavor,
1152) -> Result<CompiledProgram, SourceError> {
1153    compile_source_with_flavor_and_behavior(source, flavor, CompileBehavior::DEFAULT)
1154}
1155
1156pub fn compile_source_with_flavor_and_options(
1157    source: &str,
1158    flavor: SourceFlavor,
1159    options: CompileSourceFileOptions,
1160) -> Result<CompiledProgram, SourcePathError> {
1161    let source_owned = source.to_string();
1162    run_with_compiler_stack(move || {
1163        compile_source_with_flavor_and_options_impl(&source_owned, flavor, &options)
1164    })
1165}
1166
1167pub fn compile_source_at_path_with_flavor_and_options(
1168    path: impl AsRef<Path>,
1169    source: &str,
1170    flavor: SourceFlavor,
1171    options: CompileSourceFileOptions,
1172) -> Result<CompiledProgram, SourcePathError> {
1173    let path = path.as_ref().to_path_buf();
1174    let source_owned = source.to_string();
1175    run_with_compiler_stack(move || {
1176        compile_source_at_path_with_flavor_and_options_impl(&path, &source_owned, flavor, &options)
1177    })
1178}
1179
1180fn compile_source_with_flavor_and_behavior(
1181    source: &str,
1182    flavor: SourceFlavor,
1183    behavior: CompileBehavior,
1184) -> Result<CompiledProgram, SourceError> {
1185    let owned_source = source.to_string();
1186    run_with_compiler_stack(move || {
1187        compile_source_with_flavor_impl(&owned_source, flavor, behavior)
1188    })
1189}
1190
1191fn compile_source_for_repl_with_locals_impl(
1192    source: &str,
1193    predefined_locals: &[ReplLocalBinding],
1194    moved_names: &[String],
1195) -> Result<CompiledReplProgram, SourceError> {
1196    let mut source_map = SourceMap::new();
1197    let source_id = source_map.add_source("<source>", source.to_string());
1198    // REPL parsing/compiler entry state is separate from normal program compilation so
1199    // persisted locals do not leak into the generic frontend or IR surface.
1200    let parsed =
1201        frontends::parse_rustscript_repl_source(source, predefined_locals).map_err(|err| {
1202            SourceError::Parse(err.with_line_span_from_source(&source_map, source_id))
1203        })?;
1204    let entry_local_types = build_entry_local_types(&parsed.ir, predefined_locals);
1205    let entry_availability =
1206        build_entry_local_availability(&parsed.ir, predefined_locals, moved_names);
1207    let compiled = match compile_parsed_output_with_entry_locals(
1208        source.to_string(),
1209        parsed.ir,
1210        &entry_availability,
1211        &entry_local_types,
1212        CompileBehavior::REPL,
1213        TypingMode::StrictRustScript,
1214        true,
1215    ) {
1216        Err(SourceError::Parse(err)) => Err(SourceError::Parse(
1217            err.with_line_span_from_source(&source_map, source_id),
1218        )),
1219        other => other,
1220    }?;
1221    Ok(CompiledReplProgram {
1222        compiled,
1223        bindings: parsed.bindings,
1224    })
1225}
1226
1227fn build_entry_local_availability(
1228    parsed: &FrontendIr,
1229    predefined_locals: &[ReplLocalBinding],
1230    moved_names: &[String],
1231) -> Vec<lifetime::EntryLocalAvailability> {
1232    let predefined_by_name = predefined_locals
1233        .iter()
1234        .map(|binding| (binding.name.as_str(), binding))
1235        .collect::<HashMap<_, _>>();
1236    parsed
1237        .local_bindings
1238        .iter()
1239        .filter_map(|(name, slot)| {
1240            let binding = predefined_by_name.get(name.as_str())?;
1241            let schema = binding
1242                .schema
1243                .as_ref()
1244                .map(|schema| schema.split_optional().0);
1245            let copyable = matches!(
1246                schema,
1247                Some(
1248                    TypeSchema::Null
1249                        | TypeSchema::Int
1250                        | TypeSchema::Float
1251                        | TypeSchema::Number
1252                        | TypeSchema::Bool
1253                )
1254            );
1255            let movable = matches!(schema, Some(TypeSchema::String | TypeSchema::Bytes));
1256            Some(lifetime::EntryLocalAvailability {
1257                slot: *slot,
1258                copyable,
1259                movable,
1260                moved: moved_names.iter().any(|moved| moved == name),
1261            })
1262        })
1263        .collect()
1264}
1265
1266fn build_entry_local_types(
1267    parsed: &FrontendIr,
1268    predefined_locals: &[ReplLocalBinding],
1269) -> Vec<typing::EntryLocalType> {
1270    let predefined_by_name = predefined_locals
1271        .iter()
1272        .map(|binding| (binding.name.as_str(), binding))
1273        .collect::<HashMap<_, _>>();
1274    parsed
1275        .local_bindings
1276        .iter()
1277        .filter_map(|(name, slot)| {
1278            let binding = predefined_by_name.get(name.as_str())?;
1279            let (schema, schema_optional) = binding
1280                .schema
1281                .clone()
1282                .map(|schema| schema.split_optional())
1283                .map(|(schema, optional)| (Some(schema), optional))
1284                .unwrap_or((None, false));
1285            Some(typing::EntryLocalType {
1286                slot: *slot,
1287                schema,
1288                optional: binding.optional || schema_optional,
1289            })
1290        })
1291        .collect()
1292}
1293
1294fn compile_source_with_flavor_impl(
1295    source: &str,
1296    flavor: SourceFlavor,
1297    behavior: CompileBehavior,
1298) -> Result<CompiledProgram, SourceError> {
1299    let mut source_map = SourceMap::new();
1300    let source_id = source_map.add_source("<source>", source.to_string());
1301    let parsed = frontends::parse_source(source, flavor, &CompileSourceFileOptions::default())
1302        .map_err(|err| {
1303            SourceError::Parse(err.with_line_span_from_source(&source_map, source_id))
1304        })?;
1305    match compile_parsed_output(
1306        source.to_string(),
1307        parsed,
1308        behavior,
1309        TypingMode::for_flavor(flavor),
1310        matches!(flavor, SourceFlavor::RustScript),
1311    ) {
1312        Err(SourceError::Parse(err)) => Err(SourceError::Parse(
1313            err.with_line_span_from_source(&source_map, source_id),
1314        )),
1315        other => other,
1316    }
1317}
1318
1319fn compile_source_with_flavor_and_options_impl(
1320    source: &str,
1321    flavor: SourceFlavor,
1322    options: &CompileSourceFileOptions,
1323) -> Result<CompiledProgram, SourcePathError> {
1324    if !options.has_module_overrides() && !options.has_source_plugins() {
1325        return compile_source_with_flavor_impl(source, flavor, CompileBehavior::DEFAULT)
1326            .map_err(SourcePathError::Source);
1327    }
1328
1329    let path = virtual_inmemory_entry_path(flavor);
1330    let (_root_parse_source, units) = load_units_for_source_file(&path, flavor, source, options)?;
1331    let merged = merge_units(units)?;
1332    compile_parsed_output(
1333        source.to_string(),
1334        merged,
1335        CompileBehavior::DEFAULT,
1336        TypingMode::for_flavor(flavor),
1337        matches!(flavor, SourceFlavor::RustScript),
1338    )
1339    .map_err(SourcePathError::Source)
1340}
1341
1342fn compile_source_at_path_with_flavor_and_options_impl(
1343    path: &Path,
1344    source: &str,
1345    flavor: SourceFlavor,
1346    options: &CompileSourceFileOptions,
1347) -> Result<CompiledProgram, SourcePathError> {
1348    let (_root_parse_source, units) = load_units_for_source_file(path, flavor, source, options)?;
1349    let merged = merge_units(units)?;
1350    compile_parsed_output(
1351        source.to_string(),
1352        merged,
1353        CompileBehavior::DEFAULT,
1354        TypingMode::for_flavor(flavor),
1355        matches!(flavor, SourceFlavor::RustScript),
1356    )
1357    .map_err(SourcePathError::Source)
1358}
1359
1360fn virtual_inmemory_entry_path(flavor: SourceFlavor) -> PathBuf {
1361    let ext = match flavor {
1362        SourceFlavor::RustScript => "rss",
1363        SourceFlavor::JavaScript => "js",
1364        SourceFlavor::Lua => "lua",
1365    };
1366    PathBuf::from("__pd_vm_inmemory__").join(format!("main.{ext}"))
1367}
1368
1369pub fn compile_source_file(path: impl AsRef<Path>) -> Result<CompiledProgram, SourcePathError> {
1370    compile_source_file_with_options(path, CompileSourceFileOptions::default())
1371}
1372
1373pub fn compile_source_file_with_options(
1374    path: impl AsRef<Path>,
1375    options: CompileSourceFileOptions,
1376) -> Result<CompiledProgram, SourcePathError> {
1377    let path = path.as_ref().to_path_buf();
1378    run_with_compiler_stack(move || compile_source_file_impl(&path, &options))
1379}
1380
1381fn compile_source_file_impl(
1382    path: &Path,
1383    options: &CompileSourceFileOptions,
1384) -> Result<CompiledProgram, SourcePathError> {
1385    let flavor = SourceFlavor::from_path_with_options(path, options)?;
1386    let source_raw = std::fs::read_to_string(path)?;
1387    let (_root_parse_source, units) =
1388        load_units_for_source_file(path, flavor, &source_raw, options)?;
1389    let merged = merge_units(units)?;
1390    compile_parsed_output(
1391        source_raw,
1392        merged,
1393        CompileBehavior::DEFAULT,
1394        TypingMode::for_flavor(flavor),
1395        matches!(flavor, SourceFlavor::RustScript),
1396    )
1397    .map_err(SourcePathError::Source)
1398}
1399
1400fn run_with_compiler_stack<T, F>(f: F) -> T
1401where
1402    T: Send + 'static,
1403    F: FnOnce() -> T + Send + 'static,
1404{
1405    #[cfg(target_arch = "wasm32")]
1406    {
1407        f()
1408    }
1409
1410    #[cfg(not(target_arch = "wasm32"))]
1411    {
1412        const COMPILER_STACK_SIZE: usize = 32 * 1024 * 1024;
1413        let handle = std::thread::Builder::new()
1414            .name("pd-vm-compile".to_string())
1415            .stack_size(COMPILER_STACK_SIZE)
1416            .spawn(f)
1417            .expect("failed to spawn compiler thread");
1418        match handle.join() {
1419            Ok(value) => value,
1420            Err(payload) => std::panic::resume_unwind(payload),
1421        }
1422    }
1423}