Skip to main content

vm/compiler/
pipeline.rs

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