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
+138 -3
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();
@@ -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()?;