Skip to main content

perl_parser_core/hir/
body.rs

1//! HIR body graph: arena-based expression/statement/block representation.
2//!
3//! This module provides the first vertical slice of HIR body infrastructure,
4//! implementing the arena-based graph described in ADR #2564. It introduces:
5//!
6//! - Typed arena indices: [`HirBodyId`], [`HirExprId`], [`HirStmtId`], [`HirBlockId`]
7//! - Per-body arenas and a source map: [`HirBody`], [`BodySourceMap`]
8//! - Body owners (program root + subroutine): [`BodyOwnerKind`]
9//! - Expression/statement/block node taxonomy: [`HirExpr`], [`HirStmt`], [`HirBlock`]
10//! - A lowering entry point for one Perl source string: [`lower_body`]
11//!
12//! # Specimen
13//!
14//! The first vertical slice lowers exactly one specimen end-to-end:
15//!
16//! ```text
17//! my $x = $a + $b;
18//! ```
19//!
20//! This produces a [`HirBody`] with:
21//! - One [`HirStmt::Let`] binding `$x`
22//! - One [`HirExpr::Binary`] `+` node with children reading `$a` and `$b`
23//! - One [`HirExpr::Assign`] connecting the decl place to the binary value
24//! - All nodes carry exact byte-offset source ranges in [`BodySourceMap`]
25
26use crate::SourceLocation;
27
28// ──────────────────────────────────────────────────────────────────────────────
29// Typed arena indices
30// ──────────────────────────────────────────────────────────────────────────────
31
32/// Stable index into the workspace body registry (one per body owner).
33///
34/// Currently unused at runtime — reserved so that cross-body references can be
35/// introduced later without changing existing index types.
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
37pub struct HirBodyId(pub u32);
38
39/// Typed index into a [`HirBody`]'s expression arena.
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
41pub struct HirExprId(pub u32);
42
43/// Typed index into a [`HirBody`]'s statement arena.
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
45pub struct HirStmtId(pub u32);
46
47/// Typed index into a [`HirBody`]'s block arena.
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
49pub struct HirBlockId(pub u32);
50
51// ──────────────────────────────────────────────────────────────────────────────
52// Arena
53// ──────────────────────────────────────────────────────────────────────────────
54
55/// Typed append-only arena indexed by a strongly-typed ID.
56///
57/// The ID type must implement a constructor from `u32`; see the `impl` blocks
58/// below for each concrete instantiation.
59#[derive(Debug, Clone, PartialEq, Eq)]
60pub struct Arena<T> {
61    items: Vec<T>,
62}
63
64impl<T> Default for Arena<T> {
65    fn default() -> Self {
66        Self { items: Vec::new() }
67    }
68}
69
70impl<T> Arena<T> {
71    /// Push a value and return its index.
72    pub fn alloc(&mut self, value: T) -> u32 {
73        let id = self.items.len() as u32;
74        self.items.push(value);
75        id
76    }
77
78    /// Return a reference to the value at `index`.
79    ///
80    /// Returns `None` if the index is out of bounds.
81    pub fn get(&self, index: u32) -> Option<&T> {
82        self.items.get(index as usize)
83    }
84
85    /// Return the number of allocated items.
86    pub fn len(&self) -> usize {
87        self.items.len()
88    }
89
90    /// Return true when no items have been allocated.
91    pub fn is_empty(&self) -> bool {
92        self.items.is_empty()
93    }
94
95    /// Iterate over all allocated items in allocation order.
96    pub fn iter(&self) -> impl Iterator<Item = &T> {
97        self.items.iter()
98    }
99}
100
101// ──────────────────────────────────────────────────────────────────────────────
102// Source map
103// ──────────────────────────────────────────────────────────────────────────────
104
105/// Source map for one [`HirBody`].
106///
107/// Each arena entry in [`HirBody`] has a corresponding `SourceLocation` here,
108/// indexed by the same raw `u32` that the typed ID wraps.
109#[derive(Debug, Clone, PartialEq, Eq, Default)]
110pub struct BodySourceMap {
111    /// Source range for each expression, indexed by [`HirExprId`] value.
112    pub expr_ranges: Vec<SourceLocation>,
113    /// Source range for each statement, indexed by [`HirStmtId`] value.
114    pub stmt_ranges: Vec<SourceLocation>,
115    /// Source range for each block, indexed by [`HirBlockId`] value.
116    pub block_ranges: Vec<SourceLocation>,
117}
118
119impl BodySourceMap {
120    /// Look up the source range for an expression.
121    pub fn expr_range(&self, id: HirExprId) -> Option<SourceLocation> {
122        self.expr_ranges.get(id.0 as usize).copied()
123    }
124
125    /// Look up the source range for a statement.
126    pub fn stmt_range(&self, id: HirStmtId) -> Option<SourceLocation> {
127        self.stmt_ranges.get(id.0 as usize).copied()
128    }
129
130    /// Look up the source range for a block.
131    pub fn block_range(&self, id: HirBlockId) -> Option<SourceLocation> {
132        self.block_ranges.get(id.0 as usize).copied()
133    }
134}
135
136// ──────────────────────────────────────────────────────────────────────────────
137// Body owner
138// ──────────────────────────────────────────────────────────────────────────────
139
140/// What syntactic construct owns this body.
141#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
142pub enum BodyOwnerKind {
143    /// Top-level program root (file-level statement sequence).
144    ProgramRoot,
145    /// Named subroutine body (`sub foo { ... }`).
146    Subroutine {
147        /// Subroutine name, or `None` for anonymous subs.
148        name: Option<String>,
149    },
150    /// Method body (`method foo { ... }`).
151    Method {
152        /// Method name.
153        name: String,
154    },
155}
156
157/// Stable key for a body in the per-file body registry.
158///
159/// The ordinal disambiguates multiple anonymous subroutines or method bodies
160/// with the same name in one file.
161#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
162pub struct BodyOwner {
163    /// What owns this body.
164    pub kind: BodyOwnerKind,
165    /// Zero-based ordinal for disambiguation within one file.
166    pub ordinal: u32,
167}
168
169impl BodyOwner {
170    /// Create a `BodyOwner` key.
171    pub fn new(kind: BodyOwnerKind, ordinal: u32) -> Self {
172        Self { kind, ordinal }
173    }
174}
175
176// ──────────────────────────────────────────────────────────────────────────────
177// Expression nodes
178// ──────────────────────────────────────────────────────────────────────────────
179
180/// Sigil for a variable reference.
181#[derive(Debug, Clone, PartialEq, Eq)]
182pub enum Sigil {
183    /// `$` — scalar.
184    Scalar,
185    /// `@` — array.
186    Array,
187    /// `%` — hash.
188    Hash,
189    /// `&` — code ref / sub.
190    Code,
191    /// `*` — typeglob.
192    Glob,
193}
194
195impl Sigil {
196    /// Parse from a sigil character string as produced by the AST.
197    fn from_str(s: &str) -> Self {
198        match s {
199            "$" => Sigil::Scalar,
200            "@" => Sigil::Array,
201            "%" => Sigil::Hash,
202            "&" => Sigil::Code,
203            "*" => Sigil::Glob,
204            _ => Sigil::Scalar, // graceful fallback; should not occur
205        }
206    }
207}
208
209/// How a variable is accessed.
210#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
211pub enum AccessMode {
212    /// Value is being read.
213    Read,
214    /// Variable is the target of an assignment (lexical place / lvalue).
215    Write,
216    /// Variable is both read and written — compound assignment or `++`/`--`.
217    ReadModifyWrite,
218}
219
220/// Variable origin classification.
221#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
222pub enum VariableKind {
223    /// `my`-declared lexical in scope.
224    Lexical,
225    /// Package / stash variable.
226    Package,
227}
228
229/// A variable reference node.
230#[derive(Debug, Clone, PartialEq, Eq)]
231pub struct HirVariable {
232    /// Variable sigil.
233    pub sigil: Sigil,
234    /// Variable name without sigil.
235    pub name: String,
236    /// Lexical or package origin.
237    pub kind: VariableKind,
238    /// How this node uses the variable.
239    pub access: AccessMode,
240}
241
242/// How an assignment is performed.
243#[derive(Debug, Clone, PartialEq, Eq)]
244pub enum AssignMode {
245    /// Simple `=` assignment.
246    Simple,
247    /// Compound assignment: `+=`, `-=`, `*=`, etc.
248    ///
249    /// The LHS is both read (to compute the new value) and written (to store
250    /// the result).  The LHS variable node carries [`AccessMode::ReadModifyWrite`].
251    ReadModifyWrite,
252}
253
254/// How a unary operator accesses its operand.
255#[derive(Debug, Clone, PartialEq, Eq)]
256pub enum UnaryMode {
257    /// Pure read (e.g. unary minus, `!`).
258    Read,
259    /// Read-modify-write (e.g. `++`, `--`).
260    ReadModifyWrite,
261}
262
263/// Binary operator.
264#[derive(Debug, Clone, PartialEq, Eq)]
265pub enum BinaryOp {
266    /// Numeric addition `+`.
267    Add,
268    /// Numeric subtraction `-`.
269    Sub,
270    /// Numeric multiplication `*`.
271    Mul,
272    /// Numeric division `/`.
273    Div,
274    /// String concatenation `.`.
275    Concat,
276    /// Other/unknown operator — preserves the original text.
277    Other(String),
278}
279
280impl BinaryOp {
281    fn from_str(s: &str) -> Self {
282        match s {
283            "+" => BinaryOp::Add,
284            "-" => BinaryOp::Sub,
285            "*" => BinaryOp::Mul,
286            "/" => BinaryOp::Div,
287            "." => BinaryOp::Concat,
288            other => BinaryOp::Other(other.to_string()),
289        }
290    }
291}
292
293/// One expression node in the HIR body graph.
294///
295/// Every variant that has child expressions carries explicit [`HirExprId`]
296/// references — there are no flat shells.
297#[derive(Debug, Clone, PartialEq, Eq)]
298pub enum HirExpr {
299    /// Variable read or write-place reference.
300    Variable(HirVariable),
301
302    /// Binary expression: `lhs OP rhs`, both children are explicit IDs.
303    Binary {
304        /// Left-hand operand.
305        lhs: HirExprId,
306        /// Operator.
307        op: BinaryOp,
308        /// Right-hand operand.
309        rhs: HirExprId,
310    },
311
312    /// Assignment expression: `lhs = rhs`, both sides are explicit IDs.
313    ///
314    /// For `my $x = …`, the `lhs` is a `Variable` node with `access: Write`
315    /// representing the declared place, and `rhs` is the initializer.
316    Assign {
317        /// Assignment target (place / lvalue).
318        lhs: HirExprId,
319        /// Value being assigned.
320        rhs: HirExprId,
321        /// Assignment mode.
322        mode: AssignMode,
323    },
324
325    /// Unary expression: `OP operand`.
326    Unary {
327        /// Operand.
328        operand: HirExprId,
329        /// How the operator accesses the operand.
330        mode: UnaryMode,
331        /// Operator text, for diagnostics.
332        op: String,
333    },
334
335    /// Function/method call expression (first-pass model).
336    ///
337    /// Arguments that are individually lowerable carry explicit IDs; everything
338    /// else is `Opaque`.
339    Call {
340        /// Argument expressions in source order.
341        args: Vec<HirExprId>,
342        /// The AST node kind name, for diagnostics.
343        ast_kind: String,
344    },
345
346    /// Opaque expression — used when the AST shape is not yet modeled.
347    Opaque {
348        /// The AST node kind name for diagnostics.
349        ast_kind: String,
350    },
351}
352
353// ──────────────────────────────────────────────────────────────────────────────
354// Statement nodes
355// ──────────────────────────────────────────────────────────────────────────────
356
357/// Storage class for a variable declaration.
358#[derive(Debug, Clone, PartialEq, Eq)]
359pub enum DeclStorageClass {
360    /// `my` — lexical.
361    My,
362    /// `our` — package alias.
363    Our,
364    /// `local` — dynamic.
365    Local,
366    /// `state` — persistent lexical.
367    State,
368}
369
370impl DeclStorageClass {
371    fn from_str(s: &str) -> Self {
372        match s {
373            "my" => DeclStorageClass::My,
374            "our" => DeclStorageClass::Our,
375            "local" => DeclStorageClass::Local,
376            "state" => DeclStorageClass::State,
377            _ => DeclStorageClass::My,
378        }
379    }
380}
381
382/// One statement node in the HIR body graph.
383#[derive(Debug, Clone, PartialEq, Eq)]
384pub enum HirStmt {
385    /// Expression statement: evaluate the expression for its side effects.
386    Expr(HirExprId),
387
388    /// Variable declaration: `my $x = …`
389    ///
390    /// The `init` expression, when present, is the full initializer (which may
391    /// itself be an [`HirExpr::Assign`] that links the declared place to its
392    /// value).
393    Let {
394        /// Variable name without sigil.
395        name: String,
396        /// Sigil of the declared variable.
397        sigil: Sigil,
398        /// Storage class: `my`, `our`, `local`, `state`.
399        storage: DeclStorageClass,
400        /// Optional initializer expression ID.
401        init: Option<HirExprId>,
402    },
403}
404
405// ──────────────────────────────────────────────────────────────────────────────
406// Block node
407// ──────────────────────────────────────────────────────────────────────────────
408
409/// A sequenced list of statements — the building block of every body.
410#[derive(Debug, Clone, PartialEq, Eq, Default)]
411pub struct HirBlock {
412    /// Ordered statement IDs in source order.
413    pub stmts: Vec<HirStmtId>,
414}
415
416// ──────────────────────────────────────────────────────────────────────────────
417// HirBody
418// ──────────────────────────────────────────────────────────────────────────────
419
420/// Arena-based expression/statement/block graph for one body owner.
421///
422/// This is the unit of lowering produced by [`lower_body`]. It is separate
423/// from [`crate::hir::HirFile`]'s flat item list — flat items remain for
424/// compile-time fact extraction; bodies are the new representation for data-flow
425/// analysis, context propagation, and PIR-A lowering.
426#[derive(Debug, Clone, PartialEq, Eq)]
427pub struct HirBody {
428    /// All expression nodes, indexed by [`HirExprId`].
429    pub exprs: Arena<HirExpr>,
430    /// All statement nodes, indexed by [`HirStmtId`].
431    pub stmts: Arena<HirStmt>,
432    /// All block nodes (ordered statement sequences), indexed by [`HirBlockId`].
433    pub blocks: Arena<HirBlock>,
434    /// Source map: maps each expr/stmt/block index to its [`SourceLocation`].
435    pub source_map: BodySourceMap,
436    /// Root block — the entry point for the body.
437    pub root_block: HirBlockId,
438    /// What syntactic construct owns this body.
439    pub owner: BodyOwnerKind,
440}
441
442impl HirBody {
443    /// Look up an expression node by ID.
444    pub fn expr(&self, id: HirExprId) -> Option<&HirExpr> {
445        self.exprs.get(id.0)
446    }
447
448    /// Look up a statement node by ID.
449    pub fn stmt(&self, id: HirStmtId) -> Option<&HirStmt> {
450        self.stmts.get(id.0)
451    }
452
453    /// Look up a block node by ID.
454    pub fn block(&self, id: HirBlockId) -> Option<&HirBlock> {
455        self.blocks.get(id.0)
456    }
457}
458
459// ──────────────────────────────────────────────────────────────────────────────
460// Body builder (internal)
461// ──────────────────────────────────────────────────────────────────────────────
462
463struct BodyBuilder {
464    exprs: Arena<HirExpr>,
465    stmts: Arena<HirStmt>,
466    blocks: Arena<HirBlock>,
467    source_map: BodySourceMap,
468}
469
470impl BodyBuilder {
471    fn new() -> Self {
472        Self {
473            exprs: Arena::default(),
474            stmts: Arena::default(),
475            blocks: Arena::default(),
476            source_map: BodySourceMap::default(),
477        }
478    }
479
480    fn alloc_expr(&mut self, expr: HirExpr, range: SourceLocation) -> HirExprId {
481        let idx = self.exprs.alloc(expr);
482        self.source_map.expr_ranges.push(range);
483        HirExprId(idx)
484    }
485
486    fn alloc_stmt(&mut self, stmt: HirStmt, range: SourceLocation) -> HirStmtId {
487        let idx = self.stmts.alloc(stmt);
488        self.source_map.stmt_ranges.push(range);
489        HirStmtId(idx)
490    }
491
492    fn alloc_block(&mut self, block: HirBlock, range: SourceLocation) -> HirBlockId {
493        let idx = self.blocks.alloc(block);
494        self.source_map.block_ranges.push(range);
495        HirBlockId(idx)
496    }
497
498    fn finish(self, root_block: HirBlockId, owner: BodyOwnerKind) -> HirBody {
499        HirBody {
500            exprs: self.exprs,
501            stmts: self.stmts,
502            blocks: self.blocks,
503            source_map: self.source_map,
504            root_block,
505            owner,
506        }
507    }
508}
509
510// ──────────────────────────────────────────────────────────────────────────────
511// Lowering
512// ──────────────────────────────────────────────────────────────────────────────
513
514use crate::{Node, NodeKind};
515
516/// Lower the top-level program-root body from a parsed AST node.
517///
518/// This is the entry point for the first vertical slice. It lowers the
519/// statements inside a `Program` node into an arena-based [`HirBody`].
520///
521/// Currently handled constructs:
522/// - `my $x = EXPR;` — [`HirStmt::Let`] with an [`HirExpr::Assign`] initializer
523/// - `$a + $b` — [`HirExpr::Binary`] with explicit child IDs
524/// - Variable references — [`HirExpr::Variable`]
525/// - Everything else — [`HirExpr::Opaque`] / [`HirStmt::Expr`] fallback
526pub fn lower_body(ast: &Node) -> HirBody {
527    let mut builder = BodyBuilder::new();
528
529    let stmts = match &ast.kind {
530        NodeKind::Program { statements } => statements.as_slice(),
531        _ => std::slice::from_ref(ast),
532    };
533
534    let root_range = ast.location;
535    let mut root_block = HirBlock::default();
536
537    for stmt_node in stmts {
538        let stmt_id = lower_statement(&mut builder, stmt_node);
539        root_block.stmts.push(stmt_id);
540    }
541
542    let root_id = builder.alloc_block(root_block, root_range);
543    builder.finish(root_id, BodyOwnerKind::ProgramRoot)
544}
545
546fn lower_statement(builder: &mut BodyBuilder, node: &Node) -> HirStmtId {
547    let range = node.location;
548
549    match &node.kind {
550        NodeKind::VariableDeclaration { declarator, variable, initializer, .. } => {
551            // Extract variable name and sigil from the inner Variable node.
552            let (sigil_str, var_name) = match &variable.kind {
553                NodeKind::Variable { sigil, name } => (sigil.as_str(), name.clone()),
554                _ => ("$", String::from("<unknown>")),
555            };
556            let sigil = Sigil::from_str(sigil_str);
557            let storage = DeclStorageClass::from_str(declarator);
558
559            let init_expr_id = initializer.as_ref().map(|init_node| {
560                // The initializer is the full RHS expression.
561                // For `my $x = $a + $b`, the AST may represent this as:
562                //   VariableDeclaration { variable: $x, initializer: Binary(+, $a, $b) }
563                // We model the assignment as:
564                //   Assign { lhs: Variable($x, Write), rhs: lower_expr(initializer) }
565
566                // Allocate the write-place for $x
567                let place_expr = HirExpr::Variable(HirVariable {
568                    sigil: Sigil::from_str(sigil_str),
569                    name: var_name.clone(),
570                    kind: VariableKind::Lexical,
571                    access: AccessMode::Write,
572                });
573                let place_id = builder.alloc_expr(place_expr, variable.location);
574
575                // Lower the RHS value expression
576                let rhs_id = lower_expr(builder, init_node);
577
578                // Wrap in an Assign node spanning the full declaration range
579                let assign_range =
580                    SourceLocation { start: variable.location.start, end: init_node.location.end };
581                let assign_expr =
582                    HirExpr::Assign { lhs: place_id, rhs: rhs_id, mode: AssignMode::Simple };
583                builder.alloc_expr(assign_expr, assign_range)
584            });
585
586            builder.alloc_stmt(
587                HirStmt::Let { name: var_name, sigil, storage, init: init_expr_id },
588                range,
589            )
590        }
591
592        // Expression statement fallback
593        _ => {
594            let expr_id = lower_expr(builder, node);
595            builder.alloc_stmt(HirStmt::Expr(expr_id), range)
596        }
597    }
598}
599
600fn lower_expr(builder: &mut BodyBuilder, node: &Node) -> HirExprId {
601    let range = node.location;
602
603    match &node.kind {
604        NodeKind::Variable { sigil, name } => {
605            let var = HirVariable {
606                sigil: Sigil::from_str(sigil),
607                name: name.clone(),
608                kind: VariableKind::Lexical,
609                access: AccessMode::Read,
610            };
611            builder.alloc_expr(HirExpr::Variable(var), range)
612        }
613
614        NodeKind::Binary { op, left, right } => {
615            let lhs_id = lower_expr(builder, left);
616            let rhs_id = lower_expr(builder, right);
617            let binary_op = BinaryOp::from_str(op);
618            builder.alloc_expr(HirExpr::Binary { lhs: lhs_id, op: binary_op, rhs: rhs_id }, range)
619        }
620
621        NodeKind::Assignment { lhs, rhs, .. } => {
622            let lhs_id = lower_expr(builder, lhs);
623            let rhs_id = lower_expr(builder, rhs);
624            builder.alloc_expr(
625                HirExpr::Assign { lhs: lhs_id, rhs: rhs_id, mode: AssignMode::Simple },
626                range,
627            )
628        }
629
630        _ => {
631            let kind_name = node.kind.kind_name().to_string();
632            builder.alloc_expr(HirExpr::Opaque { ast_kind: kind_name }, range)
633        }
634    }
635}