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}