Skip to main content

aver/types/
branch_path.rs

1//! BranchPath — opaque identifier for a branch in the structural tree of
2//! `!`/`?!` groups. Used in Oracle-proof specs for generative effects
3//! (`oracle : (BranchPath, Int, args...) -> T`) and, via the trace-API
4//! `.path()` bridge, for tying trace events to oracle invocations.
5//!
6//! Three constructors — the only way to obtain a `BranchPath`:
7//!
8//! - `BranchPath.root : () -> BranchPath`                  — canonical root.
9//! - `BranchPath.child : (BranchPath, Int) -> BranchPath`  — extend a path by
10//!   entering a branch of a group.
11//! - `BranchPath.parse : String -> BranchPath`             — parse dewey-decimal
12//!   (`"2.0"`), used to bridge recordings to specs.
13//!
14//! Internal representation is a record `{ dewey: String }` — user code
15//! cannot construct arbitrary paths because the field is not exposed as a
16//! public member and record-literal construction of built-in types isn't
17//! available at the surface.
18
19use std::collections::HashMap;
20use std::sync::Arc as Rc;
21
22use crate::nan_value::{Arena, ArenaEntry, NanIntExt, NanValue};
23use crate::value::RuntimeError;
24
25pub const TYPE_NAME: &str = "BranchPath";
26const FIELD: &str = "dewey";
27
28pub fn register_nv(global: &mut HashMap<String, NanValue>, arena: &mut Arena) {
29    // `BranchPath.Root` is a nullary value constructor (like
30    // `Option.None`) — PascalCase, no parens. `.child` / `.parse`
31    // are methods that take arguments and stay lowercase with parens.
32    let root_value = make_path("", arena);
33    let mut members: Vec<(Rc<str>, NanValue)> = Vec::with_capacity(3);
34    members.push((Rc::from("Root"), root_value));
35    for method in &["child", "parse"] {
36        let idx = arena.push_builtin(&format!("{}.{}", TYPE_NAME, method));
37        members.push((Rc::from(*method), NanValue::new_builtin(idx)));
38    }
39    let ns_idx = arena.push(ArenaEntry::Namespace {
40        name: Rc::from(TYPE_NAME),
41        members,
42    });
43    global.insert(TYPE_NAME.to_string(), NanValue::new_namespace(ns_idx));
44}
45
46pub fn call_nv(
47    name: &str,
48    args: &[NanValue],
49    arena: &mut Arena,
50) -> Option<Result<NanValue, RuntimeError>> {
51    match name {
52        "BranchPath.child" => Some(child_nv(args, arena)),
53        "BranchPath.parse" => Some(parse_nv(args, arena)),
54        _ => None,
55    }
56}
57
58fn make_path(dewey: &str, arena: &mut Arena) -> NanValue {
59    let type_id = arena
60        .find_type_id(TYPE_NAME)
61        .expect("BranchPath type must be registered via register_service_types");
62    let dewey_value = NanValue::new_string_value(dewey, arena);
63    let record_idx = arena.push_record(type_id, vec![dewey_value]);
64    NanValue::new_record(record_idx)
65}
66
67fn read_dewey(value: NanValue, arena: &Arena, method: &str) -> Result<String, RuntimeError> {
68    if !value.is_record() {
69        return Err(RuntimeError::Error(format!(
70            "{}: argument must be a BranchPath, got non-record value",
71            method
72        )));
73    }
74    let (type_id, fields) = arena.get_record(value.arena_index());
75    let expected = arena
76        .find_type_id(TYPE_NAME)
77        .expect("BranchPath type must be registered");
78    if type_id != expected {
79        return Err(RuntimeError::Error(format!(
80            "{}: argument must be a BranchPath, got a different record type",
81            method
82        )));
83    }
84    let Some(field) = fields.first() else {
85        return Err(RuntimeError::Error(format!(
86            "{}: BranchPath record is missing its `{}` field",
87            method, FIELD
88        )));
89    };
90    if !field.is_string() {
91        return Err(RuntimeError::Error(format!(
92            "{}: BranchPath.{} must be a String",
93            method, FIELD
94        )));
95    }
96    Ok(arena.get_string_value(*field).to_string())
97}
98
99fn child_nv(args: &[NanValue], arena: &mut Arena) -> Result<NanValue, RuntimeError> {
100    if args.len() != 2 {
101        return Err(RuntimeError::Error(format!(
102            "BranchPath.child() takes 2 arguments (path, idx), got {}",
103            args.len()
104        )));
105    }
106    let parent = read_dewey(args[0], arena, "BranchPath.child")?;
107    if !args[1].is_int() {
108        return Err(RuntimeError::Error(
109            "BranchPath.child: `idx` must be an Int".to_string(),
110        ));
111    }
112    let idx = args[1].as_aver_int(arena);
113    let Some(idx) = idx.to_usize() else {
114        return Err(RuntimeError::Error(format!(
115            "BranchPath.child: `idx` must be a non-negative, machine-sized Int, got {}",
116            idx
117        )));
118    };
119    let dewey = if parent.is_empty() {
120        idx.to_string()
121    } else {
122        format!("{}.{}", parent, idx)
123    };
124    Ok(make_path(&dewey, arena))
125}
126
127fn parse_nv(args: &[NanValue], arena: &mut Arena) -> Result<NanValue, RuntimeError> {
128    if args.len() != 1 {
129        return Err(RuntimeError::Error(format!(
130            "BranchPath.parse() takes 1 argument, got {}",
131            args.len()
132        )));
133    }
134    if !args[0].is_string() {
135        return Err(RuntimeError::Error(
136            "BranchPath.parse: argument must be a String".to_string(),
137        ));
138    }
139    let raw = arena.get_string_value(args[0]).to_string();
140    if !is_valid_dewey(&raw) {
141        return Err(RuntimeError::Error(format!(
142            "BranchPath.parse: `{}` is not a valid dewey-decimal path (expected empty string for root or dot-separated non-negative integers like \"0\", \"2.0\")",
143            raw
144        )));
145    }
146    Ok(make_path(&raw, arena))
147}
148
149fn is_valid_dewey(s: &str) -> bool {
150    if s.is_empty() {
151        return true;
152    }
153    s.split('.')
154        .all(|seg| !seg.is_empty() && seg.bytes().all(|b| b.is_ascii_digit()))
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160
161    fn fresh_arena() -> Arena {
162        let mut arena = Arena::new();
163        crate::vm::register_service_types(&mut arena);
164        arena
165    }
166
167    #[test]
168    fn root_is_empty_dewey() {
169        let mut arena = fresh_arena();
170        let path = make_path("", &mut arena);
171        let dewey = read_dewey(path, &arena, "test").unwrap();
172        assert_eq!(dewey, "");
173    }
174
175    #[test]
176    fn child_of_root_is_index() {
177        let mut arena = fresh_arena();
178        let root = make_path("", &mut arena);
179        let idx = NanValue::new_int(3, &mut arena);
180        let child = child_nv(&[root, idx], &mut arena).unwrap();
181        let dewey = read_dewey(child, &arena, "test").unwrap();
182        assert_eq!(dewey, "3");
183    }
184
185    #[test]
186    fn nested_child_joins_with_dot() {
187        let mut arena = fresh_arena();
188        let root = make_path("", &mut arena);
189        let two = NanValue::new_int(2, &mut arena);
190        let branch2 = child_nv(&[root, two], &mut arena).unwrap();
191        let zero = NanValue::new_int(0, &mut arena);
192        let nested = child_nv(&[branch2, zero], &mut arena).unwrap();
193        let dewey = read_dewey(nested, &arena, "test").unwrap();
194        assert_eq!(dewey, "2.0");
195    }
196
197    #[test]
198    fn parse_accepts_valid_dewey() {
199        let mut arena = fresh_arena();
200        for &raw in &["", "0", "1", "2.0", "10.3.4"] {
201            let s = NanValue::new_string_value(raw, &mut arena);
202            let path = parse_nv(&[s], &mut arena).expect(raw);
203            let dewey = read_dewey(path, &arena, "test").unwrap();
204            assert_eq!(dewey, raw);
205        }
206    }
207
208    #[test]
209    fn parse_rejects_non_dewey() {
210        let mut arena = fresh_arena();
211        for &raw in &[".", "1.", ".1", "1..2", "abc", "1.a", "-1", " ", "1.2.3."] {
212            let s = NanValue::new_string_value(raw, &mut arena);
213            let result = parse_nv(&[s], &mut arena);
214            assert!(result.is_err(), "{} should be rejected", raw);
215        }
216    }
217
218    #[test]
219    fn child_rejects_negative_idx() {
220        let mut arena = fresh_arena();
221        let root = make_path("", &mut arena);
222        let neg = NanValue::new_int(-1, &mut arena);
223        let result = child_nv(&[root, neg], &mut arena);
224        assert!(result.is_err());
225    }
226
227    #[test]
228    fn child_rejects_non_path_first_arg() {
229        let mut arena = fresh_arena();
230        let garbage = NanValue::new_string_value("not a path", &mut arena);
231        let idx = NanValue::new_int(0, &mut arena);
232        let result = child_nv(&[garbage, idx], &mut arena);
233        assert!(result.is_err());
234    }
235}