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`. 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}