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}