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