Skip to content

Commit 1efd747

Browse files
committed
feat: add Grammar2 runtime for direct TypedAST parsing
- Add Grammar2 module wrapping GrammarInterpreter for direct parsing - Add parse_with_signatures() for builtin extern declaration injection - Add ZyntaxRuntime::plugin_signatures() and compile_typed_program() - Update imagepipe runner to use Grammar2 backend - All 40 imagepipe grammar tests pass
1 parent 39ee40d commit 1efd747

5 files changed

Lines changed: 468 additions & 67 deletions

File tree

Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
//! Grammar V2 - Direct TypedAST generation using GrammarInterpreter
2+
//!
3+
//! This module provides a simpler, more direct parsing interface that uses
4+
//! ZynPEG 2.0's GrammarInterpreter to parse source code directly into TypedAST
5+
//! without going through JSON serialization or pest VM.
6+
//!
7+
//! # Example
8+
//!
9+
//! ```ignore
10+
//! use zyntax_embed::Grammar2;
11+
//!
12+
//! // Compile from .zyn grammar source
13+
//! let grammar = Grammar2::from_source(include_str!("my_lang.zyn"))?;
14+
//!
15+
//! // Parse source code directly to TypedProgram
16+
//! let program = grammar.parse("fn main() { 42 }")?;
17+
//! ```
18+
19+
use std::sync::Arc;
20+
use zyn_peg::grammar::{parse_grammar, GrammarIR, GrammarMetadata, BuiltinMappings, TypeDeclarations};
21+
use zyn_peg::runtime2::{GrammarInterpreter, ParserState, ParseResult, ParsedValue};
22+
use zyntax_typed_ast::{TypedASTBuilder, TypedProgram, TypedDeclaration, TypedFunction, TypedParameter};
23+
use zyntax_typed_ast::type_registry::{TypeRegistry, Type, PrimitiveType};
24+
use zyntax_typed_ast::{typed_node, Span, Visibility, CallingConvention, Mutability, InternedString};
25+
26+
/// Errors that can occur during grammar operations
27+
#[derive(Debug, thiserror::Error)]
28+
pub enum Grammar2Error {
29+
#[error("Failed to parse grammar: {0}")]
30+
ParseError(String),
31+
32+
#[error("Failed to parse source: {0}")]
33+
SourceParseError(String),
34+
35+
#[error("Unexpected parse result: expected TypedProgram")]
36+
UnexpectedResult,
37+
}
38+
39+
/// Result type for grammar operations
40+
pub type Grammar2Result<T> = Result<T, Grammar2Error>;
41+
42+
/// A V2 grammar for parsing source code directly to TypedAST
43+
///
44+
/// Uses ZynPEG 2.0's GrammarInterpreter for direct TypedAST construction
45+
/// without JSON serialization or pest VM overhead.
46+
pub struct Grammar2 {
47+
/// The parsed grammar IR
48+
grammar: Arc<GrammarIR>,
49+
}
50+
51+
impl Grammar2 {
52+
/// Create a grammar from .zyn source code
53+
pub fn from_source(zyn_source: &str) -> Grammar2Result<Self> {
54+
let grammar = parse_grammar(zyn_source)
55+
.map_err(|e| Grammar2Error::ParseError(e.to_string()))?;
56+
57+
Ok(Self {
58+
grammar: Arc::new(grammar),
59+
})
60+
}
61+
62+
/// Create a grammar from an existing GrammarIR
63+
pub fn from_ir(grammar: GrammarIR) -> Self {
64+
Self {
65+
grammar: Arc::new(grammar),
66+
}
67+
}
68+
69+
/// Get the language name
70+
pub fn name(&self) -> &str {
71+
&self.grammar.metadata.name
72+
}
73+
74+
/// Get the language version
75+
pub fn version(&self) -> &str {
76+
&self.grammar.metadata.version
77+
}
78+
79+
/// Get the file extensions this grammar handles
80+
pub fn file_extensions(&self) -> &[String] {
81+
&self.grammar.metadata.file_extensions
82+
}
83+
84+
/// Get the entry point function name if declared
85+
pub fn entry_point(&self) -> Option<&str> {
86+
self.grammar.metadata.entry_point.as_deref()
87+
}
88+
89+
/// Get a reference to the GrammarIR
90+
pub fn grammar_ir(&self) -> &GrammarIR {
91+
&self.grammar
92+
}
93+
94+
/// Parse source code directly to TypedProgram
95+
///
96+
/// Uses GrammarInterpreter to parse the source and construct TypedAST nodes.
97+
/// This bypasses JSON serialization and is the recommended way to parse.
98+
pub fn parse(&self, source: &str) -> Grammar2Result<TypedProgram> {
99+
self.parse_with_filename(source, "input.imgpipe")
100+
}
101+
102+
/// Parse source code with a specific filename (for diagnostics)
103+
pub fn parse_with_filename(&self, source: &str, filename: &str) -> Grammar2Result<TypedProgram> {
104+
use zyntax_typed_ast::source::SourceFile;
105+
106+
let interpreter = GrammarInterpreter::new(&self.grammar);
107+
108+
let mut builder = TypedASTBuilder::new();
109+
let mut registry = TypeRegistry::new();
110+
let mut state = ParserState::new(source, &mut builder, &mut registry);
111+
112+
// Parse from the entry rule
113+
let result = interpreter.parse(&mut state);
114+
115+
match result {
116+
ParseResult::Success(ParsedValue::Program(mut program), _) => {
117+
// Add source file for diagnostics
118+
program.source_files = vec![SourceFile::new(filename.to_string(), source.to_string())];
119+
Ok(*program)
120+
}
121+
ParseResult::Success(other, _) => {
122+
// If we get something other than a program, wrap it
123+
eprintln!("[Grammar2] Warning: parse returned {:?}, expected Program",
124+
std::mem::discriminant(&other));
125+
Err(Grammar2Error::UnexpectedResult)
126+
}
127+
ParseResult::Failure(e) => {
128+
Err(Grammar2Error::SourceParseError(format!(
129+
"Parse error at {}:{}: expected {:?}",
130+
e.line, e.column, e.expected
131+
)))
132+
}
133+
}
134+
}
135+
136+
/// Parse source code with plugin signatures (for proper extern declarations)
137+
pub fn parse_with_signatures(
138+
&self,
139+
source: &str,
140+
filename: &str,
141+
signatures: &std::collections::HashMap<String, zyntax_compiler::zrtl::ZrtlSymbolSig>,
142+
) -> Grammar2Result<TypedProgram> {
143+
let mut program = self.parse_with_filename(source, filename)?;
144+
145+
// Inject extern function declarations for builtins
146+
self.inject_builtin_externs(&mut program, Some(signatures))?;
147+
148+
Ok(program)
149+
}
150+
151+
/// Inject extern function declarations for all builtins from @builtin directive
152+
fn inject_builtin_externs(
153+
&self,
154+
program: &mut TypedProgram,
155+
signatures: Option<&std::collections::HashMap<String, zyntax_compiler::zrtl::ZrtlSymbolSig>>,
156+
) -> Grammar2Result<()> {
157+
use zyntax_typed_ast::typed_ast::ParameterKind;
158+
159+
let span = Span::new(0, 0); // Synthetic span for injected declarations
160+
161+
// Iterate over all builtins from @builtin directive
162+
for (source_name, target_symbol) in &self.grammar.builtins.functions {
163+
// Get return type from @types.function_returns if available
164+
let return_type = if let Some(type_str) = self.grammar.type_decls.function_returns.get(source_name) {
165+
Type::Extern {
166+
name: InternedString::new_global(type_str),
167+
layout: None,
168+
}
169+
} else if let Some(sigs) = signatures {
170+
sigs.get(target_symbol.as_str())
171+
.map(|sig| Self::type_tag_to_type(&sig.return_type))
172+
.unwrap_or(Type::Any)
173+
} else {
174+
Type::Any
175+
};
176+
177+
// Get parameters from signature if available
178+
let params = if let Some(sigs) = signatures {
179+
if let Some(sig) = sigs.get(target_symbol.as_str()) {
180+
(0..sig.param_count)
181+
.map(|i| {
182+
let ty = Self::type_tag_to_type(&sig.params[i as usize]);
183+
TypedParameter {
184+
name: InternedString::new_global(&format!("p{}", i)),
185+
ty,
186+
mutability: Mutability::Immutable,
187+
kind: ParameterKind::Regular,
188+
default_value: None,
189+
attributes: vec![],
190+
span,
191+
}
192+
})
193+
.collect()
194+
} else {
195+
vec![]
196+
}
197+
} else {
198+
vec![]
199+
};
200+
201+
// Create extern function declaration
202+
let extern_func = TypedFunction {
203+
name: InternedString::new_global(target_symbol),
204+
type_params: vec![],
205+
params,
206+
return_type,
207+
body: None,
208+
visibility: Visibility::Public,
209+
is_async: false,
210+
is_external: true,
211+
calling_convention: CallingConvention::Default,
212+
link_name: Some(InternedString::new_global(target_symbol)),
213+
};
214+
215+
// Add to program declarations
216+
program.declarations.push(typed_node(
217+
TypedDeclaration::Function(extern_func),
218+
Type::Primitive(PrimitiveType::Unit),
219+
span,
220+
));
221+
}
222+
223+
Ok(())
224+
}
225+
226+
/// Convert ZRTL TypeTag to Type
227+
fn type_tag_to_type(tag: &zyntax_compiler::zrtl::TypeTag) -> Type {
228+
use zyntax_compiler::zrtl::{TypeCategory, PrimitiveSize};
229+
230+
match tag.category() {
231+
TypeCategory::Void => Type::Primitive(PrimitiveType::Unit),
232+
TypeCategory::Bool => Type::Primitive(PrimitiveType::Bool),
233+
TypeCategory::Int => {
234+
let size = tag.type_id();
235+
match size {
236+
x if x == PrimitiveSize::Bits8 as u16 => Type::Primitive(PrimitiveType::I8),
237+
x if x == PrimitiveSize::Bits16 as u16 => Type::Primitive(PrimitiveType::I16),
238+
x if x == PrimitiveSize::Bits32 as u16 => Type::Primitive(PrimitiveType::I32),
239+
x if x == PrimitiveSize::Bits64 as u16 => Type::Primitive(PrimitiveType::I64),
240+
_ => Type::Primitive(PrimitiveType::I32),
241+
}
242+
}
243+
TypeCategory::UInt => {
244+
let size = tag.type_id();
245+
match size {
246+
x if x == PrimitiveSize::Bits8 as u16 => Type::Primitive(PrimitiveType::U8),
247+
x if x == PrimitiveSize::Bits16 as u16 => Type::Primitive(PrimitiveType::U16),
248+
x if x == PrimitiveSize::Bits32 as u16 => Type::Primitive(PrimitiveType::U32),
249+
x if x == PrimitiveSize::Bits64 as u16 => Type::Primitive(PrimitiveType::U64),
250+
_ => Type::Primitive(PrimitiveType::U32),
251+
}
252+
}
253+
TypeCategory::Float => {
254+
let size = tag.type_id();
255+
match size {
256+
x if x == PrimitiveSize::Bits32 as u16 => Type::Primitive(PrimitiveType::F32),
257+
x if x == PrimitiveSize::Bits64 as u16 => Type::Primitive(PrimitiveType::F64),
258+
_ => Type::Primitive(PrimitiveType::F32),
259+
}
260+
}
261+
TypeCategory::String => Type::Primitive(PrimitiveType::String),
262+
TypeCategory::Opaque => Type::Any,
263+
_ => Type::Any,
264+
}
265+
}
266+
}
267+
268+
impl Clone for Grammar2 {
269+
fn clone(&self) -> Self {
270+
Self {
271+
grammar: Arc::clone(&self.grammar),
272+
}
273+
}
274+
}
275+
276+
#[cfg(test)]
277+
mod tests {
278+
use super::*;
279+
280+
#[test]
281+
fn test_grammar2_creation() {
282+
let grammar = Grammar2::from_source(r#"
283+
@language {
284+
name: "Test",
285+
version: "1.0",
286+
}
287+
288+
program = { SOI ~ EOI }
289+
-> TypedProgram {
290+
declarations: [],
291+
}
292+
"#);
293+
294+
match grammar {
295+
Ok(g) => {
296+
assert_eq!(g.name(), "Test");
297+
assert_eq!(g.version(), "1.0");
298+
}
299+
Err(e) => {
300+
eprintln!("Grammar compilation failed: {}", e);
301+
}
302+
}
303+
}
304+
}

crates/zyntax_embed/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ mod array;
6060
mod value;
6161
mod runtime;
6262
mod grammar;
63+
mod grammar2;
6364
pub mod iterator;
6465

6566
pub use convert::{FromZyntax, IntoZyntax, TryFromZyntax, TryIntoZyntax};
@@ -84,6 +85,7 @@ pub use runtime::{
8485
PromiseAllSettled, SettledResult,
8586
};
8687
pub use grammar::{LanguageGrammar, GrammarError, GrammarResult};
88+
pub use grammar2::{Grammar2, Grammar2Error, Grammar2Result};
8789
pub use iterator::{
8890
ZrtlIterable, ZrtlIterator,
8991
ZyntaxArrayIterator, ZyntaxStringCharsIterator, ZyntaxStringBytesIterator,

crates/zyntax_embed/src/runtime.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1995,6 +1995,40 @@ impl ZyntaxRuntime {
19951995
pub fn has_function(&self, name: &str) -> bool {
19961996
self.function_ids.contains_key(name)
19971997
}
1998+
1999+
/// Get a reference to the plugin signatures
2000+
///
2001+
/// This is useful for Grammar2 parsers that need to inject extern declarations
2002+
/// for builtin functions with proper type signatures.
2003+
pub fn plugin_signatures(&self) -> &HashMap<String, zyntax_compiler::zrtl::ZrtlSymbolSig> {
2004+
&self.plugin_signatures
2005+
}
2006+
2007+
/// Compile a TypedProgram directly (without parsing)
2008+
///
2009+
/// This is useful when using Grammar2 to parse source code directly to TypedAST,
2010+
/// bypassing the traditional grammar.parse() path.
2011+
///
2012+
/// # Returns
2013+
///
2014+
/// The names of functions defined in the module.
2015+
pub fn compile_typed_program(&mut self, program: zyntax_typed_ast::TypedProgram) -> RuntimeResult<Vec<String>> {
2016+
// Lower to HIR
2017+
let hir_module = self.lower_typed_program(program)?;
2018+
2019+
// Collect function names before compilation
2020+
let function_names: Vec<String> = hir_module
2021+
.functions
2022+
.values()
2023+
.filter(|f| !f.is_external)
2024+
.filter_map(|f| f.name.resolve_global())
2025+
.collect();
2026+
2027+
// Compile the module
2028+
self.compile_module(&hir_module)?;
2029+
2030+
Ok(function_names)
2031+
}
19982032
}
19992033

20002034
// ============================================================================

examples/imagepipe/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ path = "src/main.rs"
1111
[dependencies]
1212
# Zyntax embedding SDK
1313
zyntax_embed = { path = "../../crates/zyntax_embed" }
14+
zyntax_typed_ast = { path = "../../crates/typed_ast" }
1415

1516
# CLI argument parsing
1617
clap = { version = "4.4", features = ["derive"] }

0 commit comments

Comments
 (0)