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:
+138
-3
@@ -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();
|
||||
@@ -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<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()?;
|
||||
|
||||
@@ -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
@@ -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)"]);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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)"]);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
@@ -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)]
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user