Skip to main content

polydat_core/dsl/
events.rs

1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! Polydat compiler diagnostic event stream.
5//!
6//! The compiler emits typed events for each step: parsing, binding
7//! resolution, module inlining, type adaptation, constant folding,
8//! fusion, and compilation level selection.
9//!
10//! Events are tagged with severity levels:
11//! - **Info**: normal compilation steps (parsed, resolved, folded)
12//! - **Advisory**: type coercions, widenings, and implicit conversions
13//!   that the user should be aware of for module design quality
14//! - **Warning**: potential performance or correctness issues
15//! - **Error**: compilation failures (surfaced as Result::Err, not events)
16
17/// Severity level for compiler diagnostic events.
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum EventLevel {
20    /// Normal compilation step — informational only.
21    Info,
22    /// Design advisory — implicit conversion or coercion that the user
23    /// should review for module quality. Query with `--diagnose`.
24    Advisory,
25    /// Potential performance or correctness issue.
26    Warning,
27}
28
29/// A diagnostic event from the Polydat compilation pipeline.
30#[derive(Debug, Clone)]
31pub enum CompileEvent {
32    /// DSL source parsed into AST.
33    Parsed {
34        /// Top-level statements in the file.
35        statements: usize,
36    },
37    /// A binding was resolved from DSL to a node.
38    BindingResolved {
39        /// The binding's name.
40        name: String,
41        /// The node type it resolved to.
42        node_type: String,
43    },
44    /// A module was loaded and inlined.
45    ModuleInlined {
46        /// The module's name.
47        name: String,
48        /// Nodes the inlining added to the graph.
49        nodes_added: usize,
50    },
51    /// A legacy binding chain was translated to Polydat source.
52    LegacyTranslated {
53        /// The binding's name.
54        name: String,
55        /// The Polydat expression it became.
56        polydat_expr: String,
57    },
58    /// Type adapter inserted between mismatched ports.
59    TypeAdapterInserted {
60        /// The producing node.
61        from_node: String,
62        /// The consuming node.
63        to_node: String,
64        /// The adapter node inserted between them.
65        adapter: String,
66    },
67    /// Init-time constant folded (SRD 44).
68    ConstantFolded {
69        /// The node folded.
70        node: String,
71        /// The constant's rendered value.
72        value: String,
73    },
74    /// Fusion pattern matched and applied (SRD 36).
75    FusionApplied {
76        /// The fusion pattern's name.
77        pattern: String,
78        /// Nodes the fused node replaced.
79        nodes_replaced: usize,
80    },
81    /// Output declared.
82    OutputDeclared {
83        /// The output's name.
84        name: String,
85    },
86    /// Compilation level selected for a node.
87    CompileLevelSelected {
88        /// The node.
89        node: String,
90        /// The level's name.
91        level: String,
92    },
93    /// Workload parameter injected as constant.
94    ParamInjected {
95        /// The parameter's name.
96        name: String,
97        /// The value injected.
98        value: String,
99    },
100    /// Config wire connected to a cycle-time source (performance warning).
101    ConfigWireCycleWarning {
102        /// The consuming node.
103        node: String,
104        /// The config port fed by a cycle-time source.
105        port: String,
106    },
107    /// Auto-widening type coercion inserted by the compiler.
108    TypeWidening {
109        /// The source type.
110        from: &'static str,
111        /// The type widened to.
112        to: &'static str,
113        /// Where the widening was inserted.
114        context: String,
115    },
116    /// Warning during compilation.
117    Warning {
118        /// The warning text.
119        message: String,
120    },
121    /// An extern with no default: `None` until the host sets it, and
122    /// every consumer reads `None` through it (engine_parity.md, A12).
123    ExternWithoutDefault {
124        /// The extern's name.
125        name: String,
126        /// Its declared type.
127        port_type: String,
128    },
129    /// Summary of the compiled program.
130    Summary {
131        /// Nodes in the compiled graph.
132        nodes: usize,
133        /// Declared outputs.
134        outputs: usize,
135        /// Init-time constants folded.
136        constants_folded: usize,
137    },
138    /// A module-level pragma was acknowledged. Recorded once per
139    /// recognised `// @pragma: <name>` directive at the top of the
140    /// source. Lets `--diagnose` show which graph transforms the
141    /// module asked for.
142    PragmaAcknowledged {
143        /// The pragma's name.
144        name: String,
145        /// The source line it appears on.
146        line: usize,
147    },
148    /// An unrecognised module-level pragma was seen. Pragmas are
149    /// forward-compatible: an old binary parses a newer module
150    /// that opts into features it doesn't support, and the only
151    /// effect is this advisory.
152    UnknownPragma {
153        /// The pragma's name.
154        name: String,
155        /// The source line it appears on.
156        line: usize,
157    },
158    /// Strict-wire mode auto-inserted an assertion node between
159    /// `from_node` and `to_node`. SRD 15 §"Strict Wire Mode".
160    AssertionInserted {
161        /// The producing node.
162        from_node: String,
163        /// The consuming node.
164        to_node: String,
165        /// The assertion kind inserted.
166        kind: String,
167    },
168    /// Strict-wire mode considered inserting an assertion but
169    /// proved it redundant. The reason field names which skip
170    /// rule applied (constant source, upstream assertion, etc.).
171    AssertionSkipped {
172        /// The producing node.
173        from_node: String,
174        /// The consuming node.
175        to_node: String,
176        /// The skip rule that applied.
177        reason: String,
178    },
179    /// A tile hole was typed (SRD 114 §4): its expression, the wire
180    /// type the compiler inferred, the declared type if any, the
181    /// contextual expectation of its position, the encoder chosen, and
182    /// the adapter inserted between wire and declared type if one was.
183    TileHoleTyped {
184        /// The tile's name.
185        tile: String,
186        /// The hole's expression text.
187        hole: String,
188        /// The wire type the compiler inferred.
189        wire_type: String,
190        /// The declared type, if any.
191        declared: Option<String>,
192        /// The contextual expectation of the hole's position.
193        expectation: String,
194        /// The encoder chosen.
195        encoder: String,
196        /// The adapter inserted between wire and declared type, if any.
197        adapter: Option<String>,
198    },
199    /// A tile's skeleton (SRD 114 §6, §10): how many static runs it
200    /// copies and their byte total, its holes, branches, and
201    /// projections, and the source of each projection body program.
202    TileCompiled {
203        /// The tile's name.
204        tile: String,
205        /// The tile's encoding.
206        encoding: String,
207        /// Static runs the skeleton copies.
208        statics: usize,
209        /// Their byte total.
210        static_bytes: usize,
211        /// Holes.
212        holes: usize,
213        /// Branches.
214        branches: usize,
215        /// Projections.
216        projections: usize,
217        /// The source of each projection body program.
218        bodies: Vec<String>,
219    },
220}
221
222impl CompileEvent {
223    /// The severity level of this event.
224    pub fn level(&self) -> EventLevel {
225        match self {
226            // Info: normal steps
227            CompileEvent::Parsed { .. } => EventLevel::Info,
228            CompileEvent::BindingResolved { .. } => EventLevel::Info,
229            CompileEvent::ModuleInlined { .. } => EventLevel::Info,
230            CompileEvent::OutputDeclared { .. } => EventLevel::Info,
231            CompileEvent::CompileLevelSelected { .. } => EventLevel::Info,
232            CompileEvent::ParamInjected { .. } => EventLevel::Info,
233            CompileEvent::ConstantFolded { .. } => EventLevel::Info,
234            CompileEvent::FusionApplied { .. } => EventLevel::Info,
235            CompileEvent::Summary { .. } => EventLevel::Info,
236            CompileEvent::TileHoleTyped { adapter: None, .. } => EventLevel::Info,
237            CompileEvent::TileCompiled { .. } => EventLevel::Info,
238
239            // Advisory: implicit conversions the user should review
240            CompileEvent::TileHoleTyped {
241                adapter: Some(_), ..
242            } => EventLevel::Advisory,
243            CompileEvent::TypeAdapterInserted { .. } => EventLevel::Advisory,
244            CompileEvent::TypeWidening { .. } => EventLevel::Advisory,
245            CompileEvent::LegacyTranslated { .. } => EventLevel::Advisory,
246            CompileEvent::PragmaAcknowledged { .. } => EventLevel::Advisory,
247            CompileEvent::AssertionInserted { .. } => EventLevel::Advisory,
248            CompileEvent::AssertionSkipped { .. } => EventLevel::Advisory,
249
250            // Warning: potential issues
251            CompileEvent::ConfigWireCycleWarning { .. } => EventLevel::Warning,
252            CompileEvent::Warning { .. } => EventLevel::Warning,
253            CompileEvent::ExternWithoutDefault { .. } => EventLevel::Warning,
254            CompileEvent::UnknownPragma { .. } => EventLevel::Warning,
255        }
256    }
257}
258
259/// Collects diagnostic events during compilation.
260#[derive(Debug, Default)]
261pub struct CompileEventLog {
262    events: Vec<CompileEvent>,
263}
264
265impl CompileEventLog {
266    /// An empty log.
267    pub fn new() -> Self {
268        Self { events: Vec::new() }
269    }
270
271    /// Record an event.
272    pub fn push(&mut self, event: CompileEvent) {
273        self.events.push(event);
274    }
275
276    /// Every event recorded, in order.
277    pub fn events(&self) -> &[CompileEvent] {
278        &self.events
279    }
280
281    /// Whether no event has been recorded.
282    pub fn is_empty(&self) -> bool {
283        self.events.is_empty()
284    }
285
286    /// Return only advisory-level events (type coercions, widenings).
287    /// These are the "module design quality" messages users query with --diagnose.
288    pub fn advisories(&self) -> Vec<&CompileEvent> {
289        self.events
290            .iter()
291            .filter(|e| e.level() == EventLevel::Advisory)
292            .collect()
293    }
294
295    /// Return only warning-level events.
296    pub fn warnings(&self) -> Vec<&CompileEvent> {
297        self.events
298            .iter()
299            .filter(|e| e.level() == EventLevel::Warning)
300            .collect()
301    }
302
303    /// Format all events as human-readable diagnostic lines.
304    /// Each line is prefixed with the severity tag.
305    pub fn format(&self) -> String {
306        self.events.iter().map(|e| {
307            let tag = match e.level() {
308                EventLevel::Info => "info",
309                EventLevel::Advisory => "advisory",
310                EventLevel::Warning => "warning",
311            };
312            let msg = match e {
313            CompileEvent::Parsed { statements } =>
314                format!("parsed {statements} statement(s)"),
315            CompileEvent::BindingResolved { name, node_type } =>
316                format!("resolved '{name}' → {node_type}"),
317            CompileEvent::ModuleInlined { name, nodes_added } =>
318                format!("module '{name}' inlined ({nodes_added} nodes)"),
319            CompileEvent::LegacyTranslated { name, polydat_expr } =>
320                format!("legacy '{name}' → {polydat_expr}"),
321            CompileEvent::TypeAdapterInserted { from_node, to_node, adapter } =>
322                format!("type adapter {adapter}: {from_node} → {to_node}"),
323            CompileEvent::ConstantFolded { node, value } =>
324                format!("constant folded: {node} → {value}"),
325            CompileEvent::FusionApplied { pattern, nodes_replaced } =>
326                format!("fusion: {pattern} ({nodes_replaced} nodes replaced)"),
327            CompileEvent::OutputDeclared { name } =>
328                format!("output '{name}'"),
329            CompileEvent::CompileLevelSelected { node, level } =>
330                format!("{node} → {level}"),
331            CompileEvent::ParamInjected { name, value } =>
332                format!("param '{name}' = {value}"),
333            CompileEvent::ConfigWireCycleWarning { node, port } =>
334                format!("config wire '{port}' on '{node}' connected to cycle-time source"),
335            CompileEvent::TypeWidening { from, to, context } =>
336                format!("widening {from} → {to} in {context}"),
337            CompileEvent::Warning { message } =>
338                message.to_string(),
339            CompileEvent::ExternWithoutDefault { name, port_type } =>
340                format!("extern '{name}' ({port_type}) has no default: it is `None` until the host sets it"),
341            CompileEvent::Summary { nodes, outputs, constants_folded } =>
342                format!("{nodes} nodes, {outputs} outputs, {constants_folded} constant(s) folded"),
343            CompileEvent::PragmaAcknowledged { name, line } =>
344                format!("pragma '{name}' acknowledged (line {line})"),
345            CompileEvent::UnknownPragma { name, line } =>
346                format!("unknown pragma '{name}' at line {line}; ignored"),
347            CompileEvent::AssertionInserted { from_node, to_node, kind } =>
348                format!("assertion inserted: {from_node} → {to_node} ({kind})"),
349            CompileEvent::AssertionSkipped { from_node, to_node, reason } =>
350                format!("assertion skipped: {from_node} → {to_node} ({reason})"),
351            CompileEvent::TileHoleTyped { tile, hole, wire_type, declared, expectation, encoder, adapter } =>
352                format!(
353                    "tile '{tile}' hole `{hole}`: wire {wire_type}{} expects {expectation}, encoder {encoder}{}",
354                    declared.as_ref().map(|d| format!(", declared {d},")).unwrap_or_else(|| ",".to_string()),
355                    adapter.as_ref().map(|a| format!(", adapter {a}")).unwrap_or_default()
356                ),
357            CompileEvent::TileCompiled { tile, encoding, statics, static_bytes, holes, branches, projections, .. } =>
358                format!(
359                    "tile '{tile}' ({encoding}): {statics} static run(s), {static_bytes} bytes; {holes} hole(s), {branches} branch(es), {projections} projection(s)"
360                ),
361            };
362            format!("polydat[{tag}]: {msg}")
363        }).collect::<Vec<_>>().join("\n")
364    }
365}