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