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}