diff --git a/crates/plotnik-lib/src/analyze/type_check/infer.rs b/crates/plotnik-lib/src/analyze/type_check/infer.rs index d6baac3a..638130f2 100644 --- a/crates/plotnik-lib/src/analyze/type_check/infer.rs +++ b/crates/plotnik-lib/src/analyze/type_check/infer.rs @@ -267,6 +267,16 @@ impl<'a, 'd> InferenceVisitor<'a, 'd> { /// - Other expressions (named nodes, refs) don't create scopes. /// Inner fields bubble up alongside the capture field. fn infer_captured_expr(&mut self, cap: &CapturedExpr) -> TermInfo { + // Suppressive captures don't contribute to output type + if cap.is_suppressive() { + // Still infer inner for structural validation, but don't create fields + return cap + .inner() + .map(|i| self.infer_expr(&i)) + .map(|info| TermInfo::new(info.arity, TypeFlow::Void)) + .unwrap_or_else(TermInfo::void); + } + let Some(name_tok) = cap.name() else { // Recover gracefully return cap @@ -274,7 +284,7 @@ impl<'a, 'd> InferenceVisitor<'a, 'd> { .map(|i| self.infer_expr(&i)) .unwrap_or_else(TermInfo::void); }; - let capture_name = self.interner.intern(name_tok.text()); + let capture_name = self.interner.intern(&name_tok.text()[1..]); // Strip @ prefix let annotation_type = self.resolve_annotation(cap); let Some(inner) = cap.inner() else { diff --git a/crates/plotnik-lib/src/bytecode/effects.rs b/crates/plotnik-lib/src/bytecode/effects.rs index 879730cb..fb98c50b 100644 --- a/crates/plotnik-lib/src/bytecode/effects.rs +++ b/crates/plotnik-lib/src/bytecode/effects.rs @@ -15,6 +15,8 @@ pub enum EffectOpcode { Text = 9, Clear = 10, Null = 11, + SuppressBegin = 12, + SuppressEnd = 13, } impl EffectOpcode { @@ -32,6 +34,8 @@ impl EffectOpcode { 9 => Self::Text, 10 => Self::Clear, 11 => Self::Null, + 12 => Self::SuppressBegin, + 13 => Self::SuppressEnd, _ => panic!("invalid effect opcode: {v}"), } } diff --git a/crates/plotnik-lib/src/bytecode/format.rs b/crates/plotnik-lib/src/bytecode/format.rs index 551120ba..61fe6005 100644 --- a/crates/plotnik-lib/src/bytecode/format.rs +++ b/crates/plotnik-lib/src/bytecode/format.rs @@ -148,6 +148,8 @@ pub mod trace { /// Effect: data capture or structure. pub const EFFECT: Symbol = Symbol::new(" ", "⬥", " "); + /// Effect: suppressed (inside @_ capture). + pub const EFFECT_SUPPRESSED: Symbol = Symbol::new(" ", "⬦", " "); /// Call: entering definition. pub const CALL: Symbol = Symbol::new(" ", "▶", " "); @@ -275,7 +277,9 @@ impl LineBuilder { /// /// Ensures at least 2 spaces between content and successors. pub fn pad_successors(&self, base: String, successors: &str) -> String { - let padding = cols::TOTAL_WIDTH.saturating_sub(base.chars().count()).max(2); + let padding = cols::TOTAL_WIDTH + .saturating_sub(base.chars().count()) + .max(2); format!("{base}{:padding$}{successors}", "") } } @@ -299,6 +303,8 @@ pub fn format_effect(effect: &EffectOp) -> String { EffectOpcode::Text => "Text".to_string(), EffectOpcode::Clear => "Clear".to_string(), EffectOpcode::Null => "Null".to_string(), + EffectOpcode::SuppressBegin => "SuppressBegin".to_string(), + EffectOpcode::SuppressEnd => "SuppressEnd".to_string(), } } diff --git a/crates/plotnik-lib/src/compile/capture.rs b/crates/plotnik-lib/src/compile/capture.rs index 1095c3b5..3eeafd9e 100644 --- a/crates/plotnik-lib/src/compile/capture.rs +++ b/crates/plotnik-lib/src/compile/capture.rs @@ -66,7 +66,7 @@ impl Compiler<'_> { // Always look up in the current scope - bubble captures don't create new scopes, // so all fields (including nested bubble captures) reference the same root struct. if let Some(name_token) = cap.name() { - let capture_name = name_token.text(); + let capture_name = &name_token.text()[1..]; // Strip @ prefix let member_ref = self.lookup_member_in_scope(capture_name); if let Some(member_ref) = member_ref { effects.push(EffectIR::with_member(EffectOpcode::Set, member_ref)); @@ -132,7 +132,7 @@ impl Compiler<'_> { if let Expr::CapturedExpr(cap) = expr && let Some(name) = cap.name() { - names.insert(name.text().to_string()); + names.insert(name.text()[1..].to_string()); // Strip @ prefix } for child in expr.children() { collect(&child, names); diff --git a/crates/plotnik-lib/src/compile/expressions.rs b/crates/plotnik-lib/src/compile/expressions.rs index 12f57219..c140a1f3 100644 --- a/crates/plotnik-lib/src/compile/expressions.rs +++ b/crates/plotnik-lib/src/compile/expressions.rs @@ -9,8 +9,8 @@ use std::num::NonZeroU16; -use crate::bytecode::ir::{CallIR, Instruction, Label, MatchIR}; -use crate::bytecode::Nav; +use crate::bytecode::ir::{CallIR, EffectIR, Instruction, Label, MatchIR}; +use crate::bytecode::{EffectOpcode, Nav}; use crate::parser::ast::{self, Expr}; use super::capture::CaptureEffects; @@ -164,18 +164,7 @@ impl Compiler<'_> { let return_addr = if capture.post.is_empty() { exit } else { - let effects_label = self.fresh_label(); - self.instructions.push(Instruction::Match(MatchIR { - label: effects_label, - nav: Nav::Stay, - node_type: None, - node_field: None, - pre_effects: vec![], - neg_fields: vec![], - post_effects: capture.post, - successors: vec![exit], - })); - effects_label + self.emit_effects_epsilon(exit, capture.post, CaptureEffects::default()) }; // Emit Call instruction with caller-provided navigation and field constraint. @@ -281,6 +270,7 @@ impl Compiler<'_> { /// - Struct: Obj epsilon → inner_pattern[Node/Text, Set] → EndObj epsilon → exit /// - Array: Arr epsilon → quantifier (with Push on body) → EndArr+Set epsilon → exit /// - Ref: Call → Set epsilon → exit (structured result needs epsilon) + /// - Suppressive: SuppressBegin → inner → SuppressEnd → outer_effects → exit pub(super) fn compile_captured_inner( &mut self, cap: &ast::CapturedExpr, @@ -288,6 +278,11 @@ impl Compiler<'_> { nav_override: Option