Skip to main content

polydat_grammar/
pragmas.rs

1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! Pragmas for Polydat source.
5//!
6//! Pragmas are first-class Polydat statements that opt a scope into
7//! compile-time checks (polydat_grammar.md §14). They cover the
8//! strict-wire modes that complement the const-constraint metadata
9//! (graph_compiler.md §2):
10//!
11//! ```polydat
12//! pragma strict_values
13//! pragma strict_types
14//! pragma strict          // both, and strict name checking
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 are a distinct syntactic
22//! construct, not comments.
23//!
24//! [`Statement::Pragma`]: crate::ast::Statement::Pragma
25//!
26//! ## Recognised pragma names
27//!
28//! - `strict_values` — check every wire into a port that declares a
29//!   value constraint: a compile-time constant source is checked at
30//!   build, and any other source gets a value assertion node.
31//! - `strict_types` — accepted and acknowledged, with no effect on the
32//!   graph. Wires are statically typed and a resolved wire's type is
33//!   the sink port's type, so a runtime type assertion has nothing to
34//!   catch.
35//! - `strict` — a pragma of its own: it implies `strict_types` and
36//!   `strict_values`, and it turns on strict name checking, under which
37//!   a comprehension that reads a name nothing binds is refused
38//!   (comprehension_forms.md §5 V3). Outside it such a name compiles
39//!   with a warning and reads None.
40//!
41//! Unknown pragmas are recorded and warned about, not errored:
42//! pragmas are forward-compatible by design so old binaries can
43//! parse modules that opt into newer features they don't
44//! support.
45//!
46//! ## Scoping
47//!
48//! Scoping is lexical. A pragma applies to the scope it is written in
49//! and to every scope nested in it: a program, a `for` body, and a
50//! module body are scopes. A `for` body is nested in the scope it is
51//! written in and compiles under [`PragmaSet::nested`], its enclosing
52//! set plus the pragmas its own statements declare. A module body is
53//! nested in no host and compiles under its own pragmas alone. A tile
54//! is not a scope: it compiles under the set of the scope it is
55//! written in. Pragmas are presence-only, so a nested scope can add to
56//! the set and never conflicts with it.
57//!
58//! A host that compiles strictly seeds the program's top scope with
59//! `strict` ([`PragmaSet::host`]): the host switch and a `pragma strict`
60//! at the top of the program mean the same thing.
61
62use crate::ast::Statement;
63
64/// One pragma entry parsed from the source.
65#[derive(Debug, Clone, PartialEq, Eq)]
66pub struct Pragma {
67    /// Bare pragma name (e.g. `"strict_values"`).
68    pub name: String,
69    /// Whitespace-separated arguments after the name, if any.
70    pub args: Vec<String>,
71    /// 1-based line number where the pragma appeared, for diagnostics;
72    /// 0 for a pragma the host seeds ([`PragmaSet::host`]).
73    pub line: usize,
74}
75
76/// The pragmas in force in one Polydat scope: those the scope declares
77/// and those of every scope enclosing it.
78#[derive(Debug, Clone, Default)]
79pub struct PragmaSet {
80    /// The pragmas in force, the enclosing scopes' first, in order.
81    pub entries: Vec<Pragma>,
82}
83
84impl PragmaSet {
85    /// The set a program's top scope starts from before its own pragmas
86    /// are added: `strict` when the host asks for strict compilation
87    /// (`CompileOptions::strict`, the binary's `--strict`), and empty
88    /// otherwise. The seeded `strict` is in force exactly as a
89    /// `pragma strict` written at the top of the program is, so every
90    /// scope that inherits the top scope's set inherits it, and a module
91    /// body, which compiles under its own pragmas alone, does not.
92    pub fn host(strict: bool) -> PragmaSet {
93        let entries = if strict {
94            vec![Pragma {
95                name: "strict".to_string(),
96                args: Vec::new(),
97                line: 0,
98            }]
99        } else {
100            Vec::new()
101        };
102        PragmaSet { entries }
103    }
104
105    /// Returns true if the named pragma is in force.
106    pub fn contains(&self, name: &str) -> bool {
107        self.entries.iter().any(|p| p.name == name)
108    }
109
110    /// Returns true if either `strict_types` or `strict`, which implies
111    /// it, is in force.
112    pub fn strict_types(&self) -> bool {
113        self.contains("strict_types") || self.contains("strict")
114    }
115
116    /// Returns true if either `strict_values` or `strict`, which implies
117    /// it, is in force.
118    pub fn strict_values(&self) -> bool {
119        self.contains("strict_values") || self.contains("strict")
120    }
121
122    /// Returns true if strict name checking is in force: `strict`, and no
123    /// other pragma. A comprehension read in this scope that reads a
124    /// name nothing binds is then refused (comprehension_forms.md §5 V3).
125    pub fn strict_names(&self) -> bool {
126        self.contains("strict")
127    }
128
129    /// Iterate the pragmas in force that the compiler doesn't
130    /// recognise.
131    pub fn unknown(&self) -> impl Iterator<Item = &Pragma> {
132        self.entries.iter().filter(|p| !is_known(&p.name))
133    }
134
135    /// The set a scope nested in this one compiles under: every pragma
136    /// in force here, then the pragmas `statements` (the nested
137    /// scope's own) declare. Pragmas in scopes nested inside
138    /// `statements` are not collected; each applies when its own scope
139    /// compiles.
140    pub fn nested(&self, statements: &[Statement]) -> PragmaSet {
141        let mut entries = self.entries.clone();
142        entries.extend(declared_in(statements));
143        PragmaSet { entries }
144    }
145}
146
147/// Recognised pragma names. Add new names here as features land.
148pub fn is_known(name: &str) -> bool {
149    matches!(name, "strict_types" | "strict_values" | "strict")
150}
151
152/// The pragmas `statements` declare at their own level.
153pub fn declared_in(statements: &[Statement]) -> impl Iterator<Item = Pragma> + '_ {
154    statements.iter().filter_map(|stmt| match stmt {
155        Statement::Pragma { name, span } => Some(Pragma {
156            name: name.clone(),
157            args: Vec::new(),
158            line: span.line,
159        }),
160        _ => None,
161    })
162}
163
164/// Walk a parsed program and collect the pragmas it declares at its
165/// top level into a [`PragmaSet`]: the set the program's own bindings
166/// compile under. Pragmas inside a `for` body or a module body belong
167/// to that scope.
168pub fn collect_from_ast(file: &crate::ast::PolydatFile) -> PragmaSet {
169    PragmaSet::default().nested(&file.statements)
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175    use crate::lexer::lex;
176    use crate::parser::parse;
177
178    fn pragmas_from(src: &str) -> PragmaSet {
179        let tokens = lex(src).expect("lex");
180        let ast = parse(tokens).expect("parse");
181        collect_from_ast(&ast)
182    }
183
184    fn statements(src: &str) -> Vec<Statement> {
185        parse(lex(src).expect("lex")).expect("parse").statements
186    }
187
188    #[test]
189    fn strict_implies_both_modes_and_strict_names() {
190        let set = pragmas_from("pragma strict\nid := cycle\n");
191        assert!(set.strict_types());
192        assert!(set.strict_values());
193        assert!(set.strict_names());
194    }
195
196    /// Only `strict` checks names strictly: the two modes together do not.
197    #[test]
198    fn parse_individual_modes() {
199        let set = pragmas_from("pragma strict_types\npragma strict_values\nid := cycle\n");
200        assert!(set.strict_types());
201        assert!(set.strict_values());
202        assert!(!set.strict_names());
203    }
204
205    #[test]
206    fn unknown_pragmas_are_collected() {
207        let set = pragmas_from("pragma warp_drive\npragma strict\nid := cycle\n");
208        assert!(set.strict_types());
209        let unknown: Vec<_> = set.unknown().collect();
210        assert_eq!(unknown.len(), 1);
211        assert_eq!(unknown[0].name, "warp_drive");
212    }
213
214    #[test]
215    fn host_strict_seeds_strict_under_the_program_pragmas() {
216        let set = PragmaSet::host(true).nested(&statements("pragma strict_values\nid := cycle\n"));
217        assert!(set.strict_names() && set.strict_types() && set.strict_values());
218        assert_eq!(set.unknown().count(), 0);
219        assert!(PragmaSet::host(false).entries.is_empty());
220    }
221
222    #[test]
223    fn nested_scope_inherits_the_enclosing_set() {
224        let outer = pragmas_from("pragma strict_values\nid := cycle\n");
225        let inner = outer.nested(&statements("x := cycle\n"));
226        assert!(inner.strict_values());
227    }
228
229    #[test]
230    fn nested_scope_adds_without_changing_the_enclosing_set() {
231        let outer = pragmas_from("pragma strict_types\nid := cycle\n");
232        let inner = outer.nested(&statements("pragma strict_values\nx := cycle\n"));
233        assert!(inner.strict_types() && inner.strict_values());
234        assert!(!outer.strict_values());
235    }
236}