From b436e098406a4025282b3221f4226b1c660227f0 Mon Sep 17 00:00:00 2001 From: Brummel Date: Wed, 25 Mar 2026 15:02:19 +0100 Subject: [PATCH] Add documentation infrastructure for RTL symbols Introduces `RtlDocEntry` and `RtlRegistration` structs to store and manage documentation for native functions and constants. The `Environment` struct is updated to include a registry for these documentation entries. The `register_native_fn` and `register_constant` methods now return an `RtlRegistration` builder, allowing for optional chaining of `.doc()`, `.description()`, and `.examples()` methods before the registration is finalized when the builder is dropped. Macros `rtl_fn!` and `rtl_const!` are introduced to simplify the registration process with documentation. The MCP server is extended with `get_rtl_docs` and `get_symbol_doc` tools to expose this documentation externally. Specific RTL functions and constants (e.g., `+`, `-`, `*`, `/`, `NaN`, `true`, `false`, `date`, `now`, math functions, `series`, `push`, `len`, `create-random-ohlc`, `create-ticker`, `pipe`) have been updated to include their respective documentation using the new infrastructure. The `Signature` and `StaticType` types have gained `to_doc_string` methods for generating human-readable documentation strings. --- src/ast/environment.rs | 141 +++++++++++++++++++++++++++++++++++++- src/ast/mcp_server.rs | 33 +++++++++ src/ast/rtl/core.rs | 60 ++++++++++------ src/ast/rtl/datetime.rs | 6 +- src/ast/rtl/math.rs | 72 +++++++++---------- src/ast/rtl/mod.rs | 93 +++++++++++++++++++++++++ src/ast/rtl/series/mod.rs | 16 ++++- src/ast/rtl/streams.rs | 14 +++- src/ast/types.rs | 34 +++++++++ 9 files changed, 400 insertions(+), 69 deletions(-) diff --git a/src/ast/environment.rs b/src/ast/environment.rs index a0da692..90e6627 100644 --- a/src/ast/environment.rs +++ b/src/ast/environment.rs @@ -75,6 +75,62 @@ impl CompilationResult { } } +/// Documentation entry for a single RTL symbol (function or constant). +/// Populated via the builder returned by `register_native_fn` / `register_constant`. +pub struct RtlDocEntry { + pub name: String, + /// Short one-line description (required). + pub one_liner: &'static str, + /// Optional longer explanation. + pub description: Option<&'static str>, + /// Optional Myc code examples. + pub examples: Option<&'static [&'static str]>, +} + +/// Builder returned by `register_native_fn` and `register_constant`. +/// Call `.doc(...)` to attach documentation; optional `.description(...)` and `.examples(...)` +/// can be chained. The entry is written to the registry when the builder is dropped. +pub struct RtlRegistration { + name: String, + rtl_docs: Rc>>, + one_liner: Option<&'static str>, + description: Option<&'static str>, + examples: Option<&'static [&'static str]>, +} + +impl RtlRegistration { + /// Attach a required one-line description. + pub fn doc(mut self, one_liner: &'static str) -> Self { + self.one_liner = Some(one_liner); + self + } + + /// Attach an optional longer description (must call `.doc()` first). + pub fn description(mut self, desc: &'static str) -> Self { + self.description = Some(desc); + self + } + + /// Attach optional usage examples as Myc code strings. + pub fn examples(mut self, ex: &'static [&'static str]) -> Self { + self.examples = Some(ex); + self + } +} + +impl Drop for RtlRegistration { + fn drop(&mut self) { + if let Some(one_liner) = self.one_liner { + self.rtl_docs.borrow_mut().push(RtlDocEntry { + name: self.name.clone(), + one_liner, + description: self.description, + examples: self.examples, + }); + } + } +} + pub struct Environment { pub root_types: Rc>>, pub root_purity: Rc>>, @@ -91,6 +147,7 @@ pub struct Environment { pub pipeline_generators: Rc>>, pub search_paths: Rc>>, pub loaded_modules: Rc>>, + pub rtl_docs: Rc>>, } struct EnvFunctionRegistry { @@ -177,6 +234,7 @@ impl Environment { pipeline_generators: Rc::new(RefCell::new(Vec::new())), search_paths: Rc::new(RefCell::new(Vec::new())), loaded_modules: Rc::new(RefCell::new(HashSet::new())), + rtl_docs: Rc::new(RefCell::new(Vec::new())), }; rtl::register(&env); @@ -560,7 +618,7 @@ impl Environment { ty: StaticType, purity_level: Purity, func: impl Fn(&[Value]) -> Value + 'static, - ) { + ) -> RtlRegistration { self.register_native( name, ty, @@ -569,9 +627,16 @@ impl Environment { purity: purity_level, }), ); + RtlRegistration { + name: name.to_string(), + rtl_docs: Rc::clone(&self.rtl_docs), + one_liner: None, + description: None, + examples: None, + } } - pub fn register_constant(&self, name: &str, ty: StaticType, val: Value) { + pub fn register_constant(&self, name: &str, ty: StaticType, val: Value) -> RtlRegistration { let mut types = self.root_types.borrow_mut(); let mut values = self.root_values.borrow_mut(); let mut purity = self.root_purity.borrow_mut(); @@ -581,7 +646,7 @@ impl Environment { // populate root_scopes[0] let mut root_scopes = self.root_scopes.borrow_mut(); let mut slot_count = self.root_slot_count.borrow_mut(); - + let global_idx = GlobalIdx(*slot_count); root_scopes[0].locals.insert( Symbol::from(name), @@ -597,6 +662,14 @@ impl Environment { types.push(ty); purity.push(Purity::Pure); values.push(val); + + RtlRegistration { + name: name.to_string(), + rtl_docs: Rc::clone(&self.rtl_docs), + one_liner: None, + description: None, + examples: None, + } } @@ -613,6 +686,68 @@ impl Environment { names } + /// Returns formatted documentation for all RTL symbols that have been annotated + /// via the `.doc()` builder. Each entry includes the name, type signature + /// (derived from `StaticType`), one-liner, optional description, and examples. + pub fn list_rtl_docs(&self) -> Vec { + use crate::ast::nodes::Symbol; + let docs = self.rtl_docs.borrow(); + let root_scopes = self.root_scopes.borrow(); + let scope = &root_scopes[0]; + + let mut entries: Vec = docs.iter().map(|entry| { + // Derive the signature from the registered StaticType. + let sig = scope + .locals + .get(&Symbol::from(entry.name.as_str())) + .map(|info| info._ty.to_doc_string()) + .unwrap_or_else(|| "unknown".to_string()); + + let mut out = format!("{} : {}\n {}", entry.name, sig, entry.one_liner); + + if let Some(desc) = entry.description { + out.push_str(&format!("\n {}", desc)); + } + if let Some(examples) = entry.examples { + out.push_str("\n Examples:"); + for ex in examples.iter() { + out.push_str(&format!("\n {}", ex)); + } + } + out + }).collect(); + + entries.sort_by_key(|a| a.to_lowercase()); + entries + } + + /// Returns formatted documentation for a single RTL symbol by name, + /// or `None` if the symbol has no doc entry. + pub fn get_rtl_doc(&self, name: &str) -> Option { + use crate::ast::nodes::Symbol; + let docs = self.rtl_docs.borrow(); + let entry = docs.iter().find(|e| e.name == name)?; + + let root_scopes = self.root_scopes.borrow(); + let sig = root_scopes[0] + .locals + .get(&Symbol::from(name)) + .map(|info| info._ty.to_doc_string()) + .unwrap_or_else(|| "unknown".to_string()); + + let mut out = format!("{} : {}\n {}", entry.name, sig, entry.one_liner); + if let Some(desc) = entry.description { + out.push_str(&format!("\n {}", desc)); + } + if let Some(examples) = entry.examples { + out.push_str("\n Examples:"); + for ex in examples.iter() { + out.push_str(&format!("\n {}", ex)); + } + } + Some(out) + } + pub fn dump_ast(&self, source: &str) -> Result { self.preload_dependencies(source, None)?; let compiled = self.compile(source).into_result()?; diff --git a/src/ast/mcp_server.rs b/src/ast/mcp_server.rs index 339e41f..1a3e3a7 100644 --- a/src/ast/mcp_server.rs +++ b/src/ast/mcp_server.rs @@ -17,6 +17,13 @@ pub struct CodeInput { pub code: String, } +/// Input struct for tools that look up a single RTL symbol by name. +#[derive(Debug, serde::Deserialize, schemars::JsonSchema)] +pub struct SymbolInput { + /// The RTL symbol name to look up (e.g. "+", "push", "series"). + pub name: String, +} + /// MCP server exposing the Myc compiler as a set of tools for LLMs. #[derive(Clone)] pub struct MycMcpServer { @@ -115,6 +122,32 @@ impl MycMcpServer { fn get_language_spec(&self) -> String { LANGUAGE_SPEC.to_string() } + + /// Returns documentation for all documented RTL symbols. + #[tool(description = "Return documentation for ALL built-in RTL functions and constants at once. \ + Prefer get_symbol_doc for individual lookups to save tokens. \ + Use this only when you need a full overview of the entire standard library.")] + fn get_rtl_docs(&self) -> String { + let env = Self::make_env(); + let docs = env.list_rtl_docs(); + if docs.is_empty() { + "No documented RTL symbols found.".to_string() + } else { + docs.join("\n\n") + } + } + + /// Returns documentation for a single RTL symbol by name. + #[tool(description = "Return the type signature, description, and examples for a single \ + built-in RTL symbol. Use list_builtins to see all available names, then call this \ + for each symbol you need details on. More token-efficient than get_rtl_docs.")] + fn get_symbol_doc(&self, Parameters(SymbolInput { name }): Parameters) -> String { + let env = Self::make_env(); + match env.get_rtl_doc(&name) { + Some(doc) => doc, + None => format!("No documentation found for symbol '{}'.", name), + } + } } #[tool_handler] diff --git a/src/ast/rtl/core.rs b/src/ast/rtl/core.rs index 5b93c7b..ced35e2 100644 --- a/src/ast/rtl/core.rs +++ b/src/ast/rtl/core.rs @@ -15,9 +15,12 @@ fn register_constants(env: &Environment) { // In Delphi RTL: CFalse, CTrue, CNaN // We register NaN as a value, not a function. - env.register_constant("NaN", StaticType::Float, Value::Float(f64::NAN)); - env.register_constant("true", StaticType::Bool, Value::Bool(true)); - env.register_constant("false", StaticType::Bool, Value::Bool(false)); + env.register_constant("NaN", StaticType::Float, Value::Float(f64::NAN)) + .doc("Not-a-Number float constant. Use to represent undefined numeric results."); + env.register_constant("true", StaticType::Bool, Value::Bool(true)) + .doc("Boolean true constant."); + env.register_constant("false", StaticType::Bool, Value::Bool(false)) + .doc("Boolean false constant."); } fn register_arithmetic(env: &Environment) { @@ -76,7 +79,9 @@ fn register_arithmetic(env: &Environment) { Value::Float(acc) } } - }); + }).doc("Adds two or more values.") + .description("Supports int, float, text concatenation, and datetime+ms offset. Also variadic: (+ 1 2 3) => 6.") + .examples(&["(+ 1 2)", "(+ 1.5 2.5)", "(+ \"Hello\" \" World\")", "(+ my-date 86400000)"]); // --- Subtract (-) --- let sub_ty = StaticType::FunctionOverloads(vec![ @@ -150,7 +155,8 @@ fn register_arithmetic(env: &Environment) { } else { Value::Float(acc) } - }); + }).doc("Subtracts values. Also supports unary negation: (- x).") + .examples(&["(- 10 3)", "(- 5)", "(- 2026-01-01 2025-01-01)"]); // --- Multiply (*) --- let mul_ty = StaticType::FunctionOverloads(vec![ @@ -192,7 +198,8 @@ fn register_arithmetic(env: &Environment) { Value::Float(acc) } } - }); + }).doc("Multiplies values. Also variadic: (* 2 3 4) => 24.") + .examples(&["(* 3 4)", "(* 1.5 2.0)"]); // --- Divide (/) --- let div_ty = StaticType::FunctionOverloads(vec![ @@ -224,7 +231,8 @@ fn register_arithmetic(env: &Environment) { } else { Value::Float(a / b) } - }); + }).doc("Divides two numbers, always returns float. Returns NaN on division by zero.") + .examples(&["(/ 10 3)", "(/ 7.0 2.0)"]); // --- Integer Divide (//) --- let int_div_ty = StaticType::Function(Box::new(Signature { @@ -247,7 +255,8 @@ fn register_arithmetic(env: &Environment) { // Usually div is for integers. Let's stick to Int. _ => Value::Void, } - }); + }).doc("Integer division. Returns int, truncates towards zero.") + .examples(&["(// 10 3)"]); // --- Modulus (%) --- let mod_ty = StaticType::Function(Box::new(Signature { @@ -268,7 +277,8 @@ fn register_arithmetic(env: &Environment) { } _ => Value::Void, } - }); + }).doc("Modulo: remainder of integer division.") + .examples(&["(% 10 3)"]); } fn register_comparison(env: &Environment) { @@ -300,7 +310,7 @@ fn register_comparison(env: &Environment) { (Value::DateTime(a), Value::DateTime(b)) => Value::Bool(a > b), _ => Value::Bool(false), } - }); + }).doc("Greater-than comparison. Supports int, float, datetime."); // --- Less Than (<) --- env.register_native_fn("<", cmp_ty.clone(), Purity::Pure, |args| { @@ -315,7 +325,7 @@ fn register_comparison(env: &Environment) { (Value::DateTime(a), Value::DateTime(b)) => Value::Bool(a < b), _ => Value::Bool(false), } - }); + }).doc("Less-than comparison. Supports int, float, datetime."); // --- Greater Or Equal (>=) --- env.register_native_fn(">=", cmp_ty.clone(), Purity::Pure, |args| { @@ -330,7 +340,7 @@ fn register_comparison(env: &Environment) { (Value::DateTime(a), Value::DateTime(b)) => Value::Bool(a >= b), _ => Value::Bool(false), } - }); + }).doc("Greater-than-or-equal comparison. Supports int, float, datetime."); // --- Less Or Equal (<=) --- env.register_native_fn("<=", cmp_ty.clone(), Purity::Pure, |args| { @@ -345,7 +355,7 @@ fn register_comparison(env: &Environment) { (Value::DateTime(a), Value::DateTime(b)) => Value::Bool(a <= b), _ => Value::Bool(false), } - }); + }).doc("Less-than-or-equal comparison. Supports int, float, datetime."); // --- Equal (=) --- let eq_ty = StaticType::Function(Box::new(Signature { @@ -373,7 +383,8 @@ fn register_comparison(env: &Environment) { (Value::Void, Value::Void) => Value::Bool(true), _ => Value::Bool(false), } - }); + }).doc("Structural equality. Records are equal if they have the same layout and field values.") + .examples(&["(= 1 1)", "(= {:a 1} {:a 1})", "(= :foo :foo)"]); // --- Not Equal (<>) --- env.register_native_fn("<>", eq_ty, Purity::Pure, |args| { @@ -392,7 +403,7 @@ fn register_comparison(env: &Environment) { (Value::Void, Value::Void) => Value::Bool(false), _ => Value::Bool(true), } - }); + }).doc("Structural inequality. The inverse of =."); } fn register_logic(env: &Environment) { @@ -416,7 +427,9 @@ fn register_logic(env: &Environment) { Value::Int(i) => Value::Int(!i), // Bitwise NOT _ => Value::Void, } - }); + }).doc("Logical NOT for bool, bitwise NOT for int.") + .examples(&["(not true)", "(not 0b1010)"]); + // --- And (and) --- let logic_op_ty = StaticType::FunctionOverloads(vec![ @@ -438,7 +451,7 @@ fn register_logic(env: &Environment) { (Value::Int(a), Value::Int(b)) => Value::Int(a & b), _ => Value::Void, } - }); + }).doc("Logical AND for bool, bitwise AND for int."); // --- Or (or) --- env.register_native_fn("or", logic_op_ty.clone(), Purity::Pure, |args| { @@ -450,7 +463,7 @@ fn register_logic(env: &Environment) { (Value::Int(a), Value::Int(b)) => Value::Int(a | b), _ => Value::Void, } - }); + }).doc("Logical OR for bool, bitwise OR for int."); // --- Xor (xor) --- env.register_native_fn("xor", logic_op_ty.clone(), Purity::Pure, |args| { @@ -462,7 +475,7 @@ fn register_logic(env: &Environment) { (Value::Int(a), Value::Int(b)) => Value::Int(a ^ b), _ => Value::Void, } - }); + }).doc("Logical XOR for bool, bitwise XOR for int."); // --- Shift Left (<<) --- let shift_ty = StaticType::Function(Box::new(Signature { @@ -477,7 +490,8 @@ fn register_logic(env: &Environment) { (Value::Int(a), Value::Int(b)) => Value::Int(a << b), _ => Value::Void, } - }); + }).doc("Bitwise left shift.") + .examples(&["(<< 1 4)"]); // --- Shift Right (>>) --- env.register_native_fn(">>", shift_ty, Purity::Pure, |args| { @@ -488,7 +502,8 @@ fn register_logic(env: &Environment) { (Value::Int(a), Value::Int(b)) => Value::Int(a >> b), _ => Value::Void, } - }); + }).doc("Bitwise right shift.") + .examples(&["(>> 16 4)"]); } fn register_io(env: &Environment) { @@ -505,5 +520,6 @@ fn register_io(env: &Environment) { } println!(); Value::Void - }); + }).doc("Prints all arguments to stdout followed by a newline. Returns void.") + .examples(&["(print \"Hello World\")", "(print x y z)"]); } diff --git a/src/ast/rtl/datetime.rs b/src/ast/rtl/datetime.rs index 27160df..5bf3420 100644 --- a/src/ast/rtl/datetime.rs +++ b/src/ast/rtl/datetime.rs @@ -25,7 +25,9 @@ pub fn register(env: &Environment) { } } Value::Void - }); + }).doc("Parses a date string and returns a datetime (milliseconds since epoch).") + .description("Supported formats: \"YYYY-MM-DD\" and \"YYYY-MM-DD HH:MM:SS\".") + .examples(&["(date \"2024-01-15\")", "(date \"2024-01-15 09:30:00)\""]); let now_ty = StaticType::Function(Box::new(Signature { params: StaticType::Tuple(vec![]), @@ -35,5 +37,5 @@ pub fn register(env: &Environment) { env.register_native_fn("now", now_ty, Purity::SideEffectFree, |_| { let ts = chrono::Utc::now().timestamp_millis(); Value::DateTime(ts) - }); + }).doc("Returns the current UTC timestamp as milliseconds since the Unix epoch."); } diff --git a/src/ast/rtl/math.rs b/src/ast/rtl/math.rs index 977c7c8..d02ad48 100644 --- a/src/ast/rtl/math.rs +++ b/src/ast/rtl/math.rs @@ -6,8 +6,10 @@ use std::rc::Rc; pub fn register(env: &Environment) { // Constants - env.register_constant("PI", StaticType::Float, Value::Float(consts::PI)); - env.register_constant("E", StaticType::Float, Value::Float(consts::E)); + env.register_constant("PI", StaticType::Float, Value::Float(consts::PI)) + .doc("Mathematical constant π ≈ 3.14159265."); + env.register_constant("E", StaticType::Float, Value::Float(consts::E)) + .doc("Mathematical constant e ≈ 2.71828182 (Euler's number)."); // Helper to get f64 from Value (Int or Float) fn to_f64(v: &Value) -> Option { @@ -19,7 +21,7 @@ pub fn register(env: &Environment) { } // Unary functions (float -> float) - let register_unary = |name: &'static str, f: fn(f64) -> f64| { + let register_unary = |name: &'static str, f: fn(f64) -> f64, doc: &'static str| { let ty = StaticType::Function(Box::new(Signature { params: StaticType::Tuple(vec![StaticType::Float]), ret: StaticType::Float, @@ -30,11 +32,11 @@ pub fn register(env: &Environment) { } else { Value::Void } - }); + }).doc(doc); }; // Unary functions (float -> int) - let register_to_int = |name: &'static str, f: fn(f64) -> f64| { + let register_to_int = |name: &'static str, f: fn(f64) -> f64, doc: &'static str| { let ty = StaticType::Function(Box::new(Signature { params: StaticType::Tuple(vec![StaticType::Float]), ret: StaticType::Int, @@ -45,29 +47,29 @@ pub fn register(env: &Environment) { } else { Value::Void } - }); + }).doc(doc); }; - register_unary("sqrt", f64::sqrt); - register_unary("sin", f64::sin); - register_unary("cos", f64::cos); - register_unary("tan", f64::tan); - register_unary("asin", f64::asin); - register_unary("acos", f64::acos); - register_unary("atan", f64::atan); - register_unary("exp", f64::exp); - register_unary("ln", f64::ln); - register_unary("log10", f64::log10); - register_unary("fract", f64::fract); + register_unary("sqrt", f64::sqrt, "Returns the square root of x."); + register_unary("sin", f64::sin, "Returns the sine of x (radians)."); + register_unary("cos", f64::cos, "Returns the cosine of x (radians)."); + register_unary("tan", f64::tan, "Returns the tangent of x (radians)."); + register_unary("asin", f64::asin, "Returns the arcsine of x in radians."); + register_unary("acos", f64::acos, "Returns the arccosine of x in radians."); + register_unary("atan", f64::atan, "Returns the arctangent of x in radians."); + register_unary("exp", f64::exp, "Returns e raised to the power of x."); + register_unary("ln", f64::ln, "Returns the natural logarithm of x."); + register_unary("log10",f64::log10,"Returns the base-10 logarithm of x."); + register_unary("fract",f64::fract,"Returns the fractional part of x."); // Rounding functions returning Int - register_to_int("round", f64::round); - register_to_int("floor", f64::floor); - register_to_int("ceil", f64::ceil); - register_to_int("trunc", f64::trunc); + register_to_int("round", f64::round, "Rounds x to the nearest integer."); + register_to_int("floor", f64::floor, "Rounds x down to the largest integer ≤ x."); + register_to_int("ceil", f64::ceil, "Rounds x up to the smallest integer ≥ x."); + register_to_int("trunc", f64::trunc, "Truncates x towards zero."); // Binary functions (float, float -> float) - let register_binary = |name: &'static str, f: fn(f64, f64) -> f64| { + let register_binary = |name: &'static str, f: fn(f64, f64) -> f64, doc: &'static str| { let ty = StaticType::Function(Box::new(Signature { params: StaticType::Tuple(vec![StaticType::Float, StaticType::Float]), ret: StaticType::Float, @@ -78,12 +80,12 @@ pub fn register(env: &Environment) { } else { Value::Void } - }); + }).doc(doc); }; - register_binary("atan2", f64::atan2); - register_binary("pow", f64::powf); - register_binary("log", f64::log); + register_binary("atan2", f64::atan2, "Returns the arctangent of y/x, using signs to determine the quadrant."); + register_binary("pow", f64::powf, "Returns x raised to the power y."); + register_binary("log", f64::log, "Returns the logarithm of x in the given base: (log base x)."); // Specialized abs to handle Int -> Int, Float -> Float let abs_ty = StaticType::Function(Box::new(Signature { @@ -99,7 +101,8 @@ pub fn register(env: &Environment) { Value::Float(f) => Value::Float(f.abs()), _ => Value::Void, } - }); + }).doc("Returns the absolute value of a number. Works for both int and float.") + .examples(&["(abs -5)", "(abs -3.14)"]); // Variadic min / max let variadic_ty = StaticType::Function(Box::new(Signature { @@ -120,7 +123,7 @@ pub fn register(env: &Environment) { } } best - }); + }).doc("Returns the minimum of its arguments. Variadic: (min 3 1 2) => 1."); env.register_native_fn("max", variadic_ty, Purity::Pure, |args| { if args.is_empty() { @@ -135,7 +138,7 @@ pub fn register(env: &Environment) { } } best - }); + }).doc("Returns the maximum of its arguments. Variadic: (max 3 1 2) => 3."); // Random generator factory (Closure-based) let generator_sig = Signature { @@ -154,11 +157,7 @@ pub fn register(env: &Environment) { }, ]); - env.register_native_fn( - "make-random", - make_random_ty, - Purity::SideEffectFree, - |args| { + env.register_native_fn("make-random", make_random_ty, Purity::SideEffectFree, |args| { let rng = if args.is_empty() { fastrand::Rng::new() } else if let Value::Int(s) = args[0] { @@ -173,6 +172,7 @@ pub fn register(env: &Environment) { func: Rc::new(move |_args| Value::Float(prng.borrow_mut().f64())), purity: Purity::Impure, })) - }, - ); + }).doc("Creates a random number generator function returning floats in [0, 1).") + .description("Optionally seeded: (make-random seed) for reproducible sequences.") + .examples(&["(def rng (make-random 42))", "(rng)"]); } diff --git a/src/ast/rtl/mod.rs b/src/ast/rtl/mod.rs index d87e6ee..a31f4a0 100644 --- a/src/ast/rtl/mod.rs +++ b/src/ast/rtl/mod.rs @@ -15,3 +15,96 @@ pub fn register(env: &Environment) { series::register(env); streams::register(env); } + +/// Registers a native RTL function and optionally attaches documentation. +/// +/// Required: `doc:` — a short one-line description. +/// Optional: `description:` — a longer explanation. +/// Optional: `examples:` — a `&'static [&'static str]` of Myc code snippets. +/// +/// # Example +/// ```ignore +/// rtl_fn!(env, "+", +/// doc: "Adds two values.", +/// add_ty, Pure, |args| { ... } +/// ); +/// +/// rtl_fn!(env, "push", +/// doc: "Pushes a new record into a series.", +/// description: "The record must match the series layout.", +/// examples: &["(push my_ticks {:price 10.5})"], +/// push_ty, Impure, |args| { ... } +/// ); +/// ``` +#[macro_export] +macro_rules! rtl_fn { + // With description and examples + ($env:expr, $name:expr, + doc: $doc:expr, + description: $desc:expr, + examples: $ex:expr, + $ty:expr, $purity:expr, $impl:expr $(,)?) => { + $env.register_native_fn($name, $ty, $purity, $impl) + .doc($doc) + .description($desc) + .examples($ex); + }; + // With description only + ($env:expr, $name:expr, + doc: $doc:expr, + description: $desc:expr, + $ty:expr, $purity:expr, $impl:expr $(,)?) => { + $env.register_native_fn($name, $ty, $purity, $impl) + .doc($doc) + .description($desc); + }; + // With examples only + ($env:expr, $name:expr, + doc: $doc:expr, + examples: $ex:expr, + $ty:expr, $purity:expr, $impl:expr $(,)?) => { + $env.register_native_fn($name, $ty, $purity, $impl) + .doc($doc) + .examples($ex); + }; + // doc only + ($env:expr, $name:expr, + doc: $doc:expr, + $ty:expr, $purity:expr, $impl:expr $(,)?) => { + $env.register_native_fn($name, $ty, $purity, $impl) + .doc($doc); + }; +} + +/// Registers a native RTL constant and optionally attaches documentation. +/// Same optional fields as `rtl_fn!`. +#[macro_export] +macro_rules! rtl_const { + // With description and examples + ($env:expr, $name:expr, + doc: $doc:expr, + description: $desc:expr, + examples: $ex:expr, + $ty:expr, $val:expr $(,)?) => { + $env.register_constant($name, $ty, $val) + .doc($doc) + .description($desc) + .examples($ex); + }; + // With description only + ($env:expr, $name:expr, + doc: $doc:expr, + description: $desc:expr, + $ty:expr, $val:expr $(,)?) => { + $env.register_constant($name, $ty, $val) + .doc($doc) + .description($desc); + }; + // doc only + ($env:expr, $name:expr, + doc: $doc:expr, + $ty:expr, $val:expr $(,)?) => { + $env.register_constant($name, $ty, $val) + .doc($doc); + }; +} diff --git a/src/ast/rtl/series/mod.rs b/src/ast/rtl/series/mod.rs index 405ab64..9091790 100644 --- a/src/ast/rtl/series/mod.rs +++ b/src/ast/rtl/series/mod.rs @@ -56,7 +56,12 @@ pub fn register(env: &Environment) { _ => panic!("series expects a lookback_limit and a type keyword (:float, :int...) or a schema record (e.g., {{:price :float}})"), } }, - ); + ).doc("Creates a new series (ring buffer) with a defined layout and lookback limit.") + .description("First arg is the lookback limit (int), second is either a type keyword (:float, :int, :bool, :text, :datetime) or a schema record ({:field :type}).") + .examples(&[ + "(series 100 :float)", + "(series 200 {:price :float :volume :int})", + ]); // (push series value) -> Void env.register_native_fn( @@ -78,7 +83,11 @@ pub fn register(env: &Environment) { } panic!("push expects a series as the first argument") }, - ); + ).doc("Pushes a new value into a series. After push, index 0 returns this value.") + .examples(&[ + "(push my-ticks {:price 10.5 :volume 100})", + "(push prices 42.0)", + ]); // (len series) -> Int env.register_native_fn( @@ -94,5 +103,6 @@ pub fn register(env: &Environment) { } Value::Int(0) }, - ); + ).doc("Returns the current number of items stored in a series.") + .examples(&["(len my-ticks)"]); } diff --git a/src/ast/rtl/streams.rs b/src/ast/rtl/streams.rs index af7cece..3e2ec27 100644 --- a/src/ast/rtl/streams.rs +++ b/src/ast/rtl/streams.rs @@ -467,7 +467,9 @@ pub fn register(env: &Environment) { // 5. Return the stream reference to the script Value::Object(Rc::new(stream_node)) }, - ); + ).doc("Creates a stream of random OHLC (Open-High-Low-Close) candles.") + .description("Args: (seed: int, limit: int). Uses a random walk model. Connect via (pipe ...).") + .examples(&["(def ohlc (create-random-ohlc 42 1000))"]); // (create-ticker condition-closure) -> StreamNode let ticker_generators = env.pipeline_generators.clone(); @@ -525,7 +527,8 @@ pub fn register(env: &Environment) { // 5. Return stream Value::Object(Rc::new(stream_node)) }, - ); + ).doc("Creates a stream driven by a boolean closure. Ticks as long as closure returns true.") + .examples(&["(def ticker (create-ticker (fn [] (< (now) end-time))))"]); // (pipe inputs lambda) -> StreamNode // inputs: a Tuple of StreamNodes or a single StreamNode @@ -586,7 +589,12 @@ pub fn register(env: &Environment) { let node = build_pipeline_node(obs_streams, executor, &StaticType::Any); Value::Object(node) }, - ); + ).doc("Connects a lambda to one or more streams, producing a transformed output stream.") + .description("Returns void from lambda to act as a filter (value is dropped). Supports tuple inputs for barrier synchronization.") + .examples(&[ + "(pipe ohlc (fn [bar] (.close bar)))", + "(pipe [stream-a stream-b] (fn [a b] (+ a b)))", + ]); } #[cfg(test)] diff --git a/src/ast/types.rs b/src/ast/types.rs index a8da050..ce7d7d3 100644 --- a/src/ast/types.rs +++ b/src/ast/types.rs @@ -422,6 +422,40 @@ impl fmt::Display for StaticType { } } +impl Signature { + /// Returns a human-readable signature string for documentation purposes. + /// Format: `(param_types) → return_type` + pub fn to_doc_string(&self) -> String { + let params = match &self.params { + StaticType::Tuple(elems) => elems + .iter() + .map(|t| t.to_string()) + .collect::>() + .join(", "), + other => other.to_string(), + }; + format!("({}) → {}", params, self.ret) + } +} + +impl StaticType { + /// Returns a human-readable type signature for documentation purposes. + /// Unlike `Display`, this expands `FunctionOverloads` into individual signatures + /// and labels polymorphic functions explicitly. + pub fn to_doc_string(&self) -> String { + match self { + StaticType::Function(sig) => sig.to_doc_string(), + StaticType::FunctionOverloads(sigs) => sigs + .iter() + .map(|s| s.to_doc_string()) + .collect::>() + .join(" | "), + StaticType::PolymorphicFn { .. } => "(any) → any [polymorphic]".to_string(), + other => other.to_string(), + } + } +} + impl StaticType { /// Returns true if `other` can be assigned to a location of type `self`. pub fn is_assignable_from(&self, other: &StaticType) -> bool {