Skip to main content

harn_vm/compiler/
state.rs

1use std::collections::BTreeMap;
2use std::rc::Rc;
3
4use harn_parser::{Node, SNode, ShapeField, TypeExpr, TypedParam};
5
6use crate::chunk::{Chunk, CompiledFunction, Constant, Op};
7use crate::value::VmValue;
8
9use super::error::CompileError;
10use super::yield_scan::body_contains_yield;
11use super::{peel_node, Compiler, CompilerOptions, FinallyEntry};
12
13impl Compiler {
14    pub fn new() -> Self {
15        Self::with_options(CompilerOptions::from_env())
16    }
17
18    pub fn with_options(options: CompilerOptions) -> Self {
19        Self {
20            options,
21            chunk: Chunk::new(),
22            line: 1,
23            column: 1,
24            enum_names: std::collections::HashSet::new(),
25            struct_layouts: std::collections::HashMap::new(),
26            interface_methods: std::collections::HashMap::new(),
27            loop_stack: Vec::new(),
28            handler_depth: 0,
29            finally_bodies: Vec::new(),
30            temp_counter: 0,
31            scope_depth: 0,
32            type_aliases: std::collections::HashMap::new(),
33            type_scopes: vec![std::collections::HashMap::new()],
34            local_scopes: vec![std::collections::HashMap::new()],
35            module_level: true,
36        }
37    }
38
39    /// Compiler instance for a nested function-like body (fn, closure,
40    /// tool, parallel arm, etc.). Differs from `new()` only in that
41    /// `module_level` starts false — `try*` is allowed inside.
42    pub(super) fn for_nested_body(options: CompilerOptions) -> Self {
43        let mut c = Self::with_options(options);
44        c.module_level = false;
45        c
46    }
47
48    pub(super) fn nested_body(&self) -> Self {
49        Self::for_nested_body(self.options)
50    }
51
52    pub(super) fn nominal_type_names(&self) -> Vec<String> {
53        let mut names: Vec<String> = self
54            .struct_layouts
55            .keys()
56            .chain(self.enum_names.iter())
57            .cloned()
58            .collect();
59        names.sort();
60        names.dedup();
61        names
62    }
63
64    /// Populate `type_aliases` from a program's top-level `type T = ...`
65    /// declarations so later lowerings can resolve alias names to their
66    /// canonical `TypeExpr`.
67    pub(super) fn collect_type_aliases(&mut self, program: &[SNode]) {
68        for sn in program {
69            if let Node::TypeDecl {
70                name,
71                type_expr,
72                type_params: _,
73            } = &sn.node
74            {
75                self.type_aliases.insert(name.clone(), type_expr.clone());
76            }
77        }
78    }
79
80    /// Expand a single layer of alias references. Returns the resolved
81    /// `TypeExpr` with all `Named(T)` nodes whose `T` is a known alias
82    /// replaced by the alias's body.
83    pub(super) fn expand_alias(&self, ty: &TypeExpr) -> TypeExpr {
84        match ty {
85            TypeExpr::Named(name) => {
86                if let Some(target) = self.type_aliases.get(name) {
87                    self.expand_alias(target)
88                } else {
89                    TypeExpr::Named(name.clone())
90                }
91            }
92            TypeExpr::Union(types) => {
93                TypeExpr::Union(types.iter().map(|t| self.expand_alias(t)).collect())
94            }
95            TypeExpr::Intersection(types) => {
96                TypeExpr::Intersection(types.iter().map(|t| self.expand_alias(t)).collect())
97            }
98            TypeExpr::Shape(fields) => TypeExpr::Shape(
99                fields
100                    .iter()
101                    .map(|field| ShapeField {
102                        name: field.name.clone(),
103                        type_expr: self.expand_alias(&field.type_expr),
104                        optional: field.optional,
105                    })
106                    .collect(),
107            ),
108            TypeExpr::List(inner) => TypeExpr::List(Box::new(self.expand_alias(inner))),
109            TypeExpr::Iter(inner) => TypeExpr::Iter(Box::new(self.expand_alias(inner))),
110            TypeExpr::Generator(inner) => TypeExpr::Generator(Box::new(self.expand_alias(inner))),
111            TypeExpr::Stream(inner) => TypeExpr::Stream(Box::new(self.expand_alias(inner))),
112            TypeExpr::DictType(k, v) => TypeExpr::DictType(
113                Box::new(self.expand_alias(k)),
114                Box::new(self.expand_alias(v)),
115            ),
116            TypeExpr::FnType {
117                params,
118                return_type,
119            } => TypeExpr::FnType {
120                params: params.iter().map(|p| self.expand_alias(p)).collect(),
121                return_type: Box::new(self.expand_alias(return_type)),
122            },
123            TypeExpr::Applied { name, args } => TypeExpr::Applied {
124                name: name.clone(),
125                args: args.iter().map(|a| self.expand_alias(a)).collect(),
126            },
127            TypeExpr::Never => TypeExpr::Never,
128            TypeExpr::LitString(s) => TypeExpr::LitString(s.clone()),
129            TypeExpr::LitInt(v) => TypeExpr::LitInt(*v),
130            TypeExpr::Owned(inner) => TypeExpr::Owned(Box::new(self.expand_alias(inner))),
131        }
132    }
133
134    /// Build the JSON-Schema VmValue for a named type alias, or `None` if
135    /// the name is unknown or the alias cannot be lowered to a schema.
136    pub(super) fn schema_value_for_alias(&self, name: &str) -> Option<VmValue> {
137        let ty = self.type_aliases.get(name)?;
138        let expanded = self.expand_alias(ty);
139        Self::type_expr_to_schema_value(&expanded)
140    }
141
142    /// Schema-guard builtins that accept a schema as their second argument.
143    /// When callers pass a type-alias identifier here, the compiler lowers
144    /// it to the alias's JSON-Schema dict constant.
145    pub(super) fn is_schema_guard(name: &str) -> bool {
146        matches!(
147            name,
148            "schema_is"
149                | "schema_expect"
150                | "schema_parse"
151                | "schema_check"
152                | "is_type"
153                | "json_validate"
154        )
155    }
156
157    /// Check whether a dict-literal key node matches the given keyword
158    /// (identifier or string literal form).
159    pub(super) fn entry_key_is(key: &SNode, keyword: &str) -> bool {
160        matches!(
161            &key.node,
162            Node::Identifier(name) | Node::StringLiteral(name) | Node::RawStringLiteral(name)
163                if name == keyword
164        )
165    }
166
167    /// Compile a program (list of top-level nodes) into a Chunk.
168    /// Finds the entry pipeline and compiles its body, including inherited bodies.
169    pub fn compile(mut self, program: &[SNode]) -> Result<Chunk, CompileError> {
170        // Pre-scan so we can recognize EnumName.Variant as enum construction
171        // even when the enum is declared inside a pipeline.
172        Self::collect_enum_names(program, &mut self.enum_names);
173        self.enum_names.insert("Result".to_string());
174        Self::collect_struct_layouts(program, &mut self.struct_layouts);
175        Self::collect_interface_methods(program, &mut self.interface_methods);
176        self.collect_type_aliases(program);
177
178        for sn in program {
179            match &sn.node {
180                Node::ImportDecl { .. } | Node::SelectiveImport { .. } => {
181                    self.compile_node(sn)?;
182                }
183                _ => {}
184            }
185        }
186        let main = program
187            .iter()
188            .find(|sn| matches!(peel_node(sn), Node::Pipeline { name, .. } if name == "default"))
189            .or_else(|| {
190                program
191                    .iter()
192                    .find(|sn| matches!(peel_node(sn), Node::Pipeline { .. }))
193            });
194
195        // When a pipeline body produces a final value, that value flows
196        // out of `vm.execute()` so the CLI can map it to a process exit
197        // code (int → exit n, Result::Err(msg) → stderr+exit 1).
198        let mut pipeline_emits_value = false;
199        if let Some(sn) = main {
200            self.compile_top_level_declarations(program)?;
201            if let Node::Pipeline { body, extends, .. } = peel_node(sn) {
202                if let Some(parent_name) = extends {
203                    self.compile_parent_pipeline(program, parent_name)?;
204                }
205                let saved = std::mem::replace(&mut self.module_level, false);
206                self.compile_block(body)?;
207                self.module_level = saved;
208                pipeline_emits_value = true;
209            }
210        } else {
211            // Script mode: no pipeline found, treat top-level as implicit entry.
212            let top_level: Vec<&SNode> = program
213                .iter()
214                .filter(|sn| {
215                    !matches!(
216                        &sn.node,
217                        Node::ImportDecl { .. } | Node::SelectiveImport { .. }
218                    )
219                })
220                .collect();
221            for sn in &top_level {
222                self.compile_node(sn)?;
223                if Self::produces_value(&sn.node) {
224                    self.chunk.emit(Op::Pop, self.line);
225                }
226            }
227            // E4.1 entrypoint convention: a top-level `fn main(harness: Harness)`
228            // is invoked automatically with the runtime-provided `harness`
229            // global. The typechecker rejects every other signature with
230            // HARN-NAM-101 so we don't need to re-validate the shape here.
231            if Self::has_top_level_fn_main(program) {
232                let harness_name = self.chunk.add_constant(Constant::String("harness".into()));
233                self.chunk.emit_u16(Op::GetVar, harness_name, self.line);
234                self.emit_named_call("main", 1);
235                pipeline_emits_value = true;
236            }
237        }
238
239        for fb in self.all_pending_finallys() {
240            self.compile_finally_inline(&fb)?;
241        }
242        if !pipeline_emits_value {
243            self.chunk.emit(Op::Nil, self.line);
244        }
245        self.chunk.emit(Op::Return, self.line);
246        Ok(self.chunk)
247    }
248
249    /// True when the program declares a top-level `fn main(...)`. Drives the
250    /// auto-call wired by `compile()` for the new `main(harness: Harness)`
251    /// entrypoint convention.
252    fn has_top_level_fn_main(program: &[SNode]) -> bool {
253        program
254            .iter()
255            .any(|sn| matches!(peel_node(sn), Node::FnDecl { name, .. } if name == "main"))
256    }
257
258    /// Compile a specific named pipeline (for test runners).
259    pub fn compile_named(
260        mut self,
261        program: &[SNode],
262        pipeline_name: &str,
263    ) -> Result<Chunk, CompileError> {
264        Self::collect_enum_names(program, &mut self.enum_names);
265        self.enum_names.insert("Result".to_string());
266        Self::collect_struct_layouts(program, &mut self.struct_layouts);
267        Self::collect_interface_methods(program, &mut self.interface_methods);
268        self.collect_type_aliases(program);
269
270        for sn in program {
271            if matches!(
272                &sn.node,
273                Node::ImportDecl { .. } | Node::SelectiveImport { .. }
274            ) {
275                self.compile_node(sn)?;
276            }
277        }
278        let target = program.iter().find(
279            |sn| matches!(peel_node(sn), Node::Pipeline { name, .. } if name == pipeline_name),
280        );
281
282        if let Some(sn) = target {
283            self.compile_top_level_declarations(program)?;
284            if let Node::Pipeline { body, extends, .. } = peel_node(sn) {
285                if let Some(parent_name) = extends {
286                    self.compile_parent_pipeline(program, parent_name)?;
287                }
288                let saved = std::mem::replace(&mut self.module_level, false);
289                self.compile_block(body)?;
290                self.module_level = saved;
291            }
292        }
293
294        for fb in self.all_pending_finallys() {
295            self.compile_finally_inline(&fb)?;
296        }
297        self.chunk.emit(Op::Nil, self.line);
298        self.chunk.emit(Op::Return, self.line);
299        Ok(self.chunk)
300    }
301
302    /// Recursively compile parent pipeline bodies (for extends).
303    pub(super) fn compile_parent_pipeline(
304        &mut self,
305        program: &[SNode],
306        parent_name: &str,
307    ) -> Result<(), CompileError> {
308        let parent = program
309            .iter()
310            .find(|sn| matches!(&sn.node, Node::Pipeline { name, .. } if name == parent_name));
311        if let Some(sn) = parent {
312            if let Node::Pipeline { body, extends, .. } = &sn.node {
313                if let Some(grandparent) = extends {
314                    self.compile_parent_pipeline(program, grandparent)?;
315                }
316                for stmt in body {
317                    self.compile_node(stmt)?;
318                    if Self::produces_value(&stmt.node) {
319                        self.chunk.emit(Op::Pop, self.line);
320                    }
321                }
322            }
323        }
324        Ok(())
325    }
326
327    /// Emit bytecode preamble for default parameter values.
328    /// For each param with a default at index i, emits:
329    ///   GetArgc; PushInt (i+1); GreaterEqual; JumpIfTrue <skip>;
330    ///   [compile default expr]; DefLet param_name; <skip>:
331    pub(super) fn emit_default_preamble(
332        &mut self,
333        params: &[TypedParam],
334    ) -> Result<(), CompileError> {
335        for (i, param) in params.iter().enumerate() {
336            if let Some(default_expr) = &param.default_value {
337                self.chunk.emit(Op::GetArgc, self.line);
338                let threshold_idx = self.chunk.add_constant(Constant::Int((i + 1) as i64));
339                self.chunk.emit_u16(Op::Constant, threshold_idx, self.line);
340                self.chunk.emit(Op::GreaterEqual, self.line);
341                let skip_jump = self.chunk.emit_jump(Op::JumpIfTrue, self.line);
342                // JumpIfTrue doesn't pop its boolean operand.
343                self.chunk.emit(Op::Pop, self.line);
344                self.compile_node(default_expr)?;
345                self.emit_init_or_define_binding(&param.name, false);
346                let end_jump = self.chunk.emit_jump(Op::Jump, self.line);
347                self.chunk.patch_jump(skip_jump);
348                self.chunk.emit(Op::Pop, self.line);
349                self.chunk.patch_jump(end_jump);
350            }
351        }
352        Ok(())
353    }
354
355    /// Emit body-local type checks that call-site validation cannot cover.
356    /// Ordinary supplied arguments are validated by precomputed
357    /// [`crate::chunk::ParamSlot`] guards before the frame is entered. The
358    /// bytecode preamble still checks interface parameters, because interface
359    /// satisfaction depends on compiler-collected method metadata, and checks
360    /// defaulted schema parameters only when the caller omitted that argument.
361    pub(super) fn emit_type_checks(&mut self, params: &[TypedParam]) {
362        for (param_index, param) in params.iter().enumerate() {
363            if let Some(type_expr) = &param.type_expr {
364                let check_type = if param.rest {
365                    harn_parser::TypeExpr::List(Box::new(type_expr.clone()))
366                } else {
367                    type_expr.clone()
368                };
369
370                if let harn_parser::TypeExpr::Named(name) = &check_type {
371                    if let Some(methods) = self.interface_methods.get(name).cloned() {
372                        let fn_idx = self
373                            .chunk
374                            .add_constant(Constant::String("__assert_interface".into()));
375                        self.chunk.emit_u16(Op::Constant, fn_idx, self.line);
376                        self.emit_get_binding(&param.name);
377                        let name_idx = self
378                            .chunk
379                            .add_constant(Constant::String(param.name.clone()));
380                        self.chunk.emit_u16(Op::Constant, name_idx, self.line);
381                        let iface_idx = self.chunk.add_constant(Constant::String(name.clone()));
382                        self.chunk.emit_u16(Op::Constant, iface_idx, self.line);
383                        let methods_str = methods.join(",");
384                        let methods_idx = self.chunk.add_constant(Constant::String(methods_str));
385                        self.chunk.emit_u16(Op::Constant, methods_idx, self.line);
386                        self.chunk.emit_u8(Op::Call, 4, self.line);
387                        self.chunk.emit(Op::Pop, self.line);
388                        continue;
389                    }
390                }
391
392                if param.default_value.is_some() {
393                    if let Some(schema) = Self::type_expr_to_schema_value(&check_type) {
394                        self.emit_default_param_schema_check(param_index, param, &schema);
395                    }
396                }
397            }
398        }
399    }
400
401    fn emit_default_param_schema_check(
402        &mut self,
403        param_index: usize,
404        param: &TypedParam,
405        schema: &VmValue,
406    ) {
407        self.chunk.emit(Op::GetArgc, self.line);
408        let threshold_idx = self
409            .chunk
410            .add_constant(Constant::Int((param_index + 1) as i64));
411        self.chunk.emit_u16(Op::Constant, threshold_idx, self.line);
412        self.chunk.emit(Op::GreaterEqual, self.line);
413        let supplied_jump = self.chunk.emit_jump(Op::JumpIfTrue, self.line);
414        self.chunk.emit(Op::Pop, self.line);
415        self.emit_schema_assert_call(param, schema);
416        let end_jump = self.chunk.emit_jump(Op::Jump, self.line);
417        self.chunk.patch_jump(supplied_jump);
418        self.chunk.emit(Op::Pop, self.line);
419        self.chunk.patch_jump(end_jump);
420    }
421
422    fn emit_schema_assert_call(&mut self, param: &TypedParam, schema: &VmValue) {
423        let fn_idx = self
424            .chunk
425            .add_constant(Constant::String("__assert_schema".into()));
426        self.chunk.emit_u16(Op::Constant, fn_idx, self.line);
427        self.emit_get_binding(&param.name);
428        let name_idx = self
429            .chunk
430            .add_constant(Constant::String(param.name.clone()));
431        self.chunk.emit_u16(Op::Constant, name_idx, self.line);
432        self.emit_vm_value_literal(schema);
433        self.chunk.emit_u8(Op::Call, 3, self.line);
434        self.chunk.emit(Op::Pop, self.line);
435    }
436
437    pub(crate) fn type_expr_to_schema_value(type_expr: &harn_parser::TypeExpr) -> Option<VmValue> {
438        match type_expr {
439            harn_parser::TypeExpr::Named(name) => match name.as_str() {
440                "int" | "float" | "string" | "bool" | "list" | "dict" | "set" | "nil"
441                | "closure" | "bytes" => Some(VmValue::Dict(Rc::new(BTreeMap::from([(
442                    "type".to_string(),
443                    VmValue::String(Rc::from(name.as_str())),
444                )])))),
445                _ => None,
446            },
447            harn_parser::TypeExpr::Shape(fields) => {
448                let mut properties = BTreeMap::new();
449                let mut required = Vec::new();
450                for field in fields {
451                    let field_schema = Self::type_expr_to_schema_value(&field.type_expr)?;
452                    properties.insert(field.name.clone(), field_schema);
453                    if !field.optional {
454                        required.push(VmValue::String(Rc::from(field.name.as_str())));
455                    }
456                }
457                let mut out = BTreeMap::new();
458                out.insert("type".to_string(), VmValue::String(Rc::from("dict")));
459                out.insert("properties".to_string(), VmValue::Dict(Rc::new(properties)));
460                if !required.is_empty() {
461                    out.insert("required".to_string(), VmValue::List(Rc::new(required)));
462                }
463                Some(VmValue::Dict(Rc::new(out)))
464            }
465            harn_parser::TypeExpr::List(inner) => {
466                let mut out = BTreeMap::new();
467                out.insert("type".to_string(), VmValue::String(Rc::from("list")));
468                if let Some(item_schema) = Self::type_expr_to_schema_value(inner) {
469                    out.insert("items".to_string(), item_schema);
470                }
471                Some(VmValue::Dict(Rc::new(out)))
472            }
473            harn_parser::TypeExpr::DictType(key, value) => {
474                let mut out = BTreeMap::new();
475                out.insert("type".to_string(), VmValue::String(Rc::from("dict")));
476                if matches!(key.as_ref(), harn_parser::TypeExpr::Named(name) if name == "string") {
477                    if let Some(value_schema) = Self::type_expr_to_schema_value(value) {
478                        out.insert("additional_properties".to_string(), value_schema);
479                    }
480                }
481                Some(VmValue::Dict(Rc::new(out)))
482            }
483            harn_parser::TypeExpr::Union(members) => {
484                // Special-case unions of literals: emit as `enum: [...]`
485                // so the schema round-trips as canonical JSON Schema and
486                // is ACP-/OpenAPI-compatible. Mixed unions fall back to
487                // the `union:` key that validators recognize.
488                if !members.is_empty()
489                    && members
490                        .iter()
491                        .all(|m| matches!(m, harn_parser::TypeExpr::LitString(_)))
492                {
493                    let values = members
494                        .iter()
495                        .map(|m| match m {
496                            harn_parser::TypeExpr::LitString(s) => {
497                                VmValue::String(Rc::from(s.as_str()))
498                            }
499                            _ => unreachable!(),
500                        })
501                        .collect::<Vec<_>>();
502                    return Some(VmValue::Dict(Rc::new(BTreeMap::from([
503                        ("type".to_string(), VmValue::String(Rc::from("string"))),
504                        ("enum".to_string(), VmValue::List(Rc::new(values))),
505                    ]))));
506                }
507                if !members.is_empty()
508                    && members
509                        .iter()
510                        .all(|m| matches!(m, harn_parser::TypeExpr::LitInt(_)))
511                {
512                    let values = members
513                        .iter()
514                        .map(|m| match m {
515                            harn_parser::TypeExpr::LitInt(v) => VmValue::Int(*v),
516                            _ => unreachable!(),
517                        })
518                        .collect::<Vec<_>>();
519                    return Some(VmValue::Dict(Rc::new(BTreeMap::from([
520                        ("type".to_string(), VmValue::String(Rc::from("int"))),
521                        ("enum".to_string(), VmValue::List(Rc::new(values))),
522                    ]))));
523                }
524                let branches = members
525                    .iter()
526                    .filter_map(Self::type_expr_to_schema_value)
527                    .collect::<Vec<_>>();
528                if branches.is_empty() {
529                    None
530                } else {
531                    Some(VmValue::Dict(Rc::new(BTreeMap::from([(
532                        "union".to_string(),
533                        VmValue::List(Rc::new(branches)),
534                    )]))))
535                }
536            }
537            harn_parser::TypeExpr::Intersection(members) => {
538                // Encode `A & B` as JSON-Schema `allOf` (the runtime
539                // accepts the snake_case `all_of` key directly). The
540                // value must validate against every branch.
541                let branches = members
542                    .iter()
543                    .filter_map(Self::type_expr_to_schema_value)
544                    .collect::<Vec<_>>();
545                if branches.is_empty() {
546                    None
547                } else {
548                    Some(VmValue::Dict(Rc::new(BTreeMap::from([(
549                        "all_of".to_string(),
550                        VmValue::List(Rc::new(branches)),
551                    )]))))
552                }
553            }
554            harn_parser::TypeExpr::FnType { .. } => {
555                Some(VmValue::Dict(Rc::new(BTreeMap::from([(
556                    "type".to_string(),
557                    VmValue::String(Rc::from("closure")),
558                )]))))
559            }
560            harn_parser::TypeExpr::Applied { .. } => None,
561            harn_parser::TypeExpr::Iter(_)
562            | harn_parser::TypeExpr::Generator(_)
563            | harn_parser::TypeExpr::Stream(_) => None,
564            harn_parser::TypeExpr::Never => None,
565            harn_parser::TypeExpr::LitString(s) => Some(VmValue::Dict(Rc::new(BTreeMap::from([
566                ("type".to_string(), VmValue::String(Rc::from("string"))),
567                ("const".to_string(), VmValue::String(Rc::from(s.as_str()))),
568            ])))),
569            harn_parser::TypeExpr::LitInt(v) => Some(VmValue::Dict(Rc::new(BTreeMap::from([
570                ("type".to_string(), VmValue::String(Rc::from("int"))),
571                ("const".to_string(), VmValue::Int(*v)),
572            ])))),
573            harn_parser::TypeExpr::Owned(inner) => Self::type_expr_to_schema_value(inner),
574        }
575    }
576
577    pub(super) fn emit_vm_value_literal(&mut self, value: &VmValue) {
578        match value {
579            VmValue::String(text) => {
580                let idx = self.chunk.add_constant(Constant::String(text.to_string()));
581                self.chunk.emit_u16(Op::Constant, idx, self.line);
582            }
583            VmValue::Int(number) => {
584                let idx = self.chunk.add_constant(Constant::Int(*number));
585                self.chunk.emit_u16(Op::Constant, idx, self.line);
586            }
587            VmValue::Float(number) => {
588                let idx = self.chunk.add_constant(Constant::Float(*number));
589                self.chunk.emit_u16(Op::Constant, idx, self.line);
590            }
591            VmValue::Bool(value) => {
592                let idx = self.chunk.add_constant(Constant::Bool(*value));
593                self.chunk.emit_u16(Op::Constant, idx, self.line);
594            }
595            VmValue::Nil => self.chunk.emit(Op::Nil, self.line),
596            VmValue::List(items) => {
597                for item in items.iter() {
598                    self.emit_vm_value_literal(item);
599                }
600                self.chunk
601                    .emit_u16(Op::BuildList, items.len() as u16, self.line);
602            }
603            VmValue::Dict(entries) => {
604                for (key, item) in entries.iter() {
605                    let key_idx = self.chunk.add_constant(Constant::String(key.clone()));
606                    self.chunk.emit_u16(Op::Constant, key_idx, self.line);
607                    self.emit_vm_value_literal(item);
608                }
609                self.chunk
610                    .emit_u16(Op::BuildDict, entries.len() as u16, self.line);
611            }
612            _ => {}
613        }
614    }
615
616    /// Emit the extra u16 type name index after a TryCatchSetup jump.
617    pub(super) fn emit_type_name_extra(&mut self, type_name_idx: u16) {
618        let hi = (type_name_idx >> 8) as u8;
619        let lo = type_name_idx as u8;
620        self.chunk.code.push(hi);
621        self.chunk.code.push(lo);
622        self.chunk.lines.push(self.line);
623        self.chunk.columns.push(self.column);
624        self.chunk.lines.push(self.line);
625        self.chunk.columns.push(self.column);
626    }
627
628    /// Compile a try/catch body block (produces a value on the stack).
629    pub(super) fn compile_try_body(&mut self, body: &[SNode]) -> Result<(), CompileError> {
630        if body.is_empty() {
631            self.chunk.emit(Op::Nil, self.line);
632        } else {
633            self.compile_scoped_block(body)?;
634        }
635        Ok(())
636    }
637
638    /// Compile catch error binding (error value is on stack from handler).
639    pub(super) fn compile_catch_binding(
640        &mut self,
641        error_var: &Option<String>,
642    ) -> Result<(), CompileError> {
643        if let Some(var_name) = error_var {
644            self.emit_define_binding(var_name, false);
645        } else {
646            self.chunk.emit(Op::Pop, self.line);
647        }
648        Ok(())
649    }
650
651    /// Compile finally body inline, discarding its result value.
652    /// `compile_scoped_block` always leaves exactly one value on the stack
653    /// (Nil for non-value tail statements), so the trailing Pop is
654    /// unconditional — otherwise a finally ending in e.g. `x = x + 1`
655    /// would leave a stray Nil that corrupts the surrounding expression
656    /// when the enclosing try/finally is used in expression position.
657    pub(super) fn compile_finally_inline(
658        &mut self,
659        finally_body: &[SNode],
660    ) -> Result<(), CompileError> {
661        if !finally_body.is_empty() {
662            self.compile_scoped_block(finally_body)?;
663            self.chunk.emit(Op::Pop, self.line);
664        }
665        Ok(())
666    }
667
668    /// Collect pending finally bodies from the top of the stack down to
669    /// (but not including) the innermost `CatchBarrier`. Used by `throw`
670    /// lowering: throws caught locally don't unwind past the catch, so
671    /// finallys behind the barrier aren't on the throw's exit path.
672    pub(super) fn pending_finallys_until_barrier(&self) -> Vec<Vec<SNode>> {
673        let mut out = Vec::new();
674        for entry in self.finally_bodies.iter().rev() {
675            match entry {
676                FinallyEntry::CatchBarrier => break,
677                FinallyEntry::Finally(body) => out.push(body.clone()),
678            }
679        }
680        out
681    }
682
683    /// Collect every pending finally body from the top of the stack down
684    /// to `floor` (an index produced by `finally_bodies.len()` at some
685    /// earlier point), skipping `CatchBarrier` markers. Used by `return`,
686    /// `break`, and `continue` lowering — they transfer control past local
687    /// handlers, so every `Finally` up to their target must run.
688    pub(super) fn pending_finallys_down_to(&self, floor: usize) -> Vec<Vec<SNode>> {
689        let mut out = Vec::new();
690        for entry in self.finally_bodies[floor..].iter().rev() {
691            if let FinallyEntry::Finally(body) = entry {
692                out.push(body.clone());
693            }
694        }
695        out
696    }
697
698    /// All pending finally bodies (entire stack), skipping barriers.
699    pub(super) fn all_pending_finallys(&self) -> Vec<Vec<SNode>> {
700        self.pending_finallys_down_to(0)
701    }
702
703    /// True if there are any pending finally bodies (not just barriers).
704    pub(super) fn has_pending_finally(&self) -> bool {
705        self.finally_bodies
706            .iter()
707            .any(|e| matches!(e, FinallyEntry::Finally(_)))
708    }
709
710    /// Save a thrown value to a temp and rethrow without running finally.
711    ///
712    /// Historically this helper also invoked `compile_finally_inline` on the
713    /// thrown path, but that produced observable double-runs: the
714    /// `Node::ThrowStmt` lowering (below) already iterates `finally_bodies`
715    /// and runs each pending finally inline *before* emitting `Op::Throw`, so
716    /// a second run here fired the same side effects twice. Finally now runs
717    /// exactly once — via the throw-emit path during unwinding.
718    pub(super) fn compile_plain_rethrow(&mut self) -> Result<(), CompileError> {
719        self.temp_counter += 1;
720        let temp_name = format!("__finally_err_{}__", self.temp_counter);
721        self.emit_define_binding(&temp_name, true);
722        self.emit_get_binding(&temp_name);
723        self.chunk.emit(Op::Throw, self.line);
724        Ok(())
725    }
726
727    pub(super) fn declare_param_slots(&mut self, params: &[TypedParam]) {
728        for param in params {
729            self.define_local_slot(&param.name, false);
730        }
731    }
732
733    fn define_local_slot(&mut self, name: &str, mutable: bool) -> Option<u16> {
734        if self.module_level || harn_parser::is_discard_name(name) {
735            return None;
736        }
737        let current = self.local_scopes.last_mut()?;
738        if let Some(existing) = current.get_mut(name) {
739            if existing.mutable || mutable {
740                if mutable {
741                    existing.mutable = true;
742                    if let Some(info) = self.chunk.local_slots.get_mut(existing.slot as usize) {
743                        info.mutable = true;
744                    }
745                }
746                return Some(existing.slot);
747            }
748            return None;
749        }
750        let slot = self
751            .chunk
752            .add_local_slot(name.to_string(), mutable, self.scope_depth);
753        current.insert(name.to_string(), super::LocalBinding { slot, mutable });
754        Some(slot)
755    }
756
757    pub(super) fn resolve_local_slot(&self, name: &str) -> Option<super::LocalBinding> {
758        if self.module_level {
759            return None;
760        }
761        self.local_scopes
762            .iter()
763            .rev()
764            .find_map(|scope| scope.get(name).copied())
765    }
766
767    pub(super) fn emit_get_binding(&mut self, name: &str) {
768        if let Some(binding) = self.resolve_local_slot(name) {
769            self.chunk
770                .emit_u16(Op::GetLocalSlot, binding.slot, self.line);
771        } else {
772            let idx = self.chunk.add_constant(Constant::String(name.to_string()));
773            self.chunk.emit_u16(Op::GetVar, idx, self.line);
774        }
775    }
776
777    pub(super) fn emit_define_binding(&mut self, name: &str, mutable: bool) {
778        if let Some(slot) = self.define_local_slot(name, mutable) {
779            self.chunk.emit_u16(Op::DefLocalSlot, slot, self.line);
780        } else {
781            let idx = self.chunk.add_constant(Constant::String(name.to_string()));
782            let op = if mutable { Op::DefVar } else { Op::DefLet };
783            self.chunk.emit_u16(op, idx, self.line);
784        }
785    }
786
787    pub(super) fn emit_init_or_define_binding(&mut self, name: &str, mutable: bool) {
788        if let Some(binding) = self.resolve_local_slot(name) {
789            self.chunk
790                .emit_u16(Op::DefLocalSlot, binding.slot, self.line);
791        } else {
792            self.emit_define_binding(name, mutable);
793        }
794    }
795
796    pub(super) fn emit_set_binding(&mut self, name: &str) {
797        if let Some(binding) = self.resolve_local_slot(name) {
798            let _ = binding.mutable;
799            self.chunk
800                .emit_u16(Op::SetLocalSlot, binding.slot, self.line);
801        } else {
802            let idx = self.chunk.add_constant(Constant::String(name.to_string()));
803            self.chunk.emit_u16(Op::SetVar, idx, self.line);
804        }
805    }
806
807    pub(super) fn begin_scope(&mut self) {
808        self.chunk.emit(Op::PushScope, self.line);
809        self.scope_depth += 1;
810        self.type_scopes.push(std::collections::HashMap::new());
811        self.local_scopes.push(std::collections::HashMap::new());
812    }
813
814    pub(super) fn end_scope(&mut self) {
815        if self.scope_depth > 0 {
816            self.chunk.emit(Op::PopScope, self.line);
817            self.scope_depth -= 1;
818            self.type_scopes.pop();
819            self.local_scopes.pop();
820        }
821    }
822
823    pub(super) fn unwind_scopes_to(&mut self, target_depth: usize) {
824        while self.scope_depth > target_depth {
825            self.chunk.emit(Op::PopScope, self.line);
826            self.scope_depth -= 1;
827            self.type_scopes.pop();
828            self.local_scopes.pop();
829        }
830    }
831
832    pub(super) fn compile_scoped_block(&mut self, stmts: &[SNode]) -> Result<(), CompileError> {
833        self.begin_scope();
834        let finally_floor = self.finally_bodies.len();
835        if stmts.is_empty() {
836            self.chunk.emit(Op::Nil, self.line);
837        } else {
838            self.compile_block(stmts)?;
839        }
840        self.drain_finallys_to_floor(finally_floor)?;
841        self.end_scope();
842        Ok(())
843    }
844
845    pub(super) fn compile_scoped_statements(
846        &mut self,
847        stmts: &[SNode],
848    ) -> Result<(), CompileError> {
849        self.begin_scope();
850        let finally_floor = self.finally_bodies.len();
851        for sn in stmts {
852            self.compile_node(sn)?;
853            if Self::produces_value(&sn.node) {
854                self.chunk.emit(Op::Pop, self.line);
855            }
856        }
857        self.drain_finallys_to_floor(finally_floor)?;
858        self.end_scope();
859        Ok(())
860    }
861
862    /// Drain pending `defer` bodies down to a saved floor and run each inline
863    /// in LIFO order. Each defer body is popped *before* its code is emitted so
864    /// any `return` / `break` lowering inside the body sees the remaining
865    /// pending defers (not itself).
866    pub(super) fn drain_finallys_to_floor(&mut self, floor: usize) -> Result<(), CompileError> {
867        while self.finally_bodies.len() > floor {
868            let entry = self.finally_bodies.pop().expect("non-empty by guard");
869            if let FinallyEntry::Finally(body) = entry {
870                self.compile_finally_inline(&body)?;
871            }
872        }
873        Ok(())
874    }
875
876    /// Register an auto-drop defer for an `owned<T>` binding. The drop runs
877    /// at scope exit alongside any user-written `defer { ... }` blocks (LIFO
878    /// order) and on `return` / `break` / `continue` / `throw` via the
879    /// existing finally-unwinding machinery.
880    pub(super) fn maybe_register_owned_drop(
881        &mut self,
882        pattern: &harn_parser::BindingPattern,
883        type_ann: Option<&TypeExpr>,
884        span: harn_lexer::Span,
885    ) {
886        // Auto-drop only fires when the user explicitly opted in via
887        // `owned<T>` on a single-identifier binding. Destructured patterns
888        // (`{a, b}`, `[a, b]`, pairs) aren't auto-dropped: ownership of a
889        // composite isn't well-defined, and users can wrap individual fields
890        // with `owned<T>` and bind them separately if needed.
891        let Some(ty) = type_ann else {
892            return;
893        };
894        if !matches!(ty, TypeExpr::Owned(_)) {
895            return;
896        }
897        let harn_parser::BindingPattern::Identifier(name) = pattern else {
898            return;
899        };
900        if harn_parser::is_discard_name(name) {
901            return;
902        }
903        let call = harn_parser::spanned(
904            Node::FunctionCall {
905                name: "drop".to_string(),
906                args: vec![harn_parser::spanned(Node::Identifier(name.clone()), span)],
907                type_args: Vec::new(),
908            },
909            span,
910        );
911        self.finally_bodies.push(FinallyEntry::Finally(vec![call]));
912    }
913
914    pub(super) fn compile_block(&mut self, stmts: &[SNode]) -> Result<(), CompileError> {
915        for (i, snode) in stmts.iter().enumerate() {
916            self.compile_node(snode)?;
917            let is_last = i == stmts.len() - 1;
918            if is_last {
919                // Ensure the block always leaves exactly one value on the stack.
920                if !Self::produces_value(&snode.node) {
921                    self.chunk.emit(Op::Nil, self.line);
922                }
923            } else if Self::produces_value(&snode.node) {
924                self.chunk.emit(Op::Pop, self.line);
925            }
926        }
927        Ok(())
928    }
929
930    /// Compile a match arm body, ensuring it always pushes exactly one value.
931    pub(super) fn compile_match_body(&mut self, body: &[SNode]) -> Result<(), CompileError> {
932        self.begin_scope();
933        let finally_floor = self.finally_bodies.len();
934        if body.is_empty() {
935            self.chunk.emit(Op::Nil, self.line);
936        } else {
937            self.compile_block(body)?;
938            if !Self::produces_value(&body.last().unwrap().node) {
939                self.chunk.emit(Op::Nil, self.line);
940            }
941        }
942        self.drain_finallys_to_floor(finally_floor)?;
943        self.end_scope();
944        Ok(())
945    }
946
947    /// Emit the binary op instruction for a compound assignment operator.
948    pub(super) fn emit_compound_op(&mut self, op: &str) -> Result<(), CompileError> {
949        match op {
950            "+" => self.chunk.emit(Op::Add, self.line),
951            "-" => self.chunk.emit(Op::Sub, self.line),
952            "*" => self.chunk.emit(Op::Mul, self.line),
953            "/" => self.chunk.emit(Op::Div, self.line),
954            "%" => self.chunk.emit(Op::Mod, self.line),
955            _ => {
956                return Err(CompileError {
957                    message: format!("Unknown compound operator: {op}"),
958                    line: self.line,
959                })
960            }
961        }
962        Ok(())
963    }
964
965    /// Extract the root variable name from a (possibly nested) access expression.
966    pub(super) fn root_var_name(&self, node: &SNode) -> Option<String> {
967        match &node.node {
968            Node::Identifier(name) => Some(name.clone()),
969            Node::PropertyAccess { object, .. } | Node::OptionalPropertyAccess { object, .. } => {
970                self.root_var_name(object)
971            }
972            Node::SubscriptAccess { object, .. } | Node::OptionalSubscriptAccess { object, .. } => {
973                self.root_var_name(object)
974            }
975            _ => None,
976        }
977    }
978
979    pub(super) fn compile_top_level_declarations(
980        &mut self,
981        program: &[SNode],
982    ) -> Result<(), CompileError> {
983        // Phase 1: evaluate module-level `let` / `var` bindings first, in
984        // source order. This ensures function closures compiled in phase 2
985        // capture these names in their env snapshot via `Op::Closure` —
986        // fixing the "Undefined variable: FOO" surprise where a top-level
987        // `let FOO = "..."` was silently dropped because it wasn't in this
988        // match list. Keep in step with the import-time init path in
989        // `crates/harn-vm/src/vm/imports.rs` (`module_state` construction).
990        for sn in program {
991            if matches!(
992                &sn.node,
993                Node::LetBinding { .. } | Node::VarBinding { .. } | Node::ConstBinding { .. }
994            ) {
995                self.compile_node(sn)?;
996            }
997        }
998        // Phase 2: compile type and function declarations. Function closures
999        // created here capture the current env which now includes the
1000        // module-level bindings from phase 1. Attributed declarations are
1001        // compiled here too — the AttributedDecl arm in compile_node
1002        // dispatches to the inner declaration's compile path.
1003        for sn in program {
1004            let inner_kind = match &sn.node {
1005                Node::AttributedDecl { inner, .. } => &inner.node,
1006                other => other,
1007            };
1008            match inner_kind {
1009                Node::EvalPackDecl {
1010                    binding_name,
1011                    pack_id,
1012                    fields,
1013                    body,
1014                    summarize,
1015                    ..
1016                } => {
1017                    self.compile_eval_pack_decl(
1018                        binding_name,
1019                        pack_id,
1020                        fields,
1021                        body,
1022                        summarize,
1023                        false,
1024                    )?;
1025                }
1026                Node::FnDecl { .. }
1027                | Node::ToolDecl { .. }
1028                | Node::SkillDecl { .. }
1029                | Node::ImplBlock { .. }
1030                | Node::StructDecl { .. }
1031                | Node::EnumDecl { .. }
1032                | Node::InterfaceDecl { .. }
1033                | Node::TypeDecl { .. } => {
1034                    self.compile_node(sn)?;
1035                }
1036                _ => {}
1037            }
1038        }
1039        Ok(())
1040    }
1041
1042    /// Recursively collect all enum type names from the AST.
1043    pub(super) fn collect_enum_names(
1044        nodes: &[SNode],
1045        names: &mut std::collections::HashSet<String>,
1046    ) {
1047        for sn in nodes {
1048            match &sn.node {
1049                Node::EnumDecl { name, .. } => {
1050                    names.insert(name.clone());
1051                }
1052                Node::Pipeline { body, .. } => {
1053                    Self::collect_enum_names(body, names);
1054                }
1055                Node::FnDecl { body, .. } | Node::ToolDecl { body, .. } => {
1056                    Self::collect_enum_names(body, names);
1057                }
1058                Node::SkillDecl { fields, .. } => {
1059                    for (_k, v) in fields {
1060                        Self::collect_enum_names(std::slice::from_ref(v), names);
1061                    }
1062                }
1063                Node::EvalPackDecl {
1064                    fields,
1065                    body,
1066                    summarize,
1067                    ..
1068                } => {
1069                    for (_k, v) in fields {
1070                        Self::collect_enum_names(std::slice::from_ref(v), names);
1071                    }
1072                    Self::collect_enum_names(body, names);
1073                    if let Some(summary_body) = summarize {
1074                        Self::collect_enum_names(summary_body, names);
1075                    }
1076                }
1077                Node::Block(stmts) => {
1078                    Self::collect_enum_names(stmts, names);
1079                }
1080                Node::AttributedDecl { inner, .. } => {
1081                    Self::collect_enum_names(std::slice::from_ref(inner), names);
1082                }
1083                _ => {}
1084            }
1085        }
1086    }
1087
1088    pub(super) fn collect_struct_layouts(
1089        nodes: &[SNode],
1090        layouts: &mut std::collections::HashMap<String, Vec<String>>,
1091    ) {
1092        for sn in nodes {
1093            match &sn.node {
1094                Node::StructDecl { name, fields, .. } => {
1095                    layouts.insert(
1096                        name.clone(),
1097                        fields.iter().map(|field| field.name.clone()).collect(),
1098                    );
1099                }
1100                Node::Pipeline { body, .. }
1101                | Node::FnDecl { body, .. }
1102                | Node::ToolDecl { body, .. } => {
1103                    Self::collect_struct_layouts(body, layouts);
1104                }
1105                Node::SkillDecl { fields, .. } => {
1106                    for (_k, v) in fields {
1107                        Self::collect_struct_layouts(std::slice::from_ref(v), layouts);
1108                    }
1109                }
1110                Node::EvalPackDecl {
1111                    fields,
1112                    body,
1113                    summarize,
1114                    ..
1115                } => {
1116                    for (_k, v) in fields {
1117                        Self::collect_struct_layouts(std::slice::from_ref(v), layouts);
1118                    }
1119                    Self::collect_struct_layouts(body, layouts);
1120                    if let Some(summary_body) = summarize {
1121                        Self::collect_struct_layouts(summary_body, layouts);
1122                    }
1123                }
1124                Node::Block(stmts) => {
1125                    Self::collect_struct_layouts(stmts, layouts);
1126                }
1127                Node::AttributedDecl { inner, .. } => {
1128                    Self::collect_struct_layouts(std::slice::from_ref(inner), layouts);
1129                }
1130                _ => {}
1131            }
1132        }
1133    }
1134
1135    pub(super) fn collect_interface_methods(
1136        nodes: &[SNode],
1137        interfaces: &mut std::collections::HashMap<String, Vec<String>>,
1138    ) {
1139        for sn in nodes {
1140            match &sn.node {
1141                Node::InterfaceDecl { name, methods, .. } => {
1142                    let method_names: Vec<String> =
1143                        methods.iter().map(|m| m.name.clone()).collect();
1144                    interfaces.insert(name.clone(), method_names);
1145                }
1146                Node::Pipeline { body, .. }
1147                | Node::FnDecl { body, .. }
1148                | Node::ToolDecl { body, .. } => {
1149                    Self::collect_interface_methods(body, interfaces);
1150                }
1151                Node::SkillDecl { fields, .. } => {
1152                    for (_k, v) in fields {
1153                        Self::collect_interface_methods(std::slice::from_ref(v), interfaces);
1154                    }
1155                }
1156                Node::EvalPackDecl {
1157                    fields,
1158                    body,
1159                    summarize,
1160                    ..
1161                } => {
1162                    for (_k, v) in fields {
1163                        Self::collect_interface_methods(std::slice::from_ref(v), interfaces);
1164                    }
1165                    Self::collect_interface_methods(body, interfaces);
1166                    if let Some(summary_body) = summarize {
1167                        Self::collect_interface_methods(summary_body, interfaces);
1168                    }
1169                }
1170                Node::Block(stmts) => {
1171                    Self::collect_interface_methods(stmts, interfaces);
1172                }
1173                Node::AttributedDecl { inner, .. } => {
1174                    Self::collect_interface_methods(std::slice::from_ref(inner), interfaces);
1175                }
1176                _ => {}
1177            }
1178        }
1179    }
1180
1181    /// Compile a function body into a CompiledFunction (for import support).
1182    ///
1183    /// This path is used when a module is imported and its top-level `fn`
1184    /// declarations are loaded into the importer's environment. It MUST emit
1185    /// the same function preamble as the in-file `Node::FnDecl` path, or
1186    /// imported functions will behave differently from locally-defined ones —
1187    /// in particular, default parameter values would never be set and typed
1188    /// parameters would not be runtime-checked.
1189    ///
1190    /// `source_file`, when provided, tags the resulting chunk so runtime
1191    /// errors can attribute frames to the imported file rather than the
1192    /// entry-point pipeline.
1193    pub fn compile_fn_body(
1194        &mut self,
1195        type_params: &[harn_parser::TypeParam],
1196        params: &[TypedParam],
1197        body: &[SNode],
1198        source_file: Option<String>,
1199    ) -> Result<CompiledFunction, CompileError> {
1200        let mut fn_compiler = self.nested_body();
1201        fn_compiler.enum_names = self.enum_names.clone();
1202        fn_compiler.interface_methods = self.interface_methods.clone();
1203        fn_compiler.type_aliases = self.type_aliases.clone();
1204        fn_compiler.struct_layouts = self.struct_layouts.clone();
1205        fn_compiler.declare_param_slots(params);
1206        fn_compiler.record_param_types(params);
1207        fn_compiler.emit_default_preamble(params)?;
1208        fn_compiler.emit_type_checks(params);
1209        let is_gen = body_contains_yield(body);
1210        fn_compiler.compile_block(body)?;
1211        fn_compiler.chunk.emit(Op::Nil, 0);
1212        fn_compiler.chunk.emit(Op::Return, 0);
1213        fn_compiler.chunk.source_file = source_file;
1214        let param_slots = crate::chunk::ParamSlot::vec_from_typed(params);
1215        let has_runtime_type_checks =
1216            CompiledFunction::has_runtime_type_checks_for_params(&param_slots);
1217        Ok(CompiledFunction {
1218            name: String::new(),
1219            type_params: type_params.iter().map(|param| param.name.clone()).collect(),
1220            nominal_type_names: fn_compiler.nominal_type_names(),
1221            params: param_slots,
1222            default_start: TypedParam::default_start(params),
1223            chunk: Rc::new(fn_compiler.chunk),
1224            is_generator: is_gen,
1225            is_stream: false,
1226            has_rest_param: false,
1227            has_runtime_type_checks,
1228        })
1229    }
1230
1231    /// Check if a node produces a value on the stack that needs to be popped.
1232    pub(super) fn produces_value(node: &Node) -> bool {
1233        match node {
1234            Node::LetBinding { .. }
1235            | Node::VarBinding { .. }
1236            | Node::ConstBinding { .. }
1237            | Node::Assignment { .. }
1238            | Node::ReturnStmt { .. }
1239            | Node::FnDecl { .. }
1240            | Node::ToolDecl { .. }
1241            | Node::SkillDecl { .. }
1242            | Node::EvalPackDecl { .. }
1243            | Node::ImplBlock { .. }
1244            | Node::StructDecl { .. }
1245            | Node::EnumDecl { .. }
1246            | Node::InterfaceDecl { .. }
1247            | Node::TypeDecl { .. }
1248            | Node::ThrowStmt { .. }
1249            | Node::BreakStmt
1250            | Node::ContinueStmt
1251            | Node::RequireStmt { .. }
1252            | Node::DeferStmt { .. } => false,
1253            Node::TryCatch { has_catch: _, .. }
1254            | Node::TryExpr { .. }
1255            | Node::Retry { .. }
1256            | Node::GuardStmt { .. }
1257            | Node::DeadlineBlock { .. }
1258            | Node::MutexBlock { .. }
1259            | Node::Spread(_) => true,
1260            _ => true,
1261        }
1262    }
1263}
1264
1265impl Default for Compiler {
1266    fn default() -> Self {
1267        Self::new()
1268    }
1269}