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_root_local_count(locals);
440    compiler.set_function_decls(function_decls);
441    compiler.set_function_impls(function_impls);
442    compiler.set_struct_schemas(struct_schemas);
443    compiler.set_host_import_return_types(host_import_return_types);
444    compiler.set_host_import_signatures(host_import_signatures);
445    compiler.set_call_index_remap(call_index_remap);
446    compiler.set_enable_local_move_semantics(enable_local_move_semantics);
447    for func in &functions {
448        compiler.add_function_debug(func);
449    }
450    for (name, index) in local_bindings {
451        let range = local_debug_ranges.get(&name).copied().unwrap_or_default();
452        compiler
453            .add_local_debug(name, index, range.declared_line, range.last_line)
454            .map_err(SourceError::Compile)?;
455    }
456    let mut program = compiler
457        .compile_program(&stmts)
458        .map_err(SourceError::Compile)?;
459    program.local_count = program.local_count.max(locals);
460    program.imports = runtime_import_functions
461        .iter()
462        .map(|func| HostImport {
463            name: func.name.clone(),
464            arity: func.arity,
465            return_type: func.return_type,
466        })
467        .collect();
468    let runtime_locals = program.local_count;
469    Ok(CompiledProgram {
470        program,
471        locals: runtime_locals,
472        functions: visible_runtime_import_functions,
473    })
474}
475
476#[derive(Clone, Debug)]
477struct StrictSlotSite {
478    name: String,
479    kind: &'static str,
480    line: Option<u32>,
481    source_name: Option<String>,
482}
483
484fn reject_strict_unknown_annotations(parsed: &FrontendIr) -> Result<(), ParseError> {
485    let Some(span) = parsed.unknown_type_spans.first().copied() else {
486        return Ok(());
487    };
488    Err(ParseError {
489        line: 1,
490        message:
491            "RustScript requires concrete compile-time types; 'unknown' annotations are not allowed"
492                .to_string(),
493        span: Some(span),
494        code: Some("E_STRICT_UNKNOWN_TYPE".to_string()),
495    })
496}
497
498fn enforce_strict_rustscript_type_resolution(
499    parsed: &FrontendIr,
500    type_info: &typing::TypeInferenceResult,
501) -> Result<(), CompileError> {
502    for schema in parsed.struct_schemas.values() {
503        if schema_is_fully_known(&schema.body_schema) {
504            continue;
505        }
506        return Err(CompileError::StrictTypingRequired {
507            line: None,
508            source_name: None,
509            detail: format!(
510                "struct '{}' contains non-concrete field types; RustScript requires concrete schemas",
511                schema.name
512            ),
513        });
514    }
515
516    let function_decl_lines = collect_function_decl_lines(&parsed.stmts);
517    for decl in &parsed.functions {
518        if let Some(schema) = decl.return_schema.as_ref()
519            && !schema_is_fully_known(schema)
520        {
521            return Err(CompileError::StrictTypingRequired {
522                line: function_decl_lines.get(&decl.index).copied(),
523                source_name: parsed.function_sources.get(&decl.index).cloned(),
524                detail: format!(
525                    "function '{}' uses a non-concrete return schema; RustScript requires concrete return types",
526                    decl.name
527                ),
528            });
529        }
530    }
531
532    for (slot, site) in collect_strict_slot_sites(parsed) {
533        if slot_is_fully_typed(slot, type_info) {
534            continue;
535        }
536        return Err(CompileError::StrictTypingRequired {
537            line: site.line,
538            source_name: site.source_name,
539            detail: format!(
540                "{} '{}' does not resolve to a concrete compile-time type in RustScript",
541                site.kind, site.name
542            ),
543        });
544    }
545
546    Ok(())
547}
548
549fn slot_is_fully_typed(slot: LocalSlot, type_info: &typing::TypeInferenceResult) -> bool {
550    let slot_index = usize::from(slot);
551    if type_info
552        .callable_slots
553        .get(slot_index)
554        .copied()
555        .unwrap_or(false)
556    {
557        return true;
558    }
559    if let Some(schema) = type_info
560        .local_schemas
561        .get(slot_index)
562        .and_then(|schema| schema.as_ref())
563    {
564        return schema_is_fully_known(schema);
565    }
566    type_info.local_types.get(slot_index).copied() != Some(crate::ValueType::Unknown)
567}
568
569fn schema_is_fully_known(schema: &TypeSchema) -> bool {
570    match schema {
571        TypeSchema::Unknown => false,
572        TypeSchema::Null
573        | TypeSchema::Int
574        | TypeSchema::Float
575        | TypeSchema::Number
576        | TypeSchema::Bool
577        | TypeSchema::String
578        | TypeSchema::Bytes
579        | TypeSchema::GenericParam(_) => true,
580        TypeSchema::Optional(inner) => schema_is_fully_known(inner),
581        TypeSchema::Named(_, type_args) => type_args.iter().all(schema_is_fully_known),
582        TypeSchema::Array(item) | TypeSchema::Map(item) => schema_is_fully_known(item),
583        TypeSchema::ArrayTuple(items) => items.iter().all(schema_is_fully_known),
584        TypeSchema::ArrayTupleRest { prefix, rest } => {
585            prefix.iter().all(schema_is_fully_known) && schema_is_fully_known(rest)
586        }
587        TypeSchema::Object(fields) => fields.values().all(schema_is_fully_known),
588        TypeSchema::Callable { params, result } => {
589            params.iter().all(schema_is_fully_known) && schema_is_fully_known(result)
590        }
591    }
592}
593
594fn collect_strict_slot_sites(parsed: &FrontendIr) -> Vec<(LocalSlot, StrictSlotSite)> {
595    let mut sites = Vec::new();
596    let local_debug_ranges = collect_local_debug_ranges(&parsed.stmts, &parsed.function_impls);
597    let local_source_names = collect_local_source_names(parsed);
598    for (name, slot) in &parsed.local_bindings {
599        let line = local_debug_ranges
600            .get(slot)
601            .and_then(|range| range.declared_line);
602        sites.push((
603            *slot,
604            StrictSlotSite {
605                name: name.clone(),
606                kind: "local",
607                line,
608                source_name: local_source_names.get(slot).cloned().flatten(),
609            },
610        ));
611    }
612
613    let function_decl_lines = collect_function_decl_lines(&parsed.stmts);
614    for decl in &parsed.functions {
615        let Some(function_impl) = parsed.function_impls.get(&decl.index) else {
616            continue;
617        };
618        for (name, slot) in decl.args.iter().zip(function_impl.param_slots.iter()) {
619            sites.push((
620                *slot,
621                StrictSlotSite {
622                    name: name.clone(),
623                    kind: "parameter",
624                    line: function_decl_lines.get(&decl.index).copied(),
625                    source_name: parsed.function_sources.get(&decl.index).cloned(),
626                },
627            ));
628        }
629    }
630
631    sites.sort_by_key(|(slot, _)| *slot);
632    sites
633}
634
635fn collect_local_source_names(parsed: &FrontendIr) -> HashMap<LocalSlot, Option<String>> {
636    let mut out = HashMap::new();
637    for (index, stmt) in parsed.stmts.iter().enumerate() {
638        let source_name = parsed
639            .stmt_sources
640            .get(index)
641            .and_then(|source| source.as_deref());
642        record_local_source_names(std::slice::from_ref(stmt), source_name, &mut out);
643    }
644    for decl in &parsed.functions {
645        let Some(function_impl) = parsed.function_impls.get(&decl.index) else {
646            continue;
647        };
648        let source_name = parsed.function_sources.get(&decl.index).map(String::as_str);
649        record_local_source_names(&function_impl.body_stmts, source_name, &mut out);
650    }
651    out
652}
653
654fn record_local_source_names(
655    stmts: &[Stmt],
656    source_name: Option<&str>,
657    out: &mut HashMap<LocalSlot, Option<String>>,
658) {
659    let source_name = source_name.map(str::to_string);
660    for stmt in stmts {
661        match stmt {
662            Stmt::Let { index, .. } => {
663                out.entry(*index).or_insert_with(|| source_name.clone());
664            }
665            Stmt::IfElse {
666                then_branch,
667                else_branch,
668                ..
669            } => {
670                record_local_source_names(then_branch, source_name.as_deref(), out);
671                record_local_source_names(else_branch, source_name.as_deref(), out);
672            }
673            Stmt::For {
674                init, post, body, ..
675            } => {
676                record_local_source_names(
677                    std::slice::from_ref(init.as_ref()),
678                    source_name.as_deref(),
679                    out,
680                );
681                record_local_source_names(
682                    std::slice::from_ref(post.as_ref()),
683                    source_name.as_deref(),
684                    out,
685                );
686                record_local_source_names(body, source_name.as_deref(), out);
687            }
688            Stmt::While { body, .. } => {
689                record_local_source_names(body, source_name.as_deref(), out);
690            }
691            Stmt::Noop { .. }
692            | Stmt::Assign { .. }
693            | Stmt::ClosureLet { .. }
694            | Stmt::FuncDecl { .. }
695            | Stmt::Expr { .. }
696            | Stmt::Break { .. }
697            | Stmt::Continue { .. }
698            | Stmt::Drop { .. } => {}
699        }
700    }
701}
702
703pub fn compile_source(source: &str) -> Result<CompiledProgram, SourceError> {
704    compile_source_with_flavor(source, SourceFlavor::RustScript)
705}
706
707pub fn lint_trailing_function_return_semicolons(
708    source: &str,
709    flavor: SourceFlavor,
710) -> Result<Vec<ParseError>, ParseError> {
711    let Some(dialect) =
712        frontends::parser_dialect_for_flavor(flavor, &CompileSourceFileOptions::default())
713    else {
714        return Ok(Vec::new());
715    };
716    parser::lint_trailing_function_return_semicolons(source, 0, dialect)
717}
718
719pub fn lint_unknown_type_annotations(
720    source: &str,
721    flavor: SourceFlavor,
722) -> Result<Vec<crate::compiler::source_map::Span>, SourceError> {
723    let mut source_map = SourceMap::new();
724    let source_id = source_map.add_source("<source>", source.to_string());
725    let parsed = frontends::parse_source(source, flavor, &CompileSourceFileOptions::default())
726        .map_err(|err| {
727            SourceError::Parse(err.with_line_span_from_source(&source_map, source_id))
728        })?;
729    Ok(parsed.unknown_type_spans)
730}
731
732pub fn lint_unknown_inferred_local_types(
733    source: &str,
734    flavor: SourceFlavor,
735) -> Result<Vec<UnknownInferredLocal>, SourceError> {
736    lint_unknown_inferred_local_types_impl(source, flavor)
737}
738
739pub fn collect_inferred_local_type_hints(
740    source: &str,
741    flavor: SourceFlavor,
742) -> Result<Vec<InferredLocalTypeHint>, SourceError> {
743    collect_inferred_local_type_hints_impl(source, flavor)
744}
745
746pub fn collect_inferred_local_type_hints_with_options(
747    source: &str,
748    flavor: SourceFlavor,
749    options: CompileSourceFileOptions,
750) -> Result<Vec<InferredLocalTypeHint>, SourcePathError> {
751    let source_owned = source.to_string();
752    run_with_compiler_stack(move || {
753        collect_inferred_local_type_hints_with_options_impl(&source_owned, flavor, &options)
754    })
755}
756
757pub fn collect_inferred_local_type_hints_at_path_with_options(
758    path: impl AsRef<Path>,
759    source: &str,
760    flavor: SourceFlavor,
761    options: CompileSourceFileOptions,
762) -> Result<Vec<InferredLocalTypeHint>, SourcePathError> {
763    let path = path.as_ref().to_path_buf();
764    let source_owned = source.to_string();
765    run_with_compiler_stack(move || {
766        collect_inferred_local_type_hints_at_path_with_options_impl(
767            &path,
768            &source_owned,
769            flavor,
770            &options,
771        )
772    })
773}
774
775pub fn lint_unknown_inferred_local_types_with_options(
776    source: &str,
777    flavor: SourceFlavor,
778    options: CompileSourceFileOptions,
779) -> Result<Vec<UnknownInferredLocal>, SourcePathError> {
780    let source_owned = source.to_string();
781    run_with_compiler_stack(move || {
782        lint_unknown_inferred_local_types_with_options_impl(&source_owned, flavor, &options)
783    })
784}
785
786pub fn lint_unknown_inferred_local_types_at_path_with_options(
787    path: impl AsRef<Path>,
788    source: &str,
789    flavor: SourceFlavor,
790    options: CompileSourceFileOptions,
791) -> Result<Vec<UnknownInferredLocal>, SourcePathError> {
792    let path = path.as_ref().to_path_buf();
793    let source_owned = source.to_string();
794    run_with_compiler_stack(move || {
795        lint_unknown_inferred_local_types_at_path_with_options_impl(
796            &path,
797            &source_owned,
798            flavor,
799            &options,
800        )
801    })
802}
803
804fn lint_unknown_inferred_local_types_impl(
805    source: &str,
806    flavor: SourceFlavor,
807) -> Result<Vec<UnknownInferredLocal>, SourceError> {
808    let mut source_map = SourceMap::new();
809    let source_id = source_map.add_source("<source>", source.to_string());
810    let parsed = frontends::parse_source(source, flavor, &CompileSourceFileOptions::default())
811        .map_err(|err| {
812            SourceError::Parse(err.with_line_span_from_source(&source_map, source_id))
813        })?;
814    Ok(collect_unknown_inferred_local_types(
815        &source_map,
816        source_id,
817        parsed,
818    ))
819}
820
821fn collect_inferred_local_type_hints_impl(
822    source: &str,
823    flavor: SourceFlavor,
824) -> Result<Vec<InferredLocalTypeHint>, SourceError> {
825    let mut source_map = SourceMap::new();
826    let source_id = source_map.add_source("<source>", source.to_string());
827    let parsed = frontends::parse_source(source, flavor, &CompileSourceFileOptions::default())
828        .map_err(|err| {
829            SourceError::Parse(err.with_line_span_from_source(&source_map, source_id))
830        })?;
831    Ok(collect_named_local_type_hints(parsed))
832}
833
834fn lint_unknown_inferred_local_types_with_options_impl(
835    source: &str,
836    flavor: SourceFlavor,
837    options: &CompileSourceFileOptions,
838) -> Result<Vec<UnknownInferredLocal>, SourcePathError> {
839    if !options.has_module_overrides() && !options.has_source_plugins() {
840        return lint_unknown_inferred_local_types_impl(source, flavor)
841            .map_err(SourcePathError::Source);
842    }
843
844    let path = virtual_inmemory_entry_path(flavor);
845    lint_unknown_inferred_local_types_at_path_with_options_impl(&path, source, flavor, options)
846}
847
848fn collect_inferred_local_type_hints_with_options_impl(
849    source: &str,
850    flavor: SourceFlavor,
851    options: &CompileSourceFileOptions,
852) -> Result<Vec<InferredLocalTypeHint>, SourcePathError> {
853    if !options.has_module_overrides() && !options.has_source_plugins() {
854        return collect_inferred_local_type_hints_impl(source, flavor)
855            .map_err(SourcePathError::Source);
856    }
857
858    let path = virtual_inmemory_entry_path(flavor);
859    collect_inferred_local_type_hints_at_path_with_options_impl(&path, source, flavor, options)
860}
861
862fn lint_unknown_inferred_local_types_at_path_with_options_impl(
863    path: &Path,
864    source: &str,
865    flavor: SourceFlavor,
866    options: &CompileSourceFileOptions,
867) -> Result<Vec<UnknownInferredLocal>, SourcePathError> {
868    let mut source_map = SourceMap::new();
869    let source_id = source_map.add_source(path.display().to_string(), source.to_string());
870    let (_root_parse_source, units) = load_units_for_source_file(path, flavor, source, options)?;
871    let parsed = units
872        .into_iter()
873        .last()
874        .map(|unit| unit.parsed)
875        .expect("root parsed unit should always be present");
876    Ok(collect_unknown_inferred_local_types(
877        &source_map,
878        source_id,
879        parsed,
880    ))
881}
882
883fn collect_inferred_local_type_hints_at_path_with_options_impl(
884    path: &Path,
885    source: &str,
886    flavor: SourceFlavor,
887    options: &CompileSourceFileOptions,
888) -> Result<Vec<InferredLocalTypeHint>, SourcePathError> {
889    let (_root_parse_source, units) = load_units_for_source_file(path, flavor, source, options)?;
890    let parsed = units
891        .into_iter()
892        .last()
893        .map(|unit| unit.parsed)
894        .expect("root parsed unit should always be present");
895    Ok(collect_named_local_type_hints(parsed))
896}
897
898fn collect_unknown_inferred_local_types(
899    source_map: &SourceMap,
900    source_id: u32,
901    parsed: FrontendIr,
902) -> Vec<UnknownInferredLocal> {
903    let local_debug_ranges = collect_local_debug_ranges(&parsed.stmts, &parsed.function_impls);
904    let parsed = typing::legalize_builtins_and_bind_types(parsed, TypingMode::DynamicHints, &[]);
905    let type_info = typing::infer_types(&parsed, TypingMode::DynamicHints, &[]);
906
907    let mut warnings = Vec::new();
908    for (name, slot) in &parsed.local_bindings {
909        let Some(range) = local_debug_ranges.get(slot) else {
910            continue;
911        };
912        let Some(line_u32) = range.declared_line else {
913            continue;
914        };
915        let slot_index = usize::from(*slot);
916        if type_info
917            .callable_slots
918            .get(slot_index)
919            .copied()
920            .unwrap_or(false)
921        {
922            continue;
923        }
924        if type_info
925            .local_schema_labels
926            .get(slot_index)
927            .and_then(|label| label.as_ref())
928            .is_some_and(|label| label != "unknown")
929        {
930            continue;
931        }
932        if type_info.local_types.get(slot_index) != Some(&crate::ValueType::Unknown) {
933            continue;
934        }
935        let line = usize::try_from(line_u32).unwrap_or(usize::MAX);
936        warnings.push(UnknownInferredLocal {
937            name: name.clone(),
938            line,
939            span: find_local_name_span(source_map, source_id, line, name)
940                .or_else(|| source_map.line_span(source_id, line)),
941        });
942    }
943    warnings
944}
945
946fn collect_named_local_type_hints(parsed: FrontendIr) -> Vec<InferredLocalTypeHint> {
947    let slot_ranges = collect_local_debug_ranges(&parsed.stmts, &parsed.function_impls);
948    let function_decl_lines = collect_function_decl_lines(&parsed.stmts);
949    let parsed = typing::legalize_builtins_and_bind_types(parsed, TypingMode::DynamicHints, &[]);
950    let type_info = typing::infer_types(&parsed, TypingMode::DynamicHints, &[]);
951
952    let mut hints = Vec::new();
953    for (name, slot) in &parsed.local_bindings {
954        hints.push(InferredLocalTypeHint {
955            name: name.clone(),
956            inferred_type: inferred_slot_type_name(&type_info, *slot),
957            declared_line: slot_ranges.get(slot).and_then(|range| range.declared_line),
958            last_line: slot_ranges.get(slot).and_then(|range| range.last_line),
959        });
960    }
961
962    for decl in &parsed.functions {
963        let Some(function_impl) = parsed.function_impls.get(&decl.index) else {
964            continue;
965        };
966        let declared_line = function_decl_lines.get(&decl.index).copied();
967        let last_line = function_scope_last_line(function_impl).or(declared_line);
968        for (name, slot) in decl.args.iter().zip(function_impl.param_slots.iter()) {
969            hints.push(InferredLocalTypeHint {
970                name: name.clone(),
971                inferred_type: inferred_slot_type_name(&type_info, *slot),
972                declared_line: slot_ranges
973                    .get(slot)
974                    .and_then(|range| range.declared_line)
975                    .or(declared_line),
976                last_line: slot_ranges
977                    .get(slot)
978                    .and_then(|range| range.last_line)
979                    .or(last_line),
980            });
981        }
982    }
983
984    hints
985}
986
987fn inferred_slot_type_name(type_info: &typing::TypeInferenceResult, slot: LocalSlot) -> String {
988    let slot_index = usize::from(slot);
989    if type_info
990        .callable_slots
991        .get(slot_index)
992        .copied()
993        .unwrap_or(false)
994    {
995        return "function".to_string();
996    }
997    if let Some(label) = type_info
998        .local_schema_labels
999        .get(slot_index)
1000        .and_then(|label| label.as_ref())
1001        .filter(|label| label.as_str() != "unknown")
1002    {
1003        return label.clone();
1004    }
1005    value_type_name(
1006        type_info
1007            .local_types
1008            .get(slot_index)
1009            .copied()
1010            .unwrap_or(crate::ValueType::Unknown),
1011    )
1012    .to_string()
1013}
1014
1015fn value_type_name(value: crate::ValueType) -> &'static str {
1016    match value {
1017        crate::ValueType::Unknown => "unknown",
1018        crate::ValueType::Null => "null",
1019        crate::ValueType::Int => "int",
1020        crate::ValueType::Float => "float",
1021        crate::ValueType::Bool => "bool",
1022        crate::ValueType::String => "string",
1023        crate::ValueType::Bytes => "bytes",
1024        crate::ValueType::Array => "array",
1025        crate::ValueType::Map => "map",
1026        crate::ValueType::Callable => "callable",
1027    }
1028}
1029
1030fn collect_function_decl_lines(stmts: &[Stmt]) -> HashMap<u16, u32> {
1031    let mut lines = HashMap::new();
1032    record_function_decl_lines(stmts, &mut lines);
1033    lines
1034}
1035
1036fn record_function_decl_lines(stmts: &[Stmt], lines: &mut HashMap<u16, u32>) {
1037    for stmt in stmts {
1038        match stmt {
1039            Stmt::FuncDecl { index, line, .. } => {
1040                lines.insert(*index, *line);
1041            }
1042            Stmt::IfElse {
1043                then_branch,
1044                else_branch,
1045                ..
1046            } => {
1047                record_function_decl_lines(then_branch, lines);
1048                record_function_decl_lines(else_branch, lines);
1049            }
1050            Stmt::For {
1051                init, post, body, ..
1052            } => {
1053                record_function_decl_lines(std::slice::from_ref(init.as_ref()), lines);
1054                record_function_decl_lines(std::slice::from_ref(post.as_ref()), lines);
1055                record_function_decl_lines(body, lines);
1056            }
1057            Stmt::While { body, .. } => {
1058                record_function_decl_lines(body, lines);
1059            }
1060            Stmt::Noop { .. }
1061            | Stmt::Let { .. }
1062            | Stmt::Assign { .. }
1063            | Stmt::ClosureLet { .. }
1064            | Stmt::Expr { .. }
1065            | Stmt::Break { .. }
1066            | Stmt::Continue { .. }
1067            | Stmt::Drop { .. } => {}
1068        }
1069    }
1070}
1071
1072fn function_scope_last_line(function_impl: &FunctionImpl) -> Option<u32> {
1073    let stmt_last_line = function_impl.body_stmts.last().map(stmt_source_line);
1074    match stmt_last_line {
1075        Some(line) => Some(line.max(function_impl.body_expr_line)),
1076        None if function_impl.body_expr_line > 0 => Some(function_impl.body_expr_line),
1077        None => None,
1078    }
1079}
1080
1081fn find_local_name_span(
1082    source_map: &SourceMap,
1083    source_id: u32,
1084    line: usize,
1085    name: &str,
1086) -> Option<crate::compiler::source_map::Span> {
1087    let file = source_map.file(source_id)?;
1088    let line_range = file.line_span(line)?;
1089    let line_text = file.line_text(line)?;
1090    let mut search_start = 0usize;
1091    while let Some(relative) = line_text[search_start..].find(name) {
1092        let start = search_start + relative;
1093        let end = start + name.len();
1094        let prev_ok = start == 0
1095            || !line_text[..start]
1096                .chars()
1097                .next_back()
1098                .is_some_and(is_ident_char);
1099        let next_ok =
1100            end == line_text.len() || !line_text[end..].chars().next().is_some_and(is_ident_char);
1101        if prev_ok && next_ok {
1102            return Some(crate::compiler::source_map::Span::new(
1103                source_id,
1104                line_range.start + start,
1105                line_range.start + end,
1106            ));
1107        }
1108        search_start = end;
1109    }
1110    None
1111}
1112
1113fn is_ident_char(ch: char) -> bool {
1114    ch.is_ascii_alphanumeric() || ch == '_'
1115}
1116
1117pub fn compile_source_for_repl(source: &str) -> Result<CompiledProgram, SourceError> {
1118    compile_source_for_repl_with_locals(source, &[]).map(|compiled| compiled.compiled)
1119}
1120
1121pub fn compile_source_for_repl_with_locals(
1122    source: &str,
1123    predefined_locals: &[ReplLocalBinding],
1124) -> Result<CompiledReplProgram, SourceError> {
1125    let source_owned = source.to_string();
1126    let predefined_locals = predefined_locals.to_vec();
1127    run_with_compiler_stack(move || {
1128        compile_source_for_repl_with_locals_impl(&source_owned, &predefined_locals, &[])
1129    })
1130}
1131
1132pub fn compile_source_for_repl_with_state(
1133    source: &str,
1134    predefined_locals: &[ReplLocalState],
1135) -> Result<CompiledReplProgram, SourceError> {
1136    let source_owned = source.to_string();
1137    let predefined_locals = predefined_locals.to_vec();
1138    run_with_compiler_stack(move || {
1139        let bindings = predefined_locals
1140            .iter()
1141            .map(|state| state.binding.clone())
1142            .collect::<Vec<_>>();
1143        let moved_names = predefined_locals
1144            .iter()
1145            .filter(|state| state.moved)
1146            .map(|state| state.binding.name.clone())
1147            .collect::<Vec<_>>();
1148        compile_source_for_repl_with_locals_impl(&source_owned, &bindings, &moved_names)
1149    })
1150}
1151
1152pub fn compile_source_with_flavor(
1153    source: &str,
1154    flavor: SourceFlavor,
1155) -> Result<CompiledProgram, SourceError> {
1156    compile_source_with_flavor_and_behavior(source, flavor, CompileBehavior::DEFAULT)
1157}
1158
1159pub fn compile_source_with_flavor_and_options(
1160    source: &str,
1161    flavor: SourceFlavor,
1162    options: CompileSourceFileOptions,
1163) -> Result<CompiledProgram, SourcePathError> {
1164    let source_owned = source.to_string();
1165    run_with_compiler_stack(move || {
1166        compile_source_with_flavor_and_options_impl(&source_owned, flavor, &options)
1167    })
1168}
1169
1170pub fn compile_source_at_path_with_flavor_and_options(
1171    path: impl AsRef<Path>,
1172    source: &str,
1173    flavor: SourceFlavor,
1174    options: CompileSourceFileOptions,
1175) -> Result<CompiledProgram, SourcePathError> {
1176    let path = path.as_ref().to_path_buf();
1177    let source_owned = source.to_string();
1178    run_with_compiler_stack(move || {
1179        compile_source_at_path_with_flavor_and_options_impl(&path, &source_owned, flavor, &options)
1180    })
1181}
1182
1183fn compile_source_with_flavor_and_behavior(
1184    source: &str,
1185    flavor: SourceFlavor,
1186    behavior: CompileBehavior,
1187) -> Result<CompiledProgram, SourceError> {
1188    let owned_source = source.to_string();
1189    run_with_compiler_stack(move || {
1190        compile_source_with_flavor_impl(&owned_source, flavor, behavior)
1191    })
1192}
1193
1194fn compile_source_for_repl_with_locals_impl(
1195    source: &str,
1196    predefined_locals: &[ReplLocalBinding],
1197    moved_names: &[String],
1198) -> Result<CompiledReplProgram, SourceError> {
1199    let mut source_map = SourceMap::new();
1200    let source_id = source_map.add_source("<source>", source.to_string());
1201    // REPL parsing/compiler entry state is separate from normal program compilation so
1202    // persisted locals do not leak into the generic frontend or IR surface.
1203    let parsed =
1204        frontends::parse_rustscript_repl_source(source, predefined_locals).map_err(|err| {
1205            SourceError::Parse(err.with_line_span_from_source(&source_map, source_id))
1206        })?;
1207    let entry_local_types = build_entry_local_types(&parsed.ir, predefined_locals);
1208    let entry_availability =
1209        build_entry_local_availability(&parsed.ir, predefined_locals, moved_names);
1210    let compiled = match compile_parsed_output_with_entry_locals(
1211        source.to_string(),
1212        parsed.ir,
1213        &entry_availability,
1214        &entry_local_types,
1215        CompileBehavior::REPL,
1216        TypingMode::StrictRustScript,
1217        true,
1218    ) {
1219        Err(SourceError::Parse(err)) => Err(SourceError::Parse(
1220            err.with_line_span_from_source(&source_map, source_id),
1221        )),
1222        other => other,
1223    }?;
1224    Ok(CompiledReplProgram {
1225        compiled,
1226        bindings: parsed.bindings,
1227    })
1228}
1229
1230fn build_entry_local_availability(
1231    parsed: &FrontendIr,
1232    predefined_locals: &[ReplLocalBinding],
1233    moved_names: &[String],
1234) -> Vec<lifetime::EntryLocalAvailability> {
1235    let predefined_by_name = predefined_locals
1236        .iter()
1237        .map(|binding| (binding.name.as_str(), binding))
1238        .collect::<HashMap<_, _>>();
1239    parsed
1240        .local_bindings
1241        .iter()
1242        .filter_map(|(name, slot)| {
1243            let binding = predefined_by_name.get(name.as_str())?;
1244            let schema = binding
1245                .schema
1246                .as_ref()
1247                .map(|schema| schema.split_optional().0);
1248            let copyable = matches!(
1249                schema,
1250                Some(
1251                    TypeSchema::Null
1252                        | TypeSchema::Int
1253                        | TypeSchema::Float
1254                        | TypeSchema::Number
1255                        | TypeSchema::Bool
1256                )
1257            );
1258            let movable = matches!(schema, Some(TypeSchema::String | TypeSchema::Bytes));
1259            Some(lifetime::EntryLocalAvailability {
1260                slot: *slot,
1261                copyable,
1262                movable,
1263                moved: moved_names.iter().any(|moved| moved == name),
1264            })
1265        })
1266        .collect()
1267}
1268
1269fn build_entry_local_types(
1270    parsed: &FrontendIr,
1271    predefined_locals: &[ReplLocalBinding],
1272) -> Vec<typing::EntryLocalType> {
1273    let predefined_by_name = predefined_locals
1274        .iter()
1275        .map(|binding| (binding.name.as_str(), binding))
1276        .collect::<HashMap<_, _>>();
1277    parsed
1278        .local_bindings
1279        .iter()
1280        .filter_map(|(name, slot)| {
1281            let binding = predefined_by_name.get(name.as_str())?;
1282            let (schema, schema_optional) = binding
1283                .schema
1284                .clone()
1285                .map(|schema| schema.split_optional())
1286                .map(|(schema, optional)| (Some(schema), optional))
1287                .unwrap_or((None, false));
1288            Some(typing::EntryLocalType {
1289                slot: *slot,
1290                schema,
1291                optional: binding.optional || schema_optional,
1292            })
1293        })
1294        .collect()
1295}
1296
1297fn compile_source_with_flavor_impl(
1298    source: &str,
1299    flavor: SourceFlavor,
1300    behavior: CompileBehavior,
1301) -> Result<CompiledProgram, SourceError> {
1302    let mut source_map = SourceMap::new();
1303    let source_id = source_map.add_source("<source>", source.to_string());
1304    let parsed = frontends::parse_source(source, flavor, &CompileSourceFileOptions::default())
1305        .map_err(|err| {
1306            SourceError::Parse(err.with_line_span_from_source(&source_map, source_id))
1307        })?;
1308    match compile_parsed_output(
1309        source.to_string(),
1310        parsed,
1311        behavior,
1312        TypingMode::for_flavor(flavor),
1313        matches!(flavor, SourceFlavor::RustScript),
1314    ) {
1315        Err(SourceError::Parse(err)) => Err(SourceError::Parse(
1316            err.with_line_span_from_source(&source_map, source_id),
1317        )),
1318        other => other,
1319    }
1320}
1321
1322fn compile_source_with_flavor_and_options_impl(
1323    source: &str,
1324    flavor: SourceFlavor,
1325    options: &CompileSourceFileOptions,
1326) -> Result<CompiledProgram, SourcePathError> {
1327    if !options.has_module_overrides() && !options.has_source_plugins() {
1328        return compile_source_with_flavor_impl(source, flavor, CompileBehavior::DEFAULT)
1329            .map_err(SourcePathError::Source);
1330    }
1331
1332    let path = virtual_inmemory_entry_path(flavor);
1333    let (_root_parse_source, units) = load_units_for_source_file(&path, flavor, source, options)?;
1334    let merged = merge_units(units)?;
1335    compile_parsed_output(
1336        source.to_string(),
1337        merged,
1338        CompileBehavior::DEFAULT,
1339        TypingMode::for_flavor(flavor),
1340        matches!(flavor, SourceFlavor::RustScript),
1341    )
1342    .map_err(SourcePathError::Source)
1343}
1344
1345fn compile_source_at_path_with_flavor_and_options_impl(
1346    path: &Path,
1347    source: &str,
1348    flavor: SourceFlavor,
1349    options: &CompileSourceFileOptions,
1350) -> Result<CompiledProgram, SourcePathError> {
1351    let (_root_parse_source, units) = load_units_for_source_file(path, flavor, source, options)?;
1352    let merged = merge_units(units)?;
1353    compile_parsed_output(
1354        source.to_string(),
1355        merged,
1356        CompileBehavior::DEFAULT,
1357        TypingMode::for_flavor(flavor),
1358        matches!(flavor, SourceFlavor::RustScript),
1359    )
1360    .map_err(SourcePathError::Source)
1361}
1362
1363fn virtual_inmemory_entry_path(flavor: SourceFlavor) -> PathBuf {
1364    let ext = match flavor {
1365        SourceFlavor::RustScript => "rss",
1366        SourceFlavor::JavaScript => "js",
1367        SourceFlavor::Lua => "lua",
1368    };
1369    PathBuf::from("__pd_vm_inmemory__").join(format!("main.{ext}"))
1370}
1371
1372pub fn compile_source_file(path: impl AsRef<Path>) -> Result<CompiledProgram, SourcePathError> {
1373    compile_source_file_with_options(path, CompileSourceFileOptions::default())
1374}
1375
1376pub fn compile_source_file_with_options(
1377    path: impl AsRef<Path>,
1378    options: CompileSourceFileOptions,
1379) -> Result<CompiledProgram, SourcePathError> {
1380    let path = path.as_ref().to_path_buf();
1381    run_with_compiler_stack(move || compile_source_file_impl(&path, &options))
1382}
1383
1384fn compile_source_file_impl(
1385    path: &Path,
1386    options: &CompileSourceFileOptions,
1387) -> Result<CompiledProgram, SourcePathError> {
1388    let flavor = SourceFlavor::from_path_with_options(path, options)?;
1389    let source_raw = std::fs::read_to_string(path)?;
1390    let (_root_parse_source, units) =
1391        load_units_for_source_file(path, flavor, &source_raw, options)?;
1392    let merged = merge_units(units)?;
1393    compile_parsed_output(
1394        source_raw,
1395        merged,
1396        CompileBehavior::DEFAULT,
1397        TypingMode::for_flavor(flavor),
1398        matches!(flavor, SourceFlavor::RustScript),
1399    )
1400    .map_err(SourcePathError::Source)
1401}
1402
1403fn run_with_compiler_stack<T, F>(f: F) -> T
1404where
1405    T: Send + 'static,
1406    F: FnOnce() -> T + Send + 'static,
1407{
1408    #[cfg(target_arch = "wasm32")]
1409    {
1410        f()
1411    }
1412
1413    #[cfg(not(target_arch = "wasm32"))]
1414    {
1415        const COMPILER_STACK_SIZE: usize = 32 * 1024 * 1024;
1416        let handle = std::thread::Builder::new()
1417            .name("pd-vm-compile".to_string())
1418            .stack_size(COMPILER_STACK_SIZE)
1419            .spawn(f)
1420            .expect("failed to spawn compiler thread");
1421        match handle.join() {
1422            Ok(value) => value,
1423            Err(payload) => std::panic::resume_unwind(payload),
1424        }
1425    }
1426}