Skip to main content

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}