-
Notifications
You must be signed in to change notification settings - Fork 0
12 Embedding SDK
This chapter covers how to embed the Zyntax JIT runtime in your Rust applications using the zyntax_embed crate. This enables you to compile and execute code from any language with a ZynPEG grammar directly from Rust.
The Zyntax Embedding SDK provides:
-
Language Grammar Interface: Parse source code using any
.zyngrammar - Multi-Language Runtime: Register multiple grammars and compile from different languages
- JIT Compilation: Compile TypedAST to native code at runtime
- Multi-Tier Optimization: Automatic optimization of hot code paths
- Bidirectional Interop: Seamless conversion between Rust and Zyntax types
- Async/Await Support: Promise-based async operations
- Hot Reloading: Update functions without restarting
- ZRTL Plugin Loading: Load native runtime libraries dynamically
Add zyntax_embed to your Cargo.toml:
[dependencies]
zyntax_embed = { path = "path/to/zyntax/crates/zyntax_embed" }use zyntax_embed::{ZyntaxRuntime, LanguageGrammar, ZyntaxValue};
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Compile grammar from .zyn file
let grammar = LanguageGrammar::compile_zyn_file("grammars/zig.zyn")?;
// Create runtime
let mut runtime = ZyntaxRuntime::new()?;
// Compile source code
runtime.compile_with_grammar(&grammar, r#"
pub fn add(a: i32, b: i32) i32 {
return a + b;
}
pub fn main() i32 {
return add(10, 32);
}
"#)?;
// Call compiled function
let result: i32 = runtime.call("main", &[])?;
println!("Result: {}", result); // Output: 42
Ok(())
}use zyntax_embed::LanguageGrammar;
// Compile grammar from .zyn source
let grammar = LanguageGrammar::compile_zyn(include_str!("my_lang.zyn"))?;
// Or from a file
let grammar = LanguageGrammar::compile_zyn_file("grammars/my_lang.zyn")?;
// Save compiled grammar for faster loading later
grammar.save("my_lang.zpeg")?;LanguageGrammar wraps a compiled ZynPEG module and provides parsing capabilities.
| Method | Description |
|---|---|
load(path) |
Load pre-compiled .zpeg file |
compile_zyn(source) |
Compile from .zyn grammar source string |
compile_zyn_file(path) |
Compile from .zyn grammar file |
from_json(json) |
Load from serialized JSON string |
from_module(module) |
Create from existing ZpegModule
|
let grammar = LanguageGrammar::load("zig.zpeg")?;
println!("Language: {}", grammar.name()); // "Zig"
println!("Version: {}", grammar.version()); // "0.11"
println!("Extensions: {:?}", grammar.file_extensions()); // [".zig"]
if let Some(entry) = grammar.entry_point() {
println!("Entry point: {}", entry); // "main"
}
// Get builtin function mappings
for (name, symbol) in grammar.builtins() {
println!("Builtin '{}' maps to '{}'", name, symbol);
}// Parse to TypedProgram (for further processing)
let program: TypedProgram = grammar.parse(source_code)?;
// Parse to JSON (for debugging or serialization)
let json: String = grammar.parse_to_json(source_code)?;
println!("{}", json);For advanced use cases, implement AstHostFunctions for custom AST construction:
use zyntax_embed::{AstHostFunctions, NodeHandle, TypedAstBuilder};
// Use the default builder
let json = grammar.parse_with_builder(source, TypedAstBuilder::new())?;
// Or implement your own AstHostFunctions for custom behavior
struct MyCustomBuilder { /* ... */ }
impl AstHostFunctions for MyCustomBuilder {
// Implement required methods...
}The runtime supports registering multiple language grammars and loading modules by language name. This enables polyglot applications where different parts of your codebase can use different languages.
use zyntax_embed::{ZyntaxRuntime, LanguageGrammar};
let mut runtime = ZyntaxRuntime::new()?;
// Register grammars by language name
runtime.register_grammar("zig", LanguageGrammar::compile_zyn_file("grammars/zig.zyn")?);
runtime.register_grammar("python", LanguageGrammar::compile_zyn_file("grammars/python.zyn")?);
runtime.register_grammar("calc", LanguageGrammar::compile_zyn_file("grammars/calc.zyn")?);
// Or use convenience methods
runtime.register_grammar_file("haxe", "grammars/haxe.zyn")?;
runtime.register_grammar_zpeg("lua", "grammars/lua.zpeg")?;
// Query registered languages
println!("Languages: {:?}", runtime.languages()); // ["zig", "python", "calc", "haxe", "lua"]
println!("Has Python: {}", runtime.has_language("python")); // true// Load modules specifying the language
let functions = runtime.load_module("zig", r#"
pub fn add(a: i32, b: i32) i32 {
return a + b;
}
pub fn factorial(n: i32) i32 {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
"#)?;
println!("Loaded functions: {:?}", functions); // ["add", "factorial"]
// Load from another language
runtime.load_module("calc", "def multiply(a, b) = a * b")?;
// Call any function regardless of source language
let sum: i32 = runtime.call("add", &[10.into(), 32.into()])?;
let fact: i32 = runtime.call("factorial", &[5.into()])?;When grammars declare file extensions in their @language metadata, the runtime automatically maps extensions to languages:
// The zig.zyn grammar declares: file_extensions: [".zig"]
runtime.register_grammar("zig", LanguageGrammar::compile_zyn_file("zig.zyn")?);
// Load files - language auto-detected from extension
runtime.load_module_file("./src/math.zig")?; // Uses "zig" grammar
runtime.load_module_file("./lib/utils.py")?; // Uses "python" grammar
// Query extension mappings
println!("Language for .zig: {:?}", runtime.language_for_extension(".zig")); // Some("zig")
println!("Language for .py: {:?}", runtime.language_for_extension("py")); // Some("python")All modules loaded into the same runtime share a common execution environment. Functions can call each other across language boundaries using explicit exports:
// Load core utilities in Zig and EXPORT them for cross-module linking
runtime.load_module_with_exports("zig", r#"
pub fn square(x: i32) i32 { return x * x; }
pub fn cube(x: i32) i32 { return x * x * x; }
"#, &["square", "cube"])?;
// Load a DSL that uses the Zig functions via extern declarations
runtime.load_module("calc", r#"
extern fn square(x: i32) i32;
extern fn cube(x: i32) i32;
def sum_of_powers(a, b) = square(a) + cube(b)
"#)?;
// The calc function calls into the Zig implementation
let result: i32 = runtime.call("sum_of_powers", &[3.into(), 2.into()])?;
assert_eq!(result, 17); // square(3) + cube(2) = 9 + 8 = 17Functions must be explicitly exported to be available for cross-module linking:
// Method 1: Export during load
runtime.load_module_with_exports("zig", source, &["fn1", "fn2"])?;
// Method 2: Export after load
runtime.load_module("zig", source)?;
runtime.export_function("my_func")?;
// Check for symbol conflicts
if let Some(existing_ptr) = runtime.check_export_conflict("my_func") {
println!("Warning: my_func already exported at {:?}", existing_ptr);
}
// List all exported symbols
for (name, ptr) in runtime.exported_symbols() {
println!("Exported: {} at {:?}", name, ptr);
}Note: Attempting to export a function with the same name as an existing export will log a warning and overwrite the existing symbol.
The TieredRuntime also supports multi-language modules with the same API:
use zyntax_embed::TieredRuntime;
let mut runtime = TieredRuntime::production()?;
runtime.register_grammar("zig", LanguageGrammar::compile_zyn_file("zig.zyn")?);
runtime.load_module("zig", "pub fn compute(x: i32) i32 { return x * 2; }")?;
// Hot functions are automatically optimized regardless of source language
let result: i32 = runtime.call("compute", &[21.into()])?;Best for simple use cases with predictable performance:
use zyntax_embed::ZyntaxRuntime;
// Basic runtime
let mut runtime = ZyntaxRuntime::new()?;
// With custom configuration
let config = CompilationConfig::default();
let mut runtime = ZyntaxRuntime::with_config(config)?;
// With external FFI symbols
let symbols = &[
("my_c_func", my_c_func as *const u8),
("log_value", log_value as *const u8),
];
let mut runtime = ZyntaxRuntime::with_symbols(symbols)?;Adaptive compilation with automatic optimization for production workloads:
use zyntax_embed::{TieredRuntime, TieredConfig, OptimizationTier};
// Development mode: Fast startup, no background optimization
let mut runtime = TieredRuntime::development()?;
// Production mode: Full tiered optimization
let mut runtime = TieredRuntime::production()?;
// Custom configuration
let config = TieredConfig {
warm_threshold: 100, // Calls before Tier 1
hot_threshold: 1000, // Calls before Tier 2
enable_background_optimization: true,
..Default::default()
};
let mut runtime = TieredRuntime::with_config(config)?;| Tier | Name | Backend | When Used |
|---|---|---|---|
| 0 | Baseline | Cranelift (fast) | Cold code, startup |
| 1 | Standard | Cranelift (optimized) | Warm code |
| 2 | Optimized | Cranelift/LLVM | Hot code |
Functions automatically promote through tiers based on execution frequency:
// Check current optimization tier
let tier = runtime.get_function_tier("hot_function")?;
// Force optimization for latency-sensitive code
runtime.optimize_function("critical_path", OptimizationTier::Optimized)?;
// Get statistics
let stats = runtime.statistics();
println!("{}", stats.format());
// Output:
// TieredStatistics:
// Baseline (Tier 0): 45 functions
// Standard (Tier 1): 12 functions
// Optimized (Tier 2): 3 functionsuse zyntax_embed::{ZyntaxRuntime, LanguageGrammar};
let grammar = LanguageGrammar::compile_zyn(ZIG_GRAMMAR)?;
let mut runtime = ZyntaxRuntime::new()?;
// Single method compiles: parse → lower → compile
runtime.compile_with_grammar(&grammar, source_code)?;
// Now call functions
let result: i32 = runtime.call("main", &[])?;For more control, use the manual pipeline:
use zyntax_embed::{
ZyntaxRuntime, LanguageGrammar, TypedProgram,
compile_to_hir, HirModule,
};
// Step 1: Parse source to TypedAST
let grammar = LanguageGrammar::load("lang.zpeg")?;
let typed_program: TypedProgram = grammar.parse(source)?;
// Step 2: Lower to HIR (custom lowering possible here)
let hir_module: HirModule = compile_to_hir(&typed_program, type_registry, config)?;
// Step 3: Compile HIR to native code
let mut runtime = ZyntaxRuntime::new()?;
runtime.compile_module(&hir_module)?;
// Step 4: Execute
let result: i32 = runtime.call("main", &[])?;// Automatic type conversion
let sum: i32 = runtime.call("add", &[5.into(), 3.into()])?;
let greeting: String = runtime.call("greet", &["World".into()])?;
let point: (f64, f64) = runtime.call("make_point", &[1.0.into(), 2.0.into()])?;let result: ZyntaxValue = runtime.call_raw("compute", &[data.into()])?;
match result {
ZyntaxValue::Int(n) => println!("Integer: {}", n),
ZyntaxValue::Float(f) => println!("Float: {}", f),
ZyntaxValue::String(s) => println!("String: {}", s),
ZyntaxValue::Bool(b) => println!("Bool: {}", b),
ZyntaxValue::Array(items) => {
println!("Array with {} items", items.len());
}
ZyntaxValue::Struct { name, fields } => {
println!("Struct {}: {:?}", name, fields);
}
ZyntaxValue::Null => println!("Null"),
_ => println!("Other: {:?}", result),
}For JIT-compiled functions, use call_function with an explicit signature for optimal performance. This bypasses the DynamicValue ABI and calls functions with native types directly.
use zyntax_embed::{ZyntaxRuntime, NativeType, NativeSignature};
// Define the function signature: (i32, i32) -> i32
let sig = NativeSignature::new(&[NativeType::I32, NativeType::I32], NativeType::I32);
// Call with native types
let result = runtime.call_function("add", &[10.into(), 32.into()], &sig)?;
assert_eq!(result.as_i32().unwrap(), 42);
// Or parse signature from a string
let sig = NativeSignature::parse("(i32, i32) -> i32").unwrap();
let result = runtime.call_function("multiply", &[6.into(), 7.into()], &sig)?;| NativeType | Rust Equivalent | Description |
|---|---|---|
I32 |
i32 |
32-bit signed integer |
I64 |
i64 |
64-bit signed integer |
F32 |
f32 |
32-bit float |
F64 |
f64 |
64-bit float |
Bool |
bool |
Boolean (passed as i8) |
Void |
() |
No return value |
Ptr |
*mut u8 |
Pointer type |
The NativeSignature::parse method accepts strings in the format:
-
"() -> void"- No parameters, no return -
"(i32) -> i32"- Single parameter -
"(i32, i32) -> i64"- Multiple parameters -
"(f64, f64) -> f64"- Floating point types
Use call_function when:
- You know the exact function signature at compile time
- You need maximum performance for hot paths
- You're calling JIT-compiled functions with primitive types
Use call or call_raw when:
- You need automatic type conversion
- The function signature is dynamic
- You're working with complex types (structs, arrays)
use zyntax_embed::{ZyntaxPromise, PromiseState};
// Call async function
let promise: ZyntaxPromise = runtime.call_async("fetch_data", &[url.into()])?;
// Option 1: Block and wait
let data: String = promise.await_result()?;
// Option 2: Poll manually
loop {
match promise.poll() {
PromiseState::Pending => {
// Do other work...
std::thread::yield_now();
}
PromiseState::Ready(value) => {
println!("Got: {:?}", value);
break;
}
PromiseState::Failed(err) => {
eprintln!("Error: {}", err);
break;
}
}
}
// Option 3: Chain with .then() and .catch()
let processed = promise
.then(|data| ZyntaxValue::String(format!("Processed: {:?}", data)))
.catch(|err| ZyntaxValue::String(format!("Error: {}", err)));
// Option 4: Use with async/await (implements Future)
async fn process_data(runtime: &ZyntaxRuntime) -> Result<String, RuntimeError> {
let promise = runtime.call_async("fetch", &["data".into()])?;
let result: String = promise.await?;
Ok(result)
}The universal runtime value type:
use zyntax_embed::ZyntaxValue;
// From Rust types
let int_val: ZyntaxValue = 42i32.into();
let float_val: ZyntaxValue = 3.14f64.into();
let str_val: ZyntaxValue = "hello".into();
let bool_val: ZyntaxValue = true.into();
let vec_val: ZyntaxValue = vec![1, 2, 3].into();
// Create structs
let point = ZyntaxValue::new_struct("Point")
.field("x", 10.0f64)
.field("y", 20.0f64)
.build();
// Access struct fields
if let ZyntaxValue::Struct { fields, .. } = &point {
if let Some(ZyntaxValue::Float(x)) = fields.get("x") {
println!("x = {}", x);
}
}use zyntax_embed::{FromZyntax, IntoZyntax, ZyntaxValue};
// Rust → ZyntaxValue
let value: ZyntaxValue = 42i32.into_zyntax();
// ZyntaxValue → Rust
let num: i32 = FromZyntax::from_zyntax(value)?;
let opt: Option<i32> = FromZyntax::from_zyntax(nullable_value)?;use zyntax_embed::{ZyntaxString, ZyntaxArray};
// ZyntaxString: Length-prefixed UTF-8
let s = ZyntaxString::from_str("Hello!");
let ptr = s.as_ptr(); // Memory: [i32 length][utf8_bytes...]
println!("{}", s.as_str().unwrap());
// ZyntaxArray: Capacity + length header
let mut arr: ZyntaxArray<i32> = [1, 2, 3].into();
arr.push(4);
let ptr = arr.as_ptr(); // Memory: [i32 capacity][i32 length][elements...]Register resolvers for handling import statements:
// Callback-based resolver
runtime.add_import_resolver(Box::new(|module_path| {
match module_path {
"std.math" => Ok(Some(include_str!("stdlib/math.zig").to_string())),
"std.io" => Ok(Some(include_str!("stdlib/io.zig").to_string())),
_ => Ok(None), // Not found, try next resolver
}
}));
// File-system resolver
runtime.add_filesystem_resolver("./src", "zig"); // Looks for .zig files
runtime.add_filesystem_resolver("./lib", "hx"); // Looks for .hx files
// Check resolver count
println!("Resolvers: {}", runtime.import_resolver_count());Register Rust functions directly with the runtime without creating a ZRTL plugin. This is the simplest way to expose native functionality to Zyntax code.
Use with_symbols to register FFI functions when creating the runtime:
use zyntax_embed::ZyntaxRuntime;
// Define extern "C" functions
extern "C" fn native_add(a: i32, b: i32) -> i32 {
a + b
}
extern "C" fn native_print(value: i32) {
println!("[Native] Value: {}", value);
}
extern "C" fn native_sqrt(x: f64) -> f64 {
x.sqrt()
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Register symbols at construction
let symbols: &[(&str, *const u8)] = &[
("native_add", native_add as *const u8),
("native_print", native_print as *const u8),
("native_sqrt", native_sqrt as *const u8),
];
let mut runtime = ZyntaxRuntime::with_symbols(symbols)?;
// Now Zyntax code can call these functions
runtime.compile_with_grammar(&grammar, r#"
// Declare external functions
extern fn native_add(a: i32, b: i32) i32;
extern fn native_print(value: i32) void;
pub fn main() i32 {
const result = native_add(10, 32);
native_print(result);
return result;
}
"#)?;
let result: i32 = runtime.call("main", &[])?;
Ok(())
}Add functions after runtime creation using register_function:
use zyntax_embed::{ZyntaxRuntime, ExternalFunction};
let mut runtime = ZyntaxRuntime::new()?;
// Register individual functions
runtime.register_function(ExternalFunction {
name: "log_message".to_string(),
ptr: log_message as *const u8,
signature: "(ptr, i32) -> void".to_string(), // Optional signature hint
})?;
// Register multiple functions
runtime.register_functions(&[
ExternalFunction::new("math_sin", math_sin as *const u8),
ExternalFunction::new("math_cos", math_cos as *const u8),
ExternalFunction::new("math_tan", math_tan as *const u8),
])?;External functions must use the C ABI (extern "C"). Supported parameter and return types:
| Rust Type | Zyntax Type | Notes |
|---|---|---|
i32 |
i32 |
Signed 32-bit integer |
i64 |
i64 |
Signed 64-bit integer |
f32 |
f32 |
32-bit float |
f64 |
f64 |
64-bit float |
bool |
bool |
Boolean (as i8) |
*const u8 |
[]const u8 |
Pointer to string/slice data |
*mut T |
*T |
Mutable pointer |
() |
void |
No return value |
Strings are passed as length-prefixed pointers:
use std::slice;
/// Receive a Zyntax string (length-prefixed)
extern "C" fn print_string(ptr: *const u8) {
unsafe {
// First 4 bytes are the length (i32)
let len = *(ptr as *const i32) as usize;
let data = ptr.add(4);
let bytes = slice::from_raw_parts(data, len);
if let Ok(s) = std::str::from_utf8(bytes) {
println!("{}", s);
}
}
}
/// Return a Zyntax string (must allocate with Zyntax allocator)
extern "C" fn create_greeting(name_ptr: *const u8) -> *const u8 {
unsafe {
// Read input string
let len = *(name_ptr as *const i32) as usize;
let data = name_ptr.add(4);
let name = std::str::from_utf8(slice::from_raw_parts(data, len))
.unwrap_or("World");
// Create output (using Zyntax allocator in real code)
let greeting = format!("Hello, {}!", name);
// ... allocate and return
}
}Arrays use capacity + length header:
/// Sum an array of integers
extern "C" fn sum_array(ptr: *const u8) -> i64 {
unsafe {
let header = ptr as *const i32;
let _capacity = *header;
let length = *header.add(1) as usize;
let data = (ptr as *const i32).add(2);
let mut sum: i64 = 0;
for i in 0..length {
sum += *data.add(i) as i64;
}
sum
}
}Pass Zyntax functions to Rust:
type ZyntaxCallback = extern "C" fn(i32) -> i32;
extern "C" fn apply_twice(f: ZyntaxCallback, x: i32) -> i32 {
f(f(x))
}
// Register it
let symbols = &[("apply_twice", apply_twice as *const u8)];
let mut runtime = ZyntaxRuntime::with_symbols(symbols)?;
// Use from Zyntax
runtime.compile_with_grammar(&grammar, r#"
extern fn apply_twice(f: fn(i32) i32, x: i32) i32;
fn double(n: i32) i32 {
return n * 2;
}
pub fn main() i32 {
return apply_twice(double, 5); // Returns 20
}
"#)?;use zyntax_embed::{ZyntaxRuntime, LanguageGrammar};
// Math functions
extern "C" fn math_abs(x: f64) -> f64 { x.abs() }
extern "C" fn math_floor(x: f64) -> f64 { x.floor() }
extern "C" fn math_ceil(x: f64) -> f64 { x.ceil() }
extern "C" fn math_sqrt(x: f64) -> f64 { x.sqrt() }
extern "C" fn math_pow(base: f64, exp: f64) -> f64 { base.powf(exp) }
extern "C" fn math_sin(x: f64) -> f64 { x.sin() }
extern "C" fn math_cos(x: f64) -> f64 { x.cos() }
extern "C" fn math_log(x: f64) -> f64 { x.ln() }
// I/O functions
extern "C" fn io_print_i32(x: i32) { println!("{}", x); }
extern "C" fn io_print_f64(x: f64) { println!("{}", x); }
fn main() -> Result<(), Box<dyn std::error::Error>> {
let symbols: &[(&str, *const u8)] = &[
// Math
("math_abs", math_abs as *const u8),
("math_floor", math_floor as *const u8),
("math_ceil", math_ceil as *const u8),
("math_sqrt", math_sqrt as *const u8),
("math_pow", math_pow as *const u8),
("math_sin", math_sin as *const u8),
("math_cos", math_cos as *const u8),
("math_log", math_log as *const u8),
// I/O
("io_print_i32", io_print_i32 as *const u8),
("io_print_f64", io_print_f64 as *const u8),
];
let grammar = LanguageGrammar::compile_zyn_file("grammars/zig.zyn")?;
let mut runtime = ZyntaxRuntime::with_symbols(symbols)?;
runtime.compile_with_grammar(&grammar, r#"
// External declarations
extern fn math_sqrt(x: f64) f64;
extern fn math_pow(base: f64, exp: f64) f64;
extern fn io_print_f64(x: f64) void;
pub fn hypotenuse(a: f64, b: f64) f64 {
return math_sqrt(math_pow(a, 2.0) + math_pow(b, 2.0));
}
pub fn main() void {
const h = hypotenuse(3.0, 4.0);
io_print_f64(h); // Prints: 5
}
"#)?;
runtime.call::<()>("main", &[])?;
Ok(())
}| Feature | External Functions | ZRTL Plugins |
|---|---|---|
| Setup complexity | Simple, inline code | Separate crate + build |
| Distribution | Compiled into host app | Separate .zrtl files |
| Hot loading | No | Yes |
| Symbol namespacing | Manual | Automatic ($Type$method) |
| Best for | App-specific functions | Reusable libraries |
Use External Functions when you need to expose host application functionality. Use ZRTL Plugins when creating reusable runtime libraries for distribution.
Load native runtime libraries (written in Rust or C):
// Load single plugin
runtime.load_plugin("./my_runtime.zrtl")?;
// Load all plugins from directory
let count = runtime.load_plugins_from_directory("./plugins")?;
println!("Loaded {} plugins", count);Creating a ZRTL plugin in Rust:
// In your plugin crate (lib.rs)
use zrtl::{zrtl_plugin, zrtl_export};
zrtl_plugin!("my_runtime");
#[zrtl_export("$MyRuntime$add")]
pub extern "C" fn add(a: i32, b: i32) -> i32 {
a + b
}
#[zrtl_export("$MyRuntime$print")]
pub extern "C" fn print_value(value: i32) {
println!("Value: {}", value);
}Update functions at runtime without restarting:
// Original function compiled
runtime.compile_with_grammar(&grammar, "pub fn compute(x: i32) i32 { return x * 2; }")?;
let v1: i32 = runtime.call("compute", &[10.into()])?;
println!("v1: {}", v1); // 20
// Hot reload with new implementation
let new_function = /* compile new version */;
runtime.hot_reload("compute", &new_function)?;
let v2: i32 = runtime.call("compute", &[10.into()])?;
println!("v2: {}", v2); // New resultuse zyntax_embed::{RuntimeError, GrammarError, ConversionError};
// Grammar errors
match LanguageGrammar::compile_zyn(bad_grammar) {
Ok(g) => { /* use grammar */ }
Err(GrammarError::ParseError(msg)) => eprintln!("Parse error: {}", msg),
Err(GrammarError::CompileError(msg)) => eprintln!("Compile error: {}", msg),
Err(e) => eprintln!("Error: {}", e),
}
// Runtime errors
match runtime.call::<i32>("compute", &args) {
Ok(result) => println!("Result: {}", result),
Err(RuntimeError::FunctionNotFound(name)) => {
eprintln!("Function '{}' not found", name);
}
Err(RuntimeError::Compilation(e)) => {
eprintln!("Compilation failed: {}", e);
}
Err(RuntimeError::Conversion(ConversionError::TypeMismatch { expected, actual })) => {
eprintln!("Type error: expected {:?}, got {:?}", expected, actual);
}
Err(e) => eprintln!("Error: {}", e),
}A full example showing how to build a scripting engine:
use zyntax_embed::{
ZyntaxRuntime, LanguageGrammar, ZyntaxValue,
RuntimeError, GrammarError,
};
use std::collections::HashMap;
struct ScriptEngine {
runtime: ZyntaxRuntime,
grammar: LanguageGrammar,
scripts: HashMap<String, String>,
}
impl ScriptEngine {
pub fn new(grammar_path: &str) -> Result<Self, Box<dyn std::error::Error>> {
let grammar = LanguageGrammar::load(grammar_path)?;
let mut runtime = ZyntaxRuntime::new()?;
// Register built-in functions
runtime.add_import_resolver(Box::new(|path| {
match path {
"builtins" => Ok(Some(BUILTINS_SOURCE.to_string())),
_ => Ok(None),
}
}));
Ok(Self {
runtime,
grammar,
scripts: HashMap::new(),
})
}
pub fn load_script(&mut self, name: &str, source: &str) -> Result<(), RuntimeError> {
self.runtime.compile_with_grammar(&self.grammar, source)?;
self.scripts.insert(name.to_string(), source.to_string());
Ok(())
}
pub fn call_function<T: zyntax_embed::FromZyntax>(
&self,
func: &str,
args: &[ZyntaxValue],
) -> Result<T, RuntimeError> {
self.runtime.call(func, args)
}
pub fn eval(&mut self, expr: &str) -> Result<ZyntaxValue, RuntimeError> {
// Wrap expression in a function and call it
let wrapper = format!("pub fn __eval__() {{ return {}; }}", expr);
self.runtime.compile_with_grammar(&self.grammar, &wrapper)?;
self.runtime.call_raw("__eval__", &[])
}
}
const BUILTINS_SOURCE: &str = r#"
pub fn print(msg: []const u8) void {
// Mapped to native print
}
"#;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut engine = ScriptEngine::new("zig.zpeg")?;
// Load a script
engine.load_script("math", r#"
pub fn factorial(n: i32) i32 {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
"#)?;
// Call functions
let result: i32 = engine.call_function("factorial", &[5.into()])?;
println!("5! = {}", result); // 120
// Evaluate expressions
let value = engine.eval("10 + 20 * 2")?;
println!("Result: {:?}", value); // Int(50)
Ok(())
}-
Pre-compile grammars: Use
.zpegfiles instead of compiling.zynat startup - Use TieredRuntime for production: Automatically optimizes hot paths
-
Pre-warm critical paths: Call
optimize_function()for latency-sensitive code - Reuse runtime instances: Creating new runtimes has overhead
-
Use zero-copy types:
ZyntaxStringandZyntaxArrayavoid copying - Batch imports: Load all scripts before execution begins
-
ZyntaxValuefollows Rust ownership semantics -
ZyntaxStringandZyntaxArraytrack ownership for proper cleanup - Promise state is thread-safe via internal mutex
- Function pointers are atomically swapped during tier promotion
- ZRTL plugins must follow the C ABI for safety
- See Async Runtime for detailed async/Promise documentation
- See Grammar Syntax for writing
.zyngrammars - See TypedAST for understanding the intermediate representation
- See ZRTL Plugins for creating native runtime libraries