Skip to main content

vihaco_syntax/
lib.rs

1// SPDX-FileCopyrightText: 2026 The vihaco Authors
2// SPDX-License-Identifier: MIT
3
4//! Typed SST parsing built from pattern-derived instruction and type syntax.
5//!
6//! Section owners select their header type and combine device declarations with
7//! the derived `vihaco_module::module::ProgramMetadata` record. The existing
8//! [`ParsedModule`] flow retains `fn` bodies and can validate them against supplied
9//! metadata. Instructions use their existing `Lower<C>` implementations.
10//! [`ParsedModule::lower_with_metadata`] combines validation, function-scope
11//! preparation, instruction lowering, and module assembly. Its context can use
12//! `vihaco_module::loader::MetadataLoweringContext` directly or add consumer
13//! capabilities by implementing `vihaco_module::loader::FunctionScope`.
14//!
15//! With `program-image-execution`, `HeaderDeclarations<D>` parses a collection
16//! of semicolon-terminated declarations. Consumers apply declarations separately
17//! to an application-owned preparation context, then call
18//! `lower_functions_with_metadata` with the completed metadata and instruction
19//! context. This supports contexts borrowing prepared configuration without
20//! reconstructing a parsed module or moving its metadata during body lowering.
21//!
22//! Metadata records use `#[derive(Parse)]` and
23//! `#[syntax_class(metadata, open = "{", close = "}")]` with an optional `head`.
24//! See `vihaco-stdlib/examples/loadable.rs` for header declarations, metadata
25//! validation, and execution using dialect instructions consumed by components.
26//!
27//! A [`ParsedModule`] contains [`ParsedFunction`] values whose bodies are
28//! typed instruction vectors. Unknown or malformed instructions are parse
29//! errors. With `typed-declarations`, bodies can contain the generated syntax
30//! aggregate, lowered with a program loader's capabilities before execution.
31//! Labels retain their instruction positions without emitting executable slots.
32//! Feature-off consumers retain
33//! the separate `Resolve` bridge.
34
35mod lower;
36mod types;
37
38pub use lower::lower_functions;
39
40#[cfg(feature = "program-image-execution")]
41mod header;
42
43#[cfg(feature = "program-image-execution")]
44pub use header::HeaderDeclarations;
45
46#[cfg(feature = "program-image-execution")]
47pub use lower::lower_functions_with_metadata;
48
49pub mod parse;
50#[cfg(not(feature = "typed-declarations"))]
51pub mod resolve;
52
53#[cfg(feature = "typed-declarations")]
54pub use types::ParsedLabel;
55pub use types::{Param, ParsedFunction, ParsedModule};
56pub use vihaco_parser::SurfaceInstruction;
57
58pub use parse::{block_i64_flat, block_i64_pairs, skip};
59#[cfg(not(feature = "typed-declarations"))]
60pub use resolve::Resolve;
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65    use chumsky::Parser as _;
66    use vihaco_parser::Parse;
67
68    // Minimal stub: an enum that derives Parse and has just two unit variants.
69    // Avoids pulling vihaco-cpu/-fpga into the test (cycle).
70    #[derive(Debug, Clone, PartialEq, vihaco_parser_derive::Parse)]
71    #[syntax_class(instruction, head = "stub")]
72    enum StubInst {
73        Halt,
74        Print,
75    }
76
77    #[derive(Debug, Clone, PartialEq, vihaco_parser_derive::Parse)]
78    #[syntax_class(type)]
79    enum StubType {
80        #[pattern = "`unit`"]
81        Unit,
82    }
83
84    #[test]
85    fn parses_empty_function() {
86        let src = "fn @main() {}";
87        let f = ParsedFunction::<StubInst, StubType>::parser()
88            .parse(src)
89            .into_result()
90            .unwrap();
91        assert_eq!(f.name.as_str(), "main");
92        assert!(f.params.is_empty());
93        assert!(f.body.is_empty());
94    }
95
96    #[cfg(feature = "typed-declarations")]
97    #[test]
98    fn retains_label_positions_without_executable_slots() {
99        let f = ParsedFunction::<StubInst, StubType>::parser()
100            .parse("fn @main() { label @entry label @alias stub.halt label @exit }")
101            .into_result()
102            .unwrap();
103        assert_eq!(f.body, [StubInst::Halt]);
104        assert_eq!(
105            f.labels
106                .iter()
107                .map(|label| (label.name.as_str(), label.instruction_index))
108                .collect::<Vec<_>>(),
109            [("entry", 0), ("alias", 0), ("exit", 1)],
110        );
111    }
112
113    #[test]
114    fn parses_function_parameters() {
115        let src = "fn @main(input: unit, output: unit) {}";
116        let f = ParsedFunction::<StubInst, StubType>::parser()
117            .parse(src)
118            .into_result()
119            .unwrap();
120        assert_eq!(
121            f.params,
122            vec![
123                Param {
124                    name: vihaco_parser::Ident("input".to_owned()),
125                    ty: StubType::Unit,
126                },
127                Param {
128                    name: vihaco_parser::Ident("output".to_owned()),
129                    ty: StubType::Unit,
130                },
131            ]
132        );
133    }
134
135    #[test]
136    fn parses_function_parameters_with_whitespace() {
137        let src = "fn @main( input : unit , output:unit ) {}";
138        let f = ParsedFunction::<StubInst, StubType>::parser()
139            .parse(src)
140            .into_result()
141            .unwrap();
142        assert_eq!(f.params.len(), 2);
143        assert_eq!(f.params[0].name.as_str(), "input");
144        assert_eq!(f.params[1].name.as_str(), "output");
145    }
146
147    #[test]
148    fn rejects_parameter_without_type() {
149        let src = "fn @main(input) {}";
150        assert!(
151            ParsedFunction::<StubInst, StubType>::parser()
152                .parse(src)
153                .has_errors()
154        );
155    }
156
157    #[test]
158    fn parses_function_with_canonical_body() {
159        let src = "fn @main() {\n  stub.halt\n  stub.print\n  stub.halt\n}";
160        let f = ParsedFunction::<StubInst, StubType>::parser()
161            .parse(src)
162            .into_result()
163            .unwrap();
164        assert_eq!(
165            f.body,
166            vec![StubInst::Halt, StubInst::Print, StubInst::Halt]
167        );
168    }
169
170    #[test]
171    fn rejects_unknown_instruction() {
172        let src = "fn @main() { foo bar 1 2.0 }";
173        assert!(
174            ParsedFunction::<StubInst, StubType>::parser()
175                .parse(src)
176                .has_errors()
177        );
178    }
179
180    #[test]
181    fn parses_consumer_provided_return_type() {
182        let src = "fn @main() -> unit { stub.halt }";
183        let f = ParsedFunction::<StubInst, StubType>::parser()
184            .parse(src)
185            .into_result()
186            .unwrap();
187        assert_eq!(f.return_types, vec![StubType::Unit]);
188    }
189
190    #[test]
191    fn lexical_helpers_return_explicit_newtypes() {
192        assert_eq!(
193            parse::symbol_ref().parse("@main").into_result(),
194            Ok(vihaco_parser::Ident("main".to_owned()))
195        );
196        assert_eq!(
197            parse::string_literal()
198                .parse("\"hello\\nworld\"")
199                .into_result(),
200            Ok(vihaco_parser::QuotedString("hello\nworld".to_owned()))
201        );
202    }
203
204    #[test]
205    fn skips_line_comments() {
206        let src = "\
207fn @main() {
208    // this is ignored
209    stub.halt
210}
211";
212        let f = ParsedFunction::<StubInst, StubType>::parser()
213            .parse(src)
214            .into_result()
215            .unwrap();
216        assert_eq!(f.body.len(), 1);
217    }
218
219    #[test]
220    fn rejects_malformed_known_instruction() {
221        #[derive(Debug, Clone, PartialEq, vihaco_parser_derive::Parse)]
222        #[syntax_class(instruction, head = "stub")]
223        enum OnlyOne {
224            Dump(u32),
225        }
226
227        let src = "fn @main() { stub.dump foo }";
228        assert!(
229            ParsedFunction::<OnlyOne, StubType>::parser()
230                .parse(src)
231                .has_errors()
232        );
233    }
234
235    #[test]
236    fn block_i64_flat_parses_whitespace_separated_ints() {
237        let got = block_i64_flat().parse(" 0 1 2 3 ").into_result().unwrap();
238        assert_eq!(got, vec![0, 1, 2, 3]);
239    }
240
241    #[test]
242    fn block_i64_pairs_parses_rows() {
243        let got = block_i64_pairs()
244            .parse("\n  1 1\n  5 1\n  1 5\n")
245            .into_result()
246            .unwrap();
247        assert_eq!(got, vec![(1, 1), (5, 1), (1, 5)]);
248    }
249
250    #[test]
251    fn block_i64_flat_accepts_empty() {
252        let got = block_i64_flat().parse("").into_result().unwrap();
253        assert!(got.is_empty());
254    }
255}