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