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.
This commit is contained in:
2026-03-25 15:02:19 +01:00
parent 446bdcd42d
commit b436e09840
9 changed files with 400 additions and 69 deletions
+137 -2
View File
@@ -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<RefCell<Vec<RtlDocEntry>>>,
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<RefCell<Vec<StaticType>>>,
pub root_purity: Rc<RefCell<Vec<Purity>>>,
@@ -91,6 +147,7 @@ pub struct Environment {
pub pipeline_generators: Rc<RefCell<Vec<PipelineGenerator>>>,
pub search_paths: Rc<RefCell<Vec<PathBuf>>>,
pub loaded_modules: Rc<RefCell<HashSet<PathBuf>>>,
pub rtl_docs: Rc<RefCell<Vec<RtlDocEntry>>>,
}
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();
@@ -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<String> {
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<String> = 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<String> {
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<String, String> {
self.preload_dependencies(source, None)?;
let compiled = self.compile(source).into_result()?;
+33
View File
@@ -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<SymbolInput>) -> 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]
+38 -22
View File
@@ -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)"]);
}
+4 -2
View File
@@ -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.");
}
+36 -36
View File
@@ -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<f64> {
@@ -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)"]);
}
+93
View File
@@ -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);
};
}
+13 -3
View File
@@ -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)"]);
}
+11 -3
View File
@@ -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)]
+34
View File
@@ -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::<Vec<_>>()
.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::<Vec<_>>()
.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 {