Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 53 additions & 27 deletions core/src/lambdadelta_host.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ use std::rc::Rc;
use chrono::{DateTime, Utc};
use uuid::Uuid;

use crate::lambdadelta::{Interp, LdError, LdResult, Value};
use crate::lambdadelta::{Budget, Interp, LdError, LdResult, Value};
use crate::note::Point2D;
use crate::notebook::Notebook;

Expand All @@ -46,32 +46,58 @@ use crate::notebook::Notebook;
/// assert_eq!(out, Value::Int(1));
/// ```
pub fn register(interp: &mut Interp, nb: Rc<RefCell<Notebook>>) {
// Readers (pure).
reader(interp, &nb, "notes", 0, Some(0), bi_notes);
reader(interp, &nb, "note", 1, Some(1), bi_note);
reader(interp, &nb, "title", 1, Some(1), bi_title);
reader(interp, &nb, "content", 1, Some(1), bi_content);
reader(interp, &nb, "attrs", 1, Some(1), bi_attrs);
reader(interp, &nb, "links", 1, Some(1), bi_links);
reader(interp, &nb, "backlinks", 1, Some(1), bi_backlinks);
reader(interp, &nb, "position", 1, Some(1), bi_position);
reader(interp, &nb, "attr", 2, Some(2), bi_attr);
reader(interp, &nb, "search", 1, Some(1), bi_search);
reader(interp, &nb, "resolve-title", 1, Some(1), bi_resolve_title);
reader(interp, &nb, "agents", 0, Some(0), bi_agents);
reader(interp, &nb, "run-agent", 1, Some(1), bi_run_agent);

// Mutators (effects, `!`-suffixed).
mutator(interp, &nb, "create-note!", 1, Some(3), bi_create_note);
mutator(interp, &nb, "set-title!", 2, Some(2), bi_set_title);
mutator(interp, &nb, "set-content!", 2, Some(2), bi_set_content);
mutator(interp, &nb, "set-attr!", 3, Some(3), bi_set_attr);
mutator(interp, &nb, "remove-attr!", 2, Some(2), bi_remove_attr);
mutator(interp, &nb, "move-note!", 3, Some(3), bi_move_note);
mutator(interp, &nb, "resize-note!", 3, Some(3), bi_resize_note);
mutator(interp, &nb, "link!", 2, Some(2), bi_link);
mutator(interp, &nb, "unlink!", 2, Some(2), bi_unlink);
mutator(interp, &nb, "delete-note!", 1, Some(1), bi_delete_note);
register_readers(interp, &nb);
register_mutators(interp, &nb);
}

/// Register only the pure reader builtins — the surface a **formula** or
/// **agent-predicate** context is allowed (spec §5).
pub fn register_readers(interp: &mut Interp, nb: &Rc<RefCell<Notebook>>) {
reader(interp, nb, "notes", 0, Some(0), bi_notes);
reader(interp, nb, "note", 1, Some(1), bi_note);
reader(interp, nb, "title", 1, Some(1), bi_title);
reader(interp, nb, "content", 1, Some(1), bi_content);
reader(interp, nb, "attrs", 1, Some(1), bi_attrs);
reader(interp, nb, "links", 1, Some(1), bi_links);
reader(interp, nb, "backlinks", 1, Some(1), bi_backlinks);
reader(interp, nb, "position", 1, Some(1), bi_position);
reader(interp, nb, "attr", 2, Some(2), bi_attr);
reader(interp, nb, "search", 1, Some(1), bi_search);
reader(interp, nb, "resolve-title", 1, Some(1), bi_resolve_title);
reader(interp, nb, "agents", 0, Some(0), bi_agents);
reader(interp, nb, "run-agent", 1, Some(1), bi_run_agent);
}

/// Register the `!`-suffixed mutators — permitted only in **action** contexts
/// (on-create / agent-action / stamp; spec §5).
pub fn register_mutators(interp: &mut Interp, nb: &Rc<RefCell<Notebook>>) {
mutator(interp, nb, "create-note!", 1, Some(3), bi_create_note);
mutator(interp, nb, "set-title!", 2, Some(2), bi_set_title);
mutator(interp, nb, "set-content!", 2, Some(2), bi_set_content);
mutator(interp, nb, "set-attr!", 3, Some(3), bi_set_attr);
mutator(interp, nb, "remove-attr!", 2, Some(2), bi_remove_attr);
mutator(interp, nb, "move-note!", 3, Some(3), bi_move_note);
mutator(interp, nb, "resize-note!", 3, Some(3), bi_resize_note);
mutator(interp, nb, "link!", 2, Some(2), bi_link);
mutator(interp, nb, "unlink!", 2, Some(2), bi_unlink);
mutator(interp, nb, "delete-note!", 1, Some(1), bi_delete_note);
}

/// Evaluate a **formula** (spec §5): a pure expression with `self` bound to a
/// note's snapshot map and only the *reader* builtins in scope — mutators are
/// deliberately absent, so a formula can never change the notebook. This is the
/// L1 surface: `(count (words (content self)))`, `(= (attr self :status) "todo")`.
pub fn eval_formula(
nb: Rc<RefCell<Notebook>>,
self_id: &Uuid,
src: &str,
budget: Budget,
) -> LdResult<Value> {
let mut interp = Interp::new();
register_readers(&mut interp, &nb);
let self_val = note_to_value(&nb.borrow(), self_id).unwrap_or(Value::Nil);
interp.global.set(Rc::from("self"), self_val);
interp.eval_str(src, budget)
}

type ReadFn = fn(&Notebook, &[Value]) -> LdResult<Value>;
Expand Down
33 changes: 33 additions & 0 deletions core/src/lambdadelta_host/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,3 +175,36 @@ fn mutator_on_unknown_id_errors() {
);
assert!(matches!(r, Err(LdError::User(_))));
}

#[test]
fn formula_binds_self_and_is_read_only() {
let (nb, _i) = setup();
let a = id_of(&nb, "Alpha");
assert!(a.is_some());
let Some(a) = a else { return };

// `self` is bound to the note; readers are in scope. The canonical L1 fx.
assert_eq!(
super::eval_formula(
nb.clone(),
&a,
"(count (words (content self)))",
Budget::new()
),
Ok(Value::Int(4))
);
assert_eq!(
super::eval_formula(nb.clone(), &a, "(:title self)", Budget::new()),
Ok(Value::str("Alpha"))
);

// Mutators are deliberately absent — a formula cannot change the notebook.
let m = super::eval_formula(
nb.clone(),
&a,
"(set-title! (:id self) \"X\")",
Budget::new(),
);
assert!(matches!(m, Err(LdError::Unbound(_))));
assert_eq!(id_of(&nb, "Alpha"), Some(a)); // title unchanged
}
20 changes: 20 additions & 0 deletions core/src/wasm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,26 @@ impl WasmNotebook {
result.map(|v| v.to_string()).map_err(err)
}

/// Evaluate a **formula** against note `note_id`: a pure λδ expression with
/// `self` bound to that note and only reader builtins in scope (spec §5).
/// Read-only — the notebook is never mutated. Returns the printed result.
/// This is the L1 "fx field" entrypoint.
#[wasm_bindgen(js_name = evalFormula)]
pub fn eval_formula(&mut self, note_id: &str, src: &str) -> Result<String, JsValue> {
use crate::lambdadelta::Budget;
use std::cell::RefCell;
use std::rc::Rc;

let id = parse_id(note_id)?;
let shared = Rc::new(RefCell::new(std::mem::take(&mut self.inner)));
let result = crate::lambdadelta_host::eval_formula(shared.clone(), &id, src, Budget::new());
match Rc::try_unwrap(shared) {
Ok(cell) => self.inner = cell.into_inner(),
Err(still_shared) => self.inner = still_shared.borrow().clone(),
}
result.map(|v| v.to_string()).map_err(err)
}

/// Export every note as a Markdown file: `[{ name, content }]`.
pub fn export_markdown(&self) -> Result<JsValue, JsValue> {
let files: Vec<MarkdownFileView> = crate::exchange::to_markdown(&self.inner)
Expand Down
Loading