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`. 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 program has its own [`PragmaSet`], collected once at
44//! the root by [`collect_from_ast`]. Inner contexts inherit the outer
45//! scope's pragmas: a `for` body compiles under a clone of its
46//! parent's set, so an enclosing `strict_values` applies to every
47//! nested body. [`PragmaSet::attach_to`] and [`PragmaConflict`]
48//! model a parent chain with outer-wins conflict resolution; the
49//! compiler does not use them today.
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 through their `parent` field, set by
64/// [`PragmaSet::attach_to`], to model nested scopes
65/// (program → `for` body). Per SRD 13b
66/// §"Scope composition" + SRD 15 §"Pragma Scope": each scope is
67/// its own `PragmaSet`, the chain is walked at lookup time, and
68/// outer scopes win on conflict.
69#[derive(Debug, Clone, Default)]
70pub struct PragmaSet {
71 /// The pragmas declared in this scope, in order.
72 pub entries: Vec<Pragma>,
73 /// Outer scope, if any. Lookups walk this chain after their
74 /// own entries miss; conflicts are detected at attach time
75 /// via [`PragmaSet::attach_to`]. The `Arc` keeps the outer
76 /// scope cheap to share across many child scopes (e.g. one
77 /// workload scope feeding a fan-out of phase scopes).
78 pub parent: Option<std::sync::Arc<PragmaSet>>,
79}
80
81impl PragmaSet {
82 /// Returns true if the named pragma is present in this scope
83 /// or any enclosing scope.
84 pub fn contains(&self, name: &str) -> bool {
85 if self.entries.iter().any(|p| p.name == name) {
86 return true;
87 }
88 match &self.parent {
89 Some(p) => p.contains(name),
90 None => false,
91 }
92 }
93
94 /// Returns true if either `strict_types` or the `strict` alias
95 /// is set in this scope or any enclosing scope.
96 pub fn strict_types(&self) -> bool {
97 self.contains("strict_types") || self.contains("strict")
98 }
99
100 /// Returns true if either `strict_values` or the `strict`
101 /// alias is set in this scope or any enclosing scope.
102 pub fn strict_values(&self) -> bool {
103 self.contains("strict_values") || self.contains("strict")
104 }
105
106 /// Iterate pragmas this scope declares that the compiler
107 /// doesn't recognise. Local-only — does not walk parents (the
108 /// outer scope already reported its own unknowns at its own
109 /// compile time).
110 pub fn unknown(&self) -> impl Iterator<Item = &Pragma> {
111 self.entries.iter().filter(|p| !is_known(&p.name))
112 }
113
114 /// Attach this `PragmaSet` to an outer scope, returning
115 /// `(attached, conflicts)`. Conflicts arise when this scope
116 /// declares a pragma whose effective value (currently just
117 /// `args`) differs from a same-named declaration in the
118 /// outer chain. Outer wins; the conflict is returned for
119 /// diagnostic reporting.
120 ///
121 /// The caller decides what to do with conflicts:
122 /// - non-strict: emit warning event(s)
123 /// - strict: turn each conflict into a compile error
124 ///
125 /// Today's pragma vocabulary is presence-only so `args` is
126 /// always empty; conflicts are degenerate. The framework is
127 /// in place for future value-bearing pragmas. The compiler
128 /// does not call this today: a `for` body inherits a clone of
129 /// its parent's set.
130 pub fn attach_to(self, outer: std::sync::Arc<PragmaSet>) -> (PragmaSet, Vec<PragmaConflict>) {
131 let mut conflicts = Vec::new();
132 for entry in &self.entries {
133 // Walk the outer chain looking for a same-named
134 // declaration with disagreeing args.
135 let mut cursor: &PragmaSet = outer.as_ref();
136 loop {
137 if let Some(existing) = cursor.entries.iter().find(|p| p.name == entry.name)
138 && existing.args != entry.args
139 {
140 conflicts.push(PragmaConflict {
141 name: entry.name.clone(),
142 outer_line: existing.line,
143 inner_line: entry.line,
144 });
145 break;
146 }
147 match &cursor.parent {
148 Some(p) => cursor = p.as_ref(),
149 None => break,
150 }
151 }
152 }
153 let attached = PragmaSet {
154 entries: self.entries,
155 parent: Some(outer),
156 };
157 (attached, conflicts)
158 }
159}
160
161/// Recognised pragma names. Add new names here as features land.
162fn is_known(name: &str) -> bool {
163 matches!(name, "strict_types" | "strict_values" | "strict")
164}
165
166/// Walk a parsed AST and collect every `Statement::Pragma` into a
167/// [`PragmaSet`]. This is the canonical extraction path — pragmas
168/// are first-class grammar (the `pragma` keyword) and the parser
169/// produces them as proper statements.
170pub fn collect_from_ast(file: &crate::ast::PolydatFile) -> PragmaSet {
171 use crate::ast::Statement;
172 let mut entries = Vec::new();
173 for stmt in &file.statements {
174 if let Statement::Pragma { name, span } = stmt {
175 entries.push(Pragma {
176 name: name.clone(),
177 args: Vec::new(),
178 line: span.line,
179 });
180 }
181 }
182 PragmaSet {
183 entries,
184 parent: None,
185 }
186}
187
188/// A pragma that disagreed across nested scopes. Used by
189/// [`PragmaSet::attach_to`] to surface conflicts up to the caller
190/// for either advisory logging (non-strict) or hard error (strict).
191/// Per SRD 15 §"Pragma Scope" + SRD 13b §"Scope composition", the
192/// outer scope's value wins; the conflict report is for
193/// diagnostics, not for resolution.
194#[derive(Debug, Clone)]
195pub struct PragmaConflict {
196 /// The pragma's name.
197 pub name: String,
198 /// The line the outer scope declares it on.
199 pub outer_line: usize,
200 /// The line the inner scope declares it on.
201 pub inner_line: usize,
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207 use crate::lexer::lex;
208 use crate::parser::parse;
209
210 fn pragmas_from(src: &str) -> PragmaSet {
211 let tokens = lex(src).expect("lex");
212 let ast = parse(tokens).expect("parse");
213 collect_from_ast(&ast)
214 }
215
216 #[test]
217 fn parse_strict_alias() {
218 let set = pragmas_from("pragma strict\nid := cycle\n");
219 assert!(set.strict_types());
220 assert!(set.strict_values());
221 }
222
223 #[test]
224 fn parse_individual_modes() {
225 let set = pragmas_from("pragma strict_types\npragma strict_values\nid := cycle\n");
226 assert!(set.strict_types());
227 assert!(set.strict_values());
228 }
229
230 #[test]
231 fn unknown_pragmas_are_collected() {
232 let set = pragmas_from("pragma warp_drive\npragma strict\nid := cycle\n");
233 assert!(set.strict_types());
234 let unknown: Vec<_> = set.unknown().collect();
235 assert_eq!(unknown.len(), 1);
236 assert_eq!(unknown[0].name, "warp_drive");
237 }
238
239 #[test]
240 fn attached_inherits_outer_pragmas() {
241 let outer = std::sync::Arc::new(PragmaSet {
242 entries: vec![Pragma {
243 name: "strict_values".into(),
244 args: vec![],
245 line: 1,
246 }],
247 parent: None,
248 });
249 let inner = PragmaSet::default();
250 let (attached, conflicts) = inner.attach_to(outer);
251 assert!(
252 attached.strict_values(),
253 "inner should see outer's strict_values via parent walk"
254 );
255 assert!(conflicts.is_empty());
256 }
257
258 #[test]
259 fn attached_local_pragma_wins_for_unrelated_names() {
260 // Outer says strict_types, inner adds strict_values. No
261 // conflict — both apply via the chain walk.
262 let outer = std::sync::Arc::new(PragmaSet {
263 entries: vec![Pragma {
264 name: "strict_types".into(),
265 args: vec![],
266 line: 1,
267 }],
268 parent: None,
269 });
270 let inner = PragmaSet {
271 entries: vec![Pragma {
272 name: "strict_values".into(),
273 args: vec![],
274 line: 5,
275 }],
276 parent: None,
277 };
278 let (attached, conflicts) = inner.attach_to(outer);
279 assert!(attached.strict_types());
280 assert!(attached.strict_values());
281 assert!(conflicts.is_empty());
282 }
283
284 #[test]
285 fn attached_records_arg_conflict() {
286 // Forward-compat scenario: a value-bearing pragma like
287 // `assert_for(name)` that disagrees across scopes. The
288 // keyword grammar doesn't accept args today, so build the
289 // PragmaSet by hand. Outer wins; conflict is reported.
290 let outer = std::sync::Arc::new(PragmaSet {
291 entries: vec![Pragma {
292 name: "assert_for".into(),
293 args: vec!["alpha".into()],
294 line: 1,
295 }],
296 parent: None,
297 });
298 let inner = PragmaSet {
299 entries: vec![Pragma {
300 name: "assert_for".into(),
301 args: vec!["beta".into()],
302 line: 5,
303 }],
304 parent: None,
305 };
306 let (_attached, conflicts) = inner.attach_to(outer);
307 assert_eq!(conflicts.len(), 1);
308 assert_eq!(conflicts[0].name, "assert_for");
309 assert_eq!(conflicts[0].outer_line, 1);
310 assert_eq!(conflicts[0].inner_line, 5);
311 }
312}