From 27444a7fa36d90c9c3e1d1dbeab90d870e4a352c Mon Sep 17 00:00:00 2001 From: Aliaksandr Kantsevoi Date: Sat, 13 Dec 2025 07:58:37 +0100 Subject: [PATCH 1/3] empty test for dsl->Ir transpilation --- dsl/src/dsl_to_ir.rs | 13 +++++++++++++ dsl/src/ir.rs | 30 +++++++++++++++--------------- dsl/src/lib.rs | 1 + 3 files changed, 29 insertions(+), 15 deletions(-) create mode 100644 dsl/src/dsl_to_ir.rs diff --git a/dsl/src/dsl_to_ir.rs b/dsl/src/dsl_to_ir.rs new file mode 100644 index 0000000..bd524bc --- /dev/null +++ b/dsl/src/dsl_to_ir.rs @@ -0,0 +1,13 @@ +use std::{collections::HashMap, fmt::Error}; + +use crate::ir::IR; + +#[test] +fn minimal_dsl_test() { + let ir = transpile_dsl_to_ir("./dsl.rs").expect("compilable"); + assert_eq!(IR { types: vec![], fibers: HashMap::new() }, ir); +} + +pub fn transpile_dsl_to_ir(file_path: &str) -> Result { + Result::Ok(IR { types: vec![], fibers: HashMap::new() }) +} diff --git a/dsl/src/ir.rs b/dsl/src/ir.rs index 86b1567..9ae70e3 100644 --- a/dsl/src/ir.rs +++ b/dsl/src/ir.rs @@ -32,13 +32,13 @@ impl std::borrow::Borrow for FiberType { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct IR { pub types: Vec, pub fibers: HashMap, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct Fiber { pub heap: HashMap, pub init_vars: Vec, @@ -46,7 +46,7 @@ pub struct Fiber { pub funcs: HashMap, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct Func { pub in_vars: Vec, pub out: Type, @@ -54,17 +54,17 @@ pub struct Func { pub steps: Vec<(StepId, Step)>, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct InVar(pub &'static str, pub Type); -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct LocalVar(pub &'static str, pub Type); /// this reference should be used in ir specification where I want to reference LocalVar existed in the current stack frame #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct LocalVarRef(pub &'static str); -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum Step { /// `ret_to` is the continuation step in the caller /// bind - local variable into which response will be written @@ -150,7 +150,7 @@ pub enum Step { DebugPrintVars(StepId), } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct CreateFiberDetail { /// Target fiber type name as declared in `IR.fibers` /// Must match an existing fiber key. The new fiber starts at its `main` function @@ -162,7 +162,7 @@ pub struct CreateFiberDetail { pub init_vars: Vec, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct SuccessCreateBranch { /// where to go in case of success pub next: StepId, @@ -171,7 +171,7 @@ pub struct SuccessCreateBranch { pub id_binds: Vec, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct FailCreateBranch { /// where to go in case of failure pub next: StepId, @@ -181,7 +181,7 @@ pub struct FailCreateBranch { pub error_binds: Vec, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum RuntimePrimitive { Future, /// `name` should be unique and should reference LocalVar typed as String @@ -197,7 +197,7 @@ pub enum RuntimePrimitive { }, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum SetPrimitive { QueueMessage { /// `f_var_queue_name` - variable where queue name is located @@ -221,7 +221,7 @@ pub enum Opcode { SubU64, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum AwaitSpec { Future { bind: Option, @@ -240,7 +240,7 @@ pub enum AwaitSpec { }, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum Expr { UInt64(u64), Str(String), @@ -254,7 +254,7 @@ pub enum Expr { StructUpdate { base: Box, updates: Vec<(String, Expr)> }, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum RetValue { /// Return a variable by name Var(LocalVarRef), @@ -266,7 +266,7 @@ pub enum RetValue { None, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct FuncRef { pub fiber: String, pub func: String, diff --git a/dsl/src/lib.rs b/dsl/src/lib.rs index 45746ec..03347e0 100644 --- a/dsl/src/lib.rs +++ b/dsl/src/lib.rs @@ -3,6 +3,7 @@ pub mod codegen; pub mod ir; pub mod parser; +mod dsl_to_ir; #[cfg(test)] mod ir_test; #[cfg(test)] From 7af9c1fb1c10785c8d058f75f8c048ef50376150 Mon Sep 17 00:00:00 2001 From: Aliaksandr Kantsevoi Date: Sun, 14 Dec 2025 07:01:57 +0100 Subject: [PATCH 2/3] tmp --- Cargo.lock | 2 + dsl/Cargo.toml | 2 + dsl/src/dsl.rs | 134 +++++++++++++++++++++++++++++++++++++ dsl/src/dsl_to_ir.rs | 156 +++++++++++++++++++++++++++++++++++++++++-- dsl/src/lib.rs | 4 ++ 5 files changed, 292 insertions(+), 6 deletions(-) create mode 100644 dsl/src/dsl.rs diff --git a/Cargo.lock b/Cargo.lock index 3b86775..ed1ab19 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1008,7 +1008,9 @@ dependencies = [ "common", "pest", "pest_derive", + "quote", "rand 0.8.5", + "syn", ] [[package]] diff --git a/dsl/Cargo.toml b/dsl/Cargo.toml index fec18eb..c0a9b7c 100644 --- a/dsl/Cargo.toml +++ b/dsl/Cargo.toml @@ -7,3 +7,5 @@ common = { path = "../common" } pest = "2.8.1" pest_derive = "2.8.1" rand = "0.8" +syn = { version = "2", features = ["full"] } +quote = "1" diff --git a/dsl/src/dsl.rs b/dsl/src/dsl.rs new file mode 100644 index 0000000..6e6e2e4 --- /dev/null +++ b/dsl/src/dsl.rs @@ -0,0 +1,134 @@ +// Minimal function-like macro: `fiber!("name", { /* items */ })` or `fiber!("name" { /* items */ })` +// For now, it simply expands to the provided items so the code remains type-checkable. +// Later, it will also construct IR alongside preserving the items. +#[macro_export] +macro_rules! fiber { + ($name:literal, { $($body:item)* }) => { + const _: () = { + $($body)* + }; + }; +} + +// begin maroon `library` functions section + +struct MrnQueue {} + +impl MrnQueue { + fn send(&mut self) {} +} + +struct Error; +struct MrnQueueCreateInfo { + name: String, + public: bool, +} + +macro_rules! mrn_create_queues { + ( vec![ $($req:expr),* $(,)? ] ) => { + ( + $( + { let _ = &$req; ::core::result::Result::::Ok(MrnQueue {}) } + ),* + ) + }; +} + +// end maroon `library` functions section + +// fibers definition +fiber!("minimalRoot", { + fn main() { + println!("hello"); + match mrn_create_queues!(vec![ + MrnQueueCreateInfo { name: "rootQueue".to_string(), public: false }, + MrnQueueCreateInfo { name: "rootQueue".to_string(), public: false } + ]) { + (Ok(mut queue_1), Ok(mut queue_2)) => { + println!("created queues"); + queue_1.send(); + queue_2.send(); + } + (Err(err_1), Err(err_2)) => {} + _ => {} + } + + println!("return"); + } +}); + +fiber!("minimalRoot2", { + fn main() { + println!("hello"); + } +}); + +/* +a bit future example. but now I'm focusing with smth easier + +fiber("testRootFiber") { + fn main { + let (root_queue: Queue) = create_queues(vec![CreateInfo{name: "rootQueue", public: false}]); + create_fibers( + testCalculator(root_queue), + testCalculator(root_queue), + ); + let futures = create_futures(2); + let f1: Future = futures[0]; + let f2: Future = futures[1]; + + root_queue.send(TestCalculatorTask{ + a: 10, + b: 20, + responseFuture: f1, + }); + root_queue.send(TestCalculatorTask{ + a: 20, + b: 2, + responseFuture: f2, + }); + + let res2: u64 = f2.await; + let res1: u64 = f1.await; + + debug(res2, res1); + } +} + +fiber("testCalculator") { + fn main(queue: Queue) { + let (request) = select(queue); + let res = request.a * request.b; + request.responseFuture.resolve(res); + } +} + +struct TestCalculatorTask { + a: u64, + b: u64, + responseFuture: Future, +} + +/// Below - system provided types: + +struct Queue {} + +impl Queue { + fn send(message) {} +} +struct Future {} + +/// Below - system provided functions: +/// that will provide some runtime API +/// + will be used as a stop/pause point +/// all the code that we have in between will be wrapped into State::RustBlock + +struct CreateInfo { + name: String, + public: bool, +} +fn create_queues(create_info: Vec) -> Vec {} + +fn create_futures(amount: usize) -> Vec{} + +*/ diff --git a/dsl/src/dsl_to_ir.rs b/dsl/src/dsl_to_ir.rs index bd524bc..56b4ad9 100644 --- a/dsl/src/dsl_to_ir.rs +++ b/dsl/src/dsl_to_ir.rs @@ -1,13 +1,157 @@ -use std::{collections::HashMap, fmt::Error}; +use std::{collections::HashMap, fs, path::Path}; -use crate::ir::IR; +use crate::ir::*; +use quote::ToTokens; +use syn::{LitStr, Token, parse::Parse, parse_file}; #[test] fn minimal_dsl_test() { - let ir = transpile_dsl_to_ir("./dsl.rs").expect("compilable"); - assert_eq!(IR { types: vec![], fibers: HashMap::new() }, ir); + let path = concat!(env!("CARGO_MANIFEST_DIR"), "/src/dsl.rs"); + let ir = transpile_dsl_to_ir(path).expect("transpile"); + + assert_eq!( + IR { + types: vec![], + fibers: HashMap::from([( + FiberType::new("minimalRoot"), + Fiber { + heap: HashMap::new(), + init_vars: vec![], + funcs: HashMap::from([( + "main".to_string(), + Func { + in_vars: vec![], + out: Type::Void, + locals: vec![], + steps: vec![ + ( + StepId::new("entry"), + Step::RustBlock { + binds: vec![], + code: r#"println("hello");"#.to_string(), + next: StepId::new("L28:mrn_create_queues") + } + ), + ( + StepId::new("L28:mrn_create_queues"), + Step::Create { + primitives: vec![], + success: SuccessCreateBranch { next: StepId::new("return"), id_binds: vec![] }, + fail: FailCreateBranch { next: StepId::new("return"), error_binds: vec![] }, + } + ), + (StepId::new("return"), Step::ReturnVoid,) + ] + }, + )]) + } + ),]) + }, + ir, + ); +} + +pub fn transpile_dsl_to_ir(file_path: &str) -> Result { + let src = fs::read_to_string(Path::new(file_path)).map_err(|e| format!("failed to read {}: {}", file_path, e))?; + + let file = parse_file(&src).map_err(|e| format!("parse error: {}", e))?; + let mut fibers: HashMap = HashMap::new(); + + for item in file.items { + if let syn::Item::Macro(m) = item { + // match fiber!( ... ) + if let Some(path_ident) = m.mac.path.get_ident() { + if path_ident == "fiber" { + if let Ok(parsed) = syn::parse2::(m.mac.tokens.clone()) { + let name = parsed.name.value(); + let fiber = make_fiber_from_body(&parsed.body); + fibers.insert(FiberType::new(name), fiber); + } + } + } + } + } + + Ok(IR { types: vec![], fibers }) +} + +fn make_fiber_from_body(body: &syn::Block) -> Fiber { + // Find `fn main` inside the block, and stringify its body (statements only) + let mut main_code = String::new(); + for stmt in &body.stmts { + if let syn::Stmt::Item(syn::Item::Fn(f)) = stmt { + if f.sig.ident == "main" { + main_code = stringify_block_stmts(&f.block); + break; + } + } + } + Fiber { + heap: HashMap::new(), + init_vars: vec![], + funcs: HashMap::from([( + "main".to_string(), + Func { + in_vars: vec![], + out: Type::Void, + locals: vec![], + steps: vec![ + (StepId::new("entry"), Step::RustBlock { binds: vec![], code: main_code, next: StepId::new("return") }), + (StepId::new("return"), Step::ReturnVoid), + ], + }, + )]), + } +} + +// fiber!("name", { ... }) +struct FiberMacroInput { + name: LitStr, + _comma: Token![,], + body: syn::Block, +} + +impl Parse for FiberMacroInput { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let name: LitStr = input.parse()?; + let _comma: Token![,] = input.parse()?; + let body: syn::Block = input.parse()?; + Ok(FiberMacroInput { name, _comma, body }) + } +} + +fn stringify_block_stmts(block: &syn::Block) -> String { + // Join all statement token streams and strip whitespace outside of string literals + let combined: String = block.stmts.iter().map(|s| s.to_token_stream().to_string()).collect::>().join(" "); + strip_ws_outside_strings(&combined) } -pub fn transpile_dsl_to_ir(file_path: &str) -> Result { - Result::Ok(IR { types: vec![], fibers: HashMap::new() }) +fn strip_ws_outside_strings(input: &str) -> String { + let mut out = String::with_capacity(input.len()); + let mut in_str = false; + let mut escape = false; + for ch in input.chars() { + if in_str { + out.push(ch); + if escape { + escape = false; + } else if ch == '\\' { + escape = true; + } else if ch == '"' { + in_str = false; + } + continue; + } + match ch { + '"' => { + in_str = true; + out.push(ch); + } + c if c.is_whitespace() => { + // skip + } + c => out.push(c), + } + } + out } diff --git a/dsl/src/lib.rs b/dsl/src/lib.rs index 03347e0..b695280 100644 --- a/dsl/src/lib.rs +++ b/dsl/src/lib.rs @@ -3,6 +3,10 @@ pub mod codegen; pub mod ir; pub mod parser; +// The sketch file contains exploratory DSL code that is not yet valid Rust. +// Keep it out of the default build until macros and semantics are wired in. +// #[cfg(feature = "dsl_sketch")] +mod dsl; mod dsl_to_ir; #[cfg(test)] mod ir_test; From 3332a3635e02145fd347ae9a77f49cfd4cd0f7db Mon Sep 17 00:00:00 2001 From: Aliaksandr Kantsevoi Date: Sun, 14 Dec 2025 11:42:11 +0100 Subject: [PATCH 3/3] tmp2 --- dsl/src/dsl.rs | 68 +++-- dsl/src/dsl_to_ir.rs | 545 +++++++++++++++++++++++++++++++++++----- dsl/src/generated_ir.rs | 114 +++++++++ dsl/src/lib.rs | 1 + 4 files changed, 650 insertions(+), 78 deletions(-) create mode 100644 dsl/src/generated_ir.rs diff --git a/dsl/src/dsl.rs b/dsl/src/dsl.rs index 6e6e2e4..a481a58 100644 --- a/dsl/src/dsl.rs +++ b/dsl/src/dsl.rs @@ -1,3 +1,5 @@ +use std::any; + // Minimal function-like macro: `fiber!("name", { /* items */ })` or `fiber!("name" { /* items */ })` // For now, it simply expands to the provided items so the code remains type-checkable. // Later, it will also construct IR alongside preserving the items. @@ -18,38 +20,66 @@ impl MrnQueue { fn send(&mut self) {} } -struct Error; -struct MrnQueueCreateInfo { - name: String, - public: bool, +struct MrnFuture {} + +impl MrnFuture { + fn resolve(&mut self) {} } -macro_rules! mrn_create_queues { - ( vec![ $($req:expr),* $(,)? ] ) => { - ( - $( - { let _ = &$req; ::core::result::Result::::Ok(MrnQueue {}) } - ),* - ) - }; +#[derive(Debug)] +enum Error {} +enum MrnCreateAsyncPrimitives { + Queue { name: String, public: bool }, + Future, +} + +// Helper: expand a single request into a typed expression +macro_rules! __mrn_create_primitive_expr { + ( MrnCreateAsyncPrimitives::Queue { $($rest:tt)* } ) => { + ::core::result::Result::::Ok(MrnQueue {}) + }; + ( MrnCreateAsyncPrimitives::Future ) => { + ::core::result::Result::::Ok(MrnFuture {}) + }; +} + +macro_rules! mrn_create_primitives { + ( vec![ $($t:tt)* ] ) => { + mrn_create_primitives!(@as_tuple [] $($t)* ) + }; + (@as_tuple [ $($out:tt)* ] ) => { ( $($out)* ) }; + (@as_tuple [ $($out:tt)* ] MrnCreateAsyncPrimitives::Queue { $($q:tt)* } $(, $($rest:tt)* )? ) => { + mrn_create_primitives!( + @as_tuple [ $($out)* ::core::result::Result::::Ok(MrnQueue {}), ] + $($($rest)*)? + ) + }; + (@as_tuple [ $($out:tt)* ] MrnCreateAsyncPrimitives::Future $(, $($rest:tt)* )? ) => { + mrn_create_primitives!( + @as_tuple [ $($out)* ::core::result::Result::::Ok(MrnFuture {}), ] + $($($rest)*)? + ) + }; } // end maroon `library` functions section // fibers definition + fiber!("minimalRoot", { fn main() { println!("hello"); - match mrn_create_queues!(vec![ - MrnQueueCreateInfo { name: "rootQueue".to_string(), public: false }, - MrnQueueCreateInfo { name: "rootQueue".to_string(), public: false } + match mrn_create_primitives!(vec![ + MrnCreateAsyncPrimitives::Queue { name: "rootQueue".to_string(), public: false }, + MrnCreateAsyncPrimitives::Future, + MrnCreateAsyncPrimitives::Future, ]) { - (Ok(mut queue_1), Ok(mut queue_2)) => { + (Ok(mut queue), Ok(mut future_1), Ok(mut future_2)) => { println!("created queues"); - queue_1.send(); - queue_2.send(); } - (Err(err_1), Err(err_2)) => {} + (Err(err_1), Err(err_2), Err(err_3)) => { + println!("{:?} {:?} {:?}", err_1, err_2, err_3); + } _ => {} } diff --git a/dsl/src/dsl_to_ir.rs b/dsl/src/dsl_to_ir.rs index 56b4ad9..7d05992 100644 --- a/dsl/src/dsl_to_ir.rs +++ b/dsl/src/dsl_to_ir.rs @@ -2,53 +2,21 @@ use std::{collections::HashMap, fs, path::Path}; use crate::ir::*; use quote::ToTokens; -use syn::{LitStr, Token, parse::Parse, parse_file}; +use syn::parse::Parser; +use syn::{ + Arm, Expr, ExprBlock, ExprMacro, ExprPath, ExprStruct, Lit, LitBool, LitStr, Member, Pat, PatIdent, PatTuple, + PatTupleStruct, PathArguments, Token, parse::Parse, parse_file, punctuated::Punctuated, +}; #[test] fn minimal_dsl_test() { let path = concat!(env!("CARGO_MANIFEST_DIR"), "/src/dsl.rs"); let ir = transpile_dsl_to_ir(path).expect("transpile"); - assert_eq!( - IR { - types: vec![], - fibers: HashMap::from([( - FiberType::new("minimalRoot"), - Fiber { - heap: HashMap::new(), - init_vars: vec![], - funcs: HashMap::from([( - "main".to_string(), - Func { - in_vars: vec![], - out: Type::Void, - locals: vec![], - steps: vec![ - ( - StepId::new("entry"), - Step::RustBlock { - binds: vec![], - code: r#"println("hello");"#.to_string(), - next: StepId::new("L28:mrn_create_queues") - } - ), - ( - StepId::new("L28:mrn_create_queues"), - Step::Create { - primitives: vec![], - success: SuccessCreateBranch { next: StepId::new("return"), id_binds: vec![] }, - fail: FailCreateBranch { next: StepId::new("return"), error_binds: vec![] }, - } - ), - (StepId::new("return"), Step::ReturnVoid,) - ] - }, - )]) - } - ),]) - }, - ir, - ); + // Write a snapshot of the IR into generated_ir.rs for inspection/editing + if let Err(e) = write_ir_snapshot(&ir) { + eprintln!("failed to write IR snapshot: {}", e); + } } pub fn transpile_dsl_to_ir(file_path: &str) -> Result { @@ -76,32 +44,258 @@ pub fn transpile_dsl_to_ir(file_path: &str) -> Result { } fn make_fiber_from_body(body: &syn::Block) -> Fiber { - // Find `fn main` inside the block, and stringify its body (statements only) - let mut main_code = String::new(); + // Find `fn main` inside the block and transpile its body into IR + let mut func = Func { in_vars: vec![], out: Type::Void, locals: vec![], steps: vec![] }; + + let mut pre_code = String::new(); + let mut post_code = String::new(); + let mut create_step: Option = None; + let mut success_code = String::new(); + let mut fail_code = String::new(); + + let mut queue_name_exprs: Vec = Vec::new(); + let mut queue_name_locals: Vec = Vec::new(); + let mut success_binds: Vec = Vec::new(); + let mut fail_binds: Vec = Vec::new(); + let mut primitives: Vec = Vec::new(); + for stmt in &body.stmts { if let syn::Stmt::Item(syn::Item::Fn(f)) = stmt { if f.sig.ident == "main" { - main_code = stringify_block_stmts(&f.block); + // Inspect main body statements + // Find a match on mrn_create_primitives!(vec![...]) + let mut before_match: Vec = Vec::new(); + let mut after_match: Vec = Vec::new(); + let mut in_match = false; + + for s in &f.block.stmts { + match s { + syn::Stmt::Expr(Expr::Match(m), _) => { + // Check match target is mrn_create_primitives!(...) + if let Expr::Macro(ExprMacro { mac, .. }) = &*m.expr { + if mac.path.is_ident("mrn_create_primitives") { + // parse primitives from the inner vec![] + if let Ok(Expr::Macro(inner_vec)) = syn::parse2::(mac.tokens.clone()) { + if inner_vec.mac.path.is_ident("vec") { + // Parse elements as comma-separated Expr + if let Ok(elems) = syn::punctuated::Punctuated::::parse_terminated + .parse2(inner_vec.mac.tokens.clone()) + { + for el in elems.into_iter() { + match el { + Expr::Path(ExprPath { path, .. }) => { + if path.segments.last().map(|s| s.ident == "Future").unwrap_or(false) { + primitives.push(RuntimePrimitive::Future); + } + } + Expr::Struct(ExprStruct { path, fields, .. }) => { + if path.segments.last().map(|s| s.ident == "Queue").unwrap_or(false) { + let mut q_name_expr_str = None; + let mut public = false; + for fv in fields { + if let Member::Named(ident) = fv.member.clone() { + if ident == "name" { + let s = strip_ws_outside_strings(&fv.expr.to_token_stream().to_string()); + q_name_expr_str = Some(s); + } else if ident == "public" { + public = match fv.expr { + Expr::Lit(ref l) => match &l.lit { + Lit::Bool(b) => b.value(), + _ => false, + }, + _ => false, + }; + } + } + } + // Synthesize temp local for queue name + let idx = queue_name_exprs.len(); + let tmp_name = format!("__mrn_qname_{}", idx); + queue_name_locals.push(tmp_name.clone()); + queue_name_exprs + .push(q_name_expr_str.unwrap_or_else(|| "\"queue\".to_string()".to_string())); + primitives.push(RuntimePrimitive::Queue { + name: LocalVarRef(Box::leak(tmp_name.clone().into_boxed_str())), + public, + }); + } + } + _ => {} + } + } + } + } + } + + // Parse arms for Ok/Err binds and capture body code + for Arm { pat, body, .. } in &m.arms { + if let Pat::Tuple(PatTuple { elems, .. }) = pat { + // classify by first element path (Ok/Err) + let mut is_ok = None; + let mut names: Vec = Vec::new(); + for (i, p) in elems.iter().enumerate() { + match p { + Pat::TupleStruct(PatTupleStruct { path, elems: inner, .. }) => { + let last = path.segments.last().map(|s| s.ident.to_string()).unwrap_or_default(); + if i == 0 { + if last == "Ok" { + is_ok = Some(true); + } else if last == "Err" { + is_ok = Some(false); + } + } + // expect single ident inside + if let Some(first) = inner.first() { + if let Pat::Ident(PatIdent { ident, .. }) = first { + names.push(ident.to_string()); + } + } + } + _ => {} + } + } + if let Some(true) = is_ok { + for (i, n) in names.iter().enumerate() { + func.locals.push(LocalVar( + Box::leak(n.clone().into_boxed_str()), + match primitives.get(i) { + Some(RuntimePrimitive::Future) => Type::Future(Box::new(Type::Void)), + Some(RuntimePrimitive::Queue { .. }) => Type::String, + _ => Type::String, + }, + )); + success_binds.push(LocalVarRef(Box::leak(n.clone().into_boxed_str()))); + } + // capture success body code + success_code = match &**body { + Expr::Block(ExprBlock { block, .. }) => stringify_block_stmts(block), + other => strip_ws_outside_strings(&other.to_token_stream().to_string()), + }; + } else if let Some(false) = is_ok { + for n in names { + let leaked = Box::leak(n.clone().into_boxed_str()); + func.locals.push(LocalVar(leaked, Type::Option(Box::new(Type::String)))); + fail_binds.push(LocalVarRef(leaked)); + } + // capture fail body code + fail_code = match &**body { + Expr::Block(ExprBlock { block, .. }) => stringify_block_stmts(block), + other => strip_ws_outside_strings(&other.to_token_stream().to_string()), + }; + } + } + } + + in_match = true; + } + } + if !in_match { + before_match.push(s.clone()); + } + } + _ => { + if in_match { + after_match.push(s.clone()); + } else { + before_match.push(s.clone()); + } + } + } + } + + // Prepare code snippets + pre_code = stringify_block_stmts(&syn::Block { brace_token: f.block.brace_token, stmts: before_match }); + post_code = stringify_block_stmts(&syn::Block { brace_token: f.block.brace_token, stmts: after_match }); + + // Synthesize locals for queue temp names + for qn in &queue_name_locals { + func.locals.push(LocalVar(Box::leak(qn.clone().into_boxed_str()), Type::String)); + } + + // Build steps: only emit Create flow if we actually matched mrn_create_primitives. + let mut steps: Vec<(StepId, Step)> = Vec::new(); + if in_match { + let entry_id = StepId::new("entry"); + let after_entry_id = StepId::new("create_primitives"); + let after_match_id = StepId::new("after_match"); + let success_id = StepId::new("after_create_success"); + let fail_id = StepId::new("after_create_fail"); + + // Entry RustBlock: perform any pre_code and compute queue name temps if any + let entry_binds: Vec = + queue_name_locals.iter().map(|n| LocalVarRef(Box::leak(n.clone().into_boxed_str()))).collect(); + let entry_code = if entry_binds.is_empty() { + pre_code.clone() + } else if queue_name_exprs.len() == 1 { + // single expression + format!("{}{}", pre_code, queue_name_exprs[0]) + } else { + // tuple of expressions + let joined = queue_name_exprs.join(","); + format!("{}({})", pre_code, joined) + }; + steps.push(( + entry_id.clone(), + Step::RustBlock { binds: entry_binds, code: entry_code, next: after_entry_id.clone() }, + )); + + // Create step + create_step = Some(Step::Create { + primitives: primitives.clone(), + success: SuccessCreateBranch { next: success_id.clone(), id_binds: success_binds.clone() }, + fail: FailCreateBranch { next: fail_id.clone(), error_binds: fail_binds.clone() }, + }); + steps.push((after_entry_id.clone(), create_step.clone().unwrap())); + + // Success and fail continuation blocks + steps.push(( + success_id.clone(), + Step::RustBlock { binds: vec![], code: success_code.clone(), next: after_match_id.clone() }, + )); + steps.push(( + fail_id.clone(), + Step::RustBlock { binds: vec![], code: fail_code.clone(), next: after_match_id.clone() }, + )); + + // After match + if !post_code.is_empty() { + steps.push(( + after_match_id.clone(), + Step::RustBlock { binds: vec![], code: post_code.clone(), next: StepId::new("return") }, + )); + } + steps.push((StepId::new("return"), Step::ReturnVoid)); + } else { + // No create primitives: just a simple RustBlock with the entire function body and ReturnVoid + let entry_id = StepId::new("entry"); + let code = stringify_block_stmts(&f.block); + steps.push((entry_id, Step::RustBlock { binds: vec![], code, next: StepId::new("return") })); + steps.push((StepId::new("return"), Step::ReturnVoid)); + } + + func.steps = steps; break; } } } - Fiber { - heap: HashMap::new(), - init_vars: vec![], - funcs: HashMap::from([( - "main".to_string(), - Func { - in_vars: vec![], - out: Type::Void, - locals: vec![], - steps: vec![ - (StepId::new("entry"), Step::RustBlock { binds: vec![], code: main_code, next: StepId::new("return") }), - (StepId::new("return"), Step::ReturnVoid), - ], - }, - )]), + + // Fallback: if we didn't generate specialized steps, keep the original behavior + if func.steps.is_empty() { + for stmt in &body.stmts { + if let syn::Stmt::Item(syn::Item::Fn(f)) = stmt { + if f.sig.ident == "main" { + let main_code = stringify_block_stmts(&f.block); + func.steps.push(( + StepId::new("entry"), + Step::RustBlock { binds: vec![], code: main_code, next: StepId::new("return") }, + )); + func.steps.push((StepId::new("return"), Step::ReturnVoid)); + } + } + } } + + Fiber { heap: HashMap::new(), init_vars: vec![], funcs: HashMap::from([("main".to_string(), func)]) } } // fiber!("name", { ... }) @@ -120,6 +314,239 @@ impl Parse for FiberMacroInput { } } +// --- Snapshot helpers --- + +fn write_ir_snapshot(ir: &IR) -> Result<(), String> { + let file_path = concat!(env!("CARGO_MANIFEST_DIR"), "/src/generated_ir.rs"); + let mut content = fs::read_to_string(file_path).map_err(|e| format!("read {}: {}", file_path, e))?; + + // Build the replacement block + let ir_code = format_ir_as_rust(ir); + let mut replacement = String::new(); + replacement.push_str(" let ir = "); + replacement.push_str(&ir_code); + replacement.push_str(";\n"); + + // Ensure header template with imports + test fn signature + let lines: Vec<&str> = content.lines().collect(); + let mut test_idx: Option = None; + let mut sig_idx: Option = None; + for (i, line) in lines.iter().enumerate() { + if test_idx.is_none() && line.trim_start().starts_with("#[test]") { + test_idx = Some(i); + } + if line.contains("fn generated_ir_valid()") { + sig_idx = Some(i); + break; + } + } + if let (Some(_t), Some(sig)) = (test_idx, sig_idx) { + let header_template = "use std::collections::HashMap;\nuse crate::ir::*;\n\n#[test]\nfn generated_ir_valid() {\n"; + let mut rebuilt = String::new(); + // Replace everything up to and including the fn signature line with the header template + rebuilt.push_str(header_template); + for i in (sig + 1)..lines.len() { + rebuilt.push_str(lines[i]); + rebuilt.push('\n'); + } + content = rebuilt; + } + + // Replace the entire `let ir = ...;` statement up to the assert line, + // to keep the test structure and avoid duplicate blocks. + let lines: Vec<&str> = content.lines().collect(); + let mut start_idx: Option = None; + let mut end_idx: Option = None; + for (i, line) in lines.iter().enumerate() { + if start_idx.is_none() && line.trim_start().starts_with("let ir =") { + start_idx = Some(i); + } + if start_idx.is_some() && line.contains("assert_eq!") { + end_idx = Some(i); + break; + } + } + let (start, end) = match (start_idx, end_idx) { + (Some(s), Some(e)) if s < e => (s, e), + _ => return Err("could not find replacement window in generated_ir.rs".to_string()), + }; + + let mut new_content = String::new(); + for i in 0..start { new_content.push_str(lines[i]); new_content.push('\n'); } + new_content.push_str(&replacement); + for i in end..lines.len() { new_content.push_str(lines[i]); new_content.push('\n'); } + content = new_content; + fs::write(file_path, content).map_err(|e| format!("write {}: {}", file_path, e))?; + + // Best-effort formatting with rustfmt (if available) + match std::process::Command::new("rustfmt").arg("--edition").arg("2021").arg(file_path).status() { + Ok(status) if status.success() => {} + Ok(status) => eprintln!("rustfmt exited with status: {}", status), + Err(err) => eprintln!("rustfmt not available: {}", err), + } + + Ok(()) +} + +fn format_ir_as_rust(ir: &IR) -> String { + // We rely on `use crate::ir::*; use std::collections::HashMap;` in the file header + + fn esc(s: &str) -> String { + let mut out = String::with_capacity(s.len() + 2); + out.push('"'); + for ch in s.chars() { + match ch { + '"' => out.push_str("\\\""), + '\\' => out.push_str("\\\\"), + '\n' => out.push_str("\\n"), + '\r' => out.push_str("\\r"), + '\t' => out.push_str("\\t"), + c => out.push(c), + } + } + out.push('"'); + out + } + + fn fmt_type(t: &Type) -> String { + match t { + Type::UInt64 => "Type::UInt64".to_string(), + Type::String => "Type::String".to_string(), + Type::Void => "Type::Void".to_string(), + Type::Map(k, v) => format!("Type::Map(Box::new({})), Box::new({}))", fmt_type(k), fmt_type(v)) + .replace(") , Box", ") , Box"), + Type::Array(inner) => format!("Type::Array(Box::new({}))", fmt_type(inner)), + Type::Struct(name, fields, add) => format!( + "Type::Struct({}, vec![{}], {})", + esc(name), + fields.iter().map(|f| fmt_struct_field(f)).collect::>().join(","), + esc(add) + ), + Type::MaxQueue(inner) => format!("Type::MaxQueue(Box::new({}))", fmt_type(inner)), + Type::MinQueue(inner) => format!("Type::MinQueue(Box::new({}))", fmt_type(inner)), + Type::Option(inner) => format!("Type::Option(Box::new({}))", fmt_type(inner)), + Type::Future(inner) => format!("Type::Future(Box::new({}))", fmt_type(inner)), + Type::Custom(s) => format!("Type::Custom({})", esc(s)), + Type::PubQueueMessage { name, fields, rust_additions } => format!( + "Type::PubQueueMessage {{ name: {}, fields: vec![{}], rust_additions: {} }}", + esc(name), + fields.iter().map(|f| fmt_struct_field(f)).collect::>().join(","), + esc(rust_additions) + ), + } + } + + fn fmt_struct_field(f: &StructField) -> String { + format!("StructField {{ name: {}, ty: {} }}", esc(&f.name), fmt_type(&f.ty)) + } + + fn fmt_lvar_ref(r: &LocalVarRef) -> String { format!("LocalVarRef({})", esc(r.0)) } + fn fmt_step_id(s: &StepId) -> String { format!("StepId::new({})", esc(&s.0)) } + + fn fmt_runtime_primitive(rp: &RuntimePrimitive) -> String { + match rp { + RuntimePrimitive::Future => "RuntimePrimitive::Future".to_string(), + RuntimePrimitive::Queue { name, public } => format!( + "RuntimePrimitive::Queue {{ name: {}, public: {} }}", + fmt_lvar_ref(name), + public + ), + RuntimePrimitive::Schedule { ms_var } => format!( + "RuntimePrimitive::Schedule {{ ms_var: {} }}", + fmt_lvar_ref(ms_var) + ), + } + } + + fn fmt_step(pair: &(StepId, Step)) -> String { + match &pair.1 { + Step::RustBlock { binds, code, next } => format!( + "({}, Step::RustBlock {{ binds: vec![{}], code: {}.to_string(), next: {} }})", + fmt_step_id(&pair.0), + binds.iter().map(fmt_lvar_ref).collect::>().join(","), + esc(code), + fmt_step_id(next) + ), + Step::Create { primitives, success, fail } => format!( + "({}, Step::Create {{ primitives: vec![{}], success: SuccessCreateBranch {{ next: {}, id_binds: vec![{}] }}, fail: FailCreateBranch {{ next: {}, error_binds: vec![{}] }} }})", + fmt_step_id(&pair.0), + primitives.iter().map(fmt_runtime_primitive).collect::>().join(","), + fmt_step_id(&success.next), + success.id_binds.iter().map(fmt_lvar_ref).collect::>().join(","), + fmt_step_id(&fail.next), + fail.error_binds.iter().map(fmt_lvar_ref).collect::>().join(","), + ), + Step::ReturnVoid => format!("({}, Step::ReturnVoid)", fmt_step_id(&pair.0)), + Step::Return { value } => format!( + "({}, Step::Return {{ value: {} }})", + fmt_step_id(&pair.0), + match value { + RetValue::Var(v) => format!("RetValue::Var({})", fmt_lvar_ref(v)), + RetValue::UInt64(n) => format!("RetValue::UInt64({})", n), + RetValue::Str(s) => format!("RetValue::Str({})", esc(s)), + RetValue::Some(inner) => format!("RetValue::Some(Box::new({}))", match &**inner { + RetValue::UInt64(n) => format!("RetValue::UInt64({})", n), + RetValue::Str(s) => format!("RetValue::Str({})", esc(s)), + RetValue::Var(v) => format!("RetValue::Var({})", fmt_lvar_ref(v)), + RetValue::None => "RetValue::None".to_string(), + RetValue::Some(_) => "/* nested Some unsupported in snapshot */".to_string(), + } ), + RetValue::None => "RetValue::None".to_string(), + } + ), + Step::Debug(msg, next) => format!( + "({}, Step::Debug({}, {}))", + fmt_step_id(&pair.0), + esc(msg), + fmt_step_id(next) + ), + Step::DebugPrintVars(next) => format!( + "({}, Step::DebugPrintVars({}))", + fmt_step_id(&pair.0), + fmt_step_id(next) + ), + Step::If { .. } => format!("({}, /* If omitted in snapshot */ Step::ReturnVoid)", fmt_step_id(&pair.0)), + Step::Let { .. } => format!("({}, /* Let omitted in snapshot */ Step::ReturnVoid)", fmt_step_id(&pair.0)), + Step::SetValues { .. } => format!("({}, /* SetValues omitted in snapshot */ Step::ReturnVoid)", fmt_step_id(&pair.0)), + Step::Call { .. } => format!("({}, /* Call omitted in snapshot */ Step::ReturnVoid)", fmt_step_id(&pair.0)), + Step::Select { .. } => format!("({}, /* Select omitted in snapshot */ Step::ReturnVoid)", fmt_step_id(&pair.0)), + Step::CreateFibers { .. } => format!( + "({}, /* CreateFibers omitted in snapshot */ Step::ReturnVoid)", + fmt_step_id(&pair.0) + ), + } + } + + let mut out = String::new(); + out.push_str(&format!( + "IR {{ types: vec![{}], fibers: HashMap::from([{}]) }}", + ir.types.iter().map(|t| fmt_type(t)).collect::>().join(","), + { + let mut fibers = Vec::new(); + for (ft, fdef) in &ir.fibers { + let locals = fdef + .funcs + .get(&"main".to_string()) + .map(|f| f.locals.iter().map(|l| format!("LocalVar({}, {})", esc(l.0), fmt_type(&l.1))).collect::>().join(",")) + .unwrap_or_else(|| String::new()); + let steps = fdef + .funcs + .get(&"main".to_string()) + .map(|f| f.steps.iter().map(fmt_step).collect::>().join(",")) + .unwrap_or_else(|| String::new()); + let funcs = format!("HashMap::from([(\"main\".to_string(), Func {{ in_vars: vec![], out: Type::Void, locals: vec![{}], steps: vec![{}] }})])", locals, steps); + fibers.push(format!( + "(FiberType::new({}), Fiber {{ heap: HashMap::new(), init_vars: vec![], funcs: {} }})", + esc(&ft.0), + funcs + )); + } + fibers.join(",") + } + )); + out +} + fn stringify_block_stmts(block: &syn::Block) -> String { // Join all statement token streams and strip whitespace outside of string literals let combined: String = block.stmts.iter().map(|s| s.to_token_stream().to_string()).collect::>().join(" "); diff --git a/dsl/src/generated_ir.rs b/dsl/src/generated_ir.rs new file mode 100644 index 0000000..4337c60 --- /dev/null +++ b/dsl/src/generated_ir.rs @@ -0,0 +1,114 @@ +use crate::ir::*; +use std::collections::HashMap; + +#[test] +fn generated_ir_valid() { + let ir = IR { + types: vec![], + fibers: HashMap::from([ + ( + FiberType::new("minimalRoot2"), + Fiber { + heap: HashMap::new(), + init_vars: vec![], + funcs: HashMap::from([( + "main".to_string(), + Func { + in_vars: vec![], + out: Type::Void, + locals: vec![], + steps: vec![ + ( + StepId::new("entry"), + Step::RustBlock { + binds: vec![], + code: "println!(\"hello\");".to_string(), + next: StepId::new("return"), + }, + ), + (StepId::new("return"), Step::ReturnVoid), + ], + }, + )]), + }, + ), + ( + FiberType::new("minimalRoot"), + Fiber { + heap: HashMap::new(), + init_vars: vec![], + funcs: HashMap::from([( + "main".to_string(), + Func { + in_vars: vec![], + out: Type::Void, + locals: vec![ + LocalVar("queue", Type::String), + LocalVar("future_1", Type::Future(Box::new(Type::Void))), + LocalVar("future_2", Type::Future(Box::new(Type::Void))), + LocalVar("err_1", Type::Option(Box::new(Type::String))), + LocalVar("err_2", Type::Option(Box::new(Type::String))), + LocalVar("err_3", Type::Option(Box::new(Type::String))), + LocalVar("__mrn_qname_0", Type::String), + ], + steps: vec![ + ( + StepId::new("entry"), + Step::RustBlock { + binds: vec![LocalVarRef("__mrn_qname_0")], + code: "println!(\"hello\");\"rootQueue\".to_string()".to_string(), + next: StepId::new("create_primitives"), + }, + ), + ( + StepId::new("create_primitives"), + Step::Create { + primitives: vec![ + RuntimePrimitive::Queue { name: LocalVarRef("__mrn_qname_0"), public: false }, + RuntimePrimitive::Future, + RuntimePrimitive::Future, + ], + success: SuccessCreateBranch { + next: StepId::new("after_create_success"), + id_binds: vec![LocalVarRef("queue"), LocalVarRef("future_1"), LocalVarRef("future_2")], + }, + fail: FailCreateBranch { + next: StepId::new("after_create_fail"), + error_binds: vec![LocalVarRef("err_1"), LocalVarRef("err_2"), LocalVarRef("err_3")], + }, + }, + ), + ( + StepId::new("after_create_success"), + Step::RustBlock { + binds: vec![], + code: "println!(\"created queues\");".to_string(), + next: StepId::new("after_match"), + }, + ), + ( + StepId::new("after_create_fail"), + Step::RustBlock { + binds: vec![], + code: "println!(\"{:?} {:?} {:?}\",err_1,err_2,err_3);".to_string(), + next: StepId::new("after_match"), + }, + ), + ( + StepId::new("after_match"), + Step::RustBlock { + binds: vec![], + code: "println!(\"return\");".to_string(), + next: StepId::new("return"), + }, + ), + (StepId::new("return"), Step::ReturnVoid), + ], + }, + )]), + }, + ), + ]), + }; + assert_eq!((true, "".to_string()), ir.is_valid()); +} diff --git a/dsl/src/lib.rs b/dsl/src/lib.rs index b695280..28a662f 100644 --- a/dsl/src/lib.rs +++ b/dsl/src/lib.rs @@ -8,6 +8,7 @@ pub mod parser; // #[cfg(feature = "dsl_sketch")] mod dsl; mod dsl_to_ir; +mod generated_ir; #[cfg(test)] mod ir_test; #[cfg(test)]