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}