polydat_grammar/pragmas.rs
1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! Module-level pragmas for Polydat source.
5//!
6//! Pragmas are first-class Polydat statements the module author places at
7//! the head of a `.polydat` file or module body to opt into compile-time
8//! graph transforms. Today they cover assertion-injection modes that
9//! complement the const-constraint metadata (SRD 15):
10//!
11//! ```polydat
12//! pragma strict_values
13//! pragma strict_types
14//! pragma strict // convenience alias for both
15//!
16//! id := mod(hash(cycle), 1000)
17//! ```
18//!
19//! `pragma` is a reserved keyword in the Polydat grammar; pragmas are
20//! [`Statement::Pragma`] in the AST and walked by the compiler the
21//! same way other statements are. They're not comments — distinct
22//! syntactic construct, distinguishable from `//`/`#` line comments.
23//!
24//! [`Statement::Pragma`]: crate::ast::Statement::Pragma
25//!
26//! ## Recognised pragma names
27//!
28//! - `strict_types` — auto-insert type assertion nodes on wires
29//! whose source can't be statically proven to deliver the right
30//! `PortType`. *Design target* — see SRD 15 §"Strict Wire Mode".
31//! - `strict_values` — auto-insert value assertion nodes on wires
32//! whose downstream node declares a value constraint the source
33//! can't satisfy at compile time.
34//! - `strict` — alias for both `strict_types` + `strict_values`.
35//!
36//! Unknown pragmas are recorded but warned about, not errored:
37//! pragmas are forward-compatible by design so old binaries can
38//! parse modules that opt into newer features they don't yet
39//! support.
40//!
41//! ## Scoping (SRD 15 §"Pragma Scope")
42//!
43//! Each Polydat graph or module has its own [`PragmaSet`]. Inner
44//! contexts inherit the outer scope's pragmas automatically — an
45//! enclosing `strict_values` applies to every nested module body
46//! attached to it. On conflict (an inner pragma whose effective
47//! value disagrees with an outer one), the outer scope wins; a
48//! warning is emitted in non-strict compilation, and the conflict
49//! becomes a hard error in `--strict` mode.
50
51/// One pragma entry parsed from the source.
52#[derive(Debug, Clone, PartialEq, Eq)]
53pub struct Pragma {
54 /// Bare pragma name (e.g. `"strict_values"`).
55 pub name: String,
56 /// Whitespace-separated arguments after the name, if any.
57 pub args: Vec<String>,
58 /// 1-based line number where the pragma appeared, for diagnostics.
59 pub line: usize,
60}
61
62/// All pragmas declared in one Polydat scope. Multiple `PragmaSet`s
63/// chain via `PragmaSet::with_parent` to model nested scopes
64/// (workload → phase → `for_each` iteration). Per SRD 13b
65/// §"Scope composition" + SRD 15 §"Pragma Scope": each scope is
66/// its own `PragmaSet`, the chain is walked at lookup time, and
67/// outer scopes win on conflict.
68#[derive(Debug, Clone, Default)]
69pub struct PragmaSet {
70 /// The pragmas declared in this scope, in order.
71 pub entries: Vec<Pragma>,
72 /// Outer scope, if any. Lookups walk this chain after their
73 /// own entries miss; conflicts are detected at attach time
74 /// via [`PragmaSet::attach_to`]. The `Arc` keeps the outer
75 /// scope cheap to share across many child scopes (e.g. one
76 /// workload scope feeding a fan-out of phase scopes).
77 pub parent: Option<std::sync::Arc<PragmaSet>>,
78}
79
80impl PragmaSet {
81 /// Returns true if the named pragma is present in this scope
82 /// or any enclosing scope.
83 pub fn contains(&self, name: &str) -> bool {
84 if self.entries.iter().any(|p| p.name == name) {
85 return true;
86 }
87 match &self.parent {
88 Some(p) => p.contains(name),
89 None => false,
90 }
91 }
92
93 /// Returns true if either `strict_types` or the `strict` alias
94 /// is set in this scope or any enclosing scope.
95 pub fn strict_types(&self) -> bool {
96 self.contains("strict_types") || self.contains("strict")
97 }
98
99 /// Returns true if either `strict_values` or the `strict`
100 /// alias is set in this scope or any enclosing scope.
101 pub fn strict_values(&self) -> bool {
102 self.contains("strict_values") || self.contains("strict")
103 }
104
105 /// Iterate pragmas this scope declares that the compiler
106 /// doesn't recognise. Local-only — does not walk parents (the
107 /// outer scope already reported its own unknowns at its own
108 /// compile time).
109 pub fn unknown(&self) -> impl Iterator<Item = &Pragma> {
110 self.entries.iter().filter(|p| !is_known(&p.name))
111 }
112
113 /// Attach this `PragmaSet` to an outer scope, returning
114 /// `(attached, conflicts)`. Conflicts arise when this scope
115 /// declares a pragma whose effective value (currently just
116 /// `args`) differs from a same-named declaration in the
117 /// outer chain. Outer wins; the conflict is returned for
118 /// diagnostic reporting.
119 ///
120 /// The caller decides what to do with conflicts:
121 /// - non-strict: emit warning event(s)
122 /// - strict: turn each conflict into a compile error
123 ///
124 /// Today's pragma vocabulary is presence-only so `args` is
125 /// always empty; conflicts are degenerate. The framework is
126 /// in place for future value-bearing pragmas.
127 pub fn attach_to(self, outer: std::sync::Arc<PragmaSet>) -> (PragmaSet, Vec<PragmaConflict>) {
128 let mut conflicts = Vec::new();
129 for entry in &self.entries {
130 // Walk the outer chain looking for a same-named
131 // declaration with disagreeing args.
132 let mut cursor: &PragmaSet = outer.as_ref();
133 loop {
134 if let Some(existing) = cursor.entries.iter().find(|p| p.name == entry.name)
135 && existing.args != entry.args
136 {
137 conflicts.push(PragmaConflict {
138 name: entry.name.clone(),
139 outer_line: existing.line,
140 inner_line: entry.line,
141 });
142 break;
143 }
144 match &cursor.parent {
145 Some(p) => cursor = p.as_ref(),
146 None => break,
147 }
148 }
149 }
150 let attached = PragmaSet {
151 entries: self.entries,
152 parent: Some(outer),
153 };
154 (attached, conflicts)
155 }
156}
157
158/// Recognised pragma names. Add new names here as features land.
159fn is_known(name: &str) -> bool {
160 matches!(name, "strict_types" | "strict_values" | "strict")
161}
162
163/// Walk a parsed AST and collect every `Statement::Pragma` into a
164/// [`PragmaSet`]. This is the canonical extraction path — pragmas
165/// are first-class grammar (the `pragma` keyword) and the parser
166/// produces them as proper statements.
167pub fn collect_from_ast(file: &crate::ast::PolydatFile) -> PragmaSet {
168 use crate::ast::Statement;
169 let mut entries = Vec::new();
170 for stmt in &file.statements {
171 if let Statement::Pragma { name, span } = stmt {
172 entries.push(Pragma {
173 name: name.clone(),
174 args: Vec::new(),
175 line: span.line,
176 });
177 }
178 }
179 PragmaSet {
180 entries,
181 parent: None,
182 }
183}
184
185/// A pragma that disagreed across nested scopes. Used by
186/// [`PragmaSet::attach_to`] to surface conflicts up to the caller
187/// for either advisory logging (non-strict) or hard error (strict).
188/// Per SRD 15 §"Pragma Scope" + SRD 13b §"Scope composition", the
189/// outer scope's value wins; the conflict report is for
190/// diagnostics, not for resolution.
191#[derive(Debug, Clone)]
192pub struct PragmaConflict {
193 /// The pragma's name.
194 pub name: String,
195 /// The line the outer scope declares it on.
196 pub outer_line: usize,
197 /// The line the inner scope declares it on.
198 pub inner_line: usize,
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204 use crate::lexer::lex;
205 use crate::parser::parse;
206
207 fn pragmas_from(src: &str) -> PragmaSet {
208 let tokens = lex(src).expect("lex");
209 let ast = parse(tokens).expect("parse");
210 collect_from_ast(&ast)
211 }
212
213 #[test]
214 fn parse_strict_alias() {
215 let set = pragmas_from("pragma strict\nid := cycle\n");
216 assert!(set.strict_types());
217 assert!(set.strict_values());
218 }
219
220 #[test]
221 fn parse_individual_modes() {
222 let set = pragmas_from("pragma strict_types\npragma strict_values\nid := cycle\n");
223 assert!(set.strict_types());
224 assert!(set.strict_values());
225 }
226
227 #[test]
228 fn unknown_pragmas_are_collected() {
229 let set = pragmas_from("pragma warp_drive\npragma strict\nid := cycle\n");
230 assert!(set.strict_types());
231 let unknown: Vec<_> = set.unknown().collect();
232 assert_eq!(unknown.len(), 1);
233 assert_eq!(unknown[0].name, "warp_drive");
234 }
235
236 #[test]
237 fn attached_inherits_outer_pragmas() {
238 let outer = std::sync::Arc::new(PragmaSet {
239 entries: vec![Pragma {
240 name: "strict_values".into(),
241 args: vec![],
242 line: 1,
243 }],
244 parent: None,
245 });
246 let inner = PragmaSet::default();
247 let (attached, conflicts) = inner.attach_to(outer);
248 assert!(
249 attached.strict_values(),
250 "inner should see outer's strict_values via parent walk"
251 );
252 assert!(conflicts.is_empty());
253 }
254
255 #[test]
256 fn attached_local_pragma_wins_for_unrelated_names() {
257 // Outer says strict_types, inner adds strict_values. No
258 // conflict — both apply via the chain walk.
259 let outer = std::sync::Arc::new(PragmaSet {
260 entries: vec![Pragma {
261 name: "strict_types".into(),
262 args: vec![],
263 line: 1,
264 }],
265 parent: None,
266 });
267 let inner = PragmaSet {
268 entries: vec![Pragma {
269 name: "strict_values".into(),
270 args: vec![],
271 line: 5,
272 }],
273 parent: None,
274 };
275 let (attached, conflicts) = inner.attach_to(outer);
276 assert!(attached.strict_types());
277 assert!(attached.strict_values());
278 assert!(conflicts.is_empty());
279 }
280
281 #[test]
282 fn attached_records_arg_conflict() {
283 // Forward-compat scenario: a value-bearing pragma like
284 // `assert_for(name)` that disagrees across scopes. The
285 // keyword grammar doesn't accept args today, so build the
286 // PragmaSet by hand. Outer wins; conflict is reported.
287 let outer = std::sync::Arc::new(PragmaSet {
288 entries: vec![Pragma {
289 name: "assert_for".into(),
290 args: vec!["alpha".into()],
291 line: 1,
292 }],
293 parent: None,
294 });
295 let inner = PragmaSet {
296 entries: vec![Pragma {
297 name: "assert_for".into(),
298 args: vec!["beta".into()],
299 line: 5,
300 }],
301 parent: None,
302 };
303 let (_attached, conflicts) = inner.attach_to(outer);
304 assert_eq!(conflicts.len(), 1);
305 assert_eq!(conflicts[0].name, "assert_for");
306 assert_eq!(conflicts[0].outer_line, 1);
307 assert_eq!(conflicts[0].inner_line, 5);
308 }
309}