Skip to main content

cairn_core/
render.rs

1//! The projection renderer: a stored tree → the Section 5 text.
2//!
3//! Read-only and total: there is no parser, and text is never the source of
4//! truth (`docs/design.md` Section 4). The renderer follows the Section 5
5//! conventions for the seed model's constructs:
6//!
7//! - `end`-delimited keyword blocks, no braces, no significant whitespace
8//! - the contract header (`given`/`produces`/`requires`/`on_failure`) before `do`
9//! - `pure` for an empty effect set
10//! - confidence as `Type @ level`, with the `structural` baseline elided
11//! - named call arguments, resolved from the enclosing module
12//!
13//! Full fidelity to every Section 5 example waits on operators, handlers, and
14//! record/variant definitions existing in the model; those are later slices.
15//! A hole renders as `<hole: …>` and a missing child as `<missing>` so an
16//! incomplete or broken tree still renders for review rather than failing.
17
18use crate::node::{Node, NodeHash};
19use crate::store::{Result, Store};
20use crate::ty::{Confidence, Effect, Type};
21use serde::Serialize;
22use std::cell::Cell;
23use std::collections::HashMap;
24
25// The addressable projection (design.md §8, v0.2 slice 1). The one
26// renderer stays canonical: when `MARK` is off (the default, every
27// existing path) `wrap` is the identity and `render()` output is
28// byte-identical. `render_addressed` flips `MARK` on, runs the *same*
29// renderer (no fork), and parses the node-bracketed string into the
30// span index. Sentinels are ASCII 0x01/0x02/0x03 — control bytes the
31// Section-5 projection never emits (string literals render via `{:?}`,
32// which escapes controls; identifiers/numbers/types/punctuation are
33// all printable), so they cannot collide with real output.
34const LB: char = '\u{1}'; // node start
35const SEP: char = '\u{2}'; // hash / body separator
36const RB: char = '\u{3}'; // node end
37
38thread_local! {
39    static MARK: Cell<bool> = const { Cell::new(false) };
40}
41
42/// Sets `MARK` for its lifetime and clears it on drop — so a panic in
43/// the renderer can never leave marking on for a later canonical
44/// `render()` (the same panic-safety the publish seam uses).
45struct MarkGuard;
46impl MarkGuard {
47    fn on() -> Self {
48        MARK.with(|m| m.set(true));
49        MarkGuard
50    }
51}
52impl Drop for MarkGuard {
53    fn drop(&mut self) {
54        MARK.with(|m| m.set(false));
55    }
56}
57
58/// Bracket `s` with `hash` iff marking is on; otherwise identity (so
59/// canonical `render()` is unchanged, byte for byte).
60fn wrap(hash: &NodeHash, s: String) -> String {
61    if MARK.with(Cell::get) {
62        format!("{LB}{hash}{SEP}{s}{RB}")
63    } else {
64        s
65    }
66}
67
68/// A node's byte range in the rendered `text`. Spans nest: a node's
69/// range strictly contains every child's; the root is `0..text.len()`.
70#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
71pub struct Span {
72    pub hash: NodeHash,
73    pub start: usize,
74    pub end: usize,
75}
76
77/// The Section-5 projection plus the span index that maps any byte
78/// position back to the node hash there — the editor substrate.
79#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
80pub struct Addressed {
81    pub text: String,
82    pub spans: Vec<Span>,
83}
84
85/// Render `root` and return the projection text together with the
86/// hash↔span index. `text` is byte-identical to [`render`]; the spans
87/// come from the *same* renderer run with node bracketing on.
88pub fn render_addressed(store: &Store, root: &NodeHash) -> Result<Addressed> {
89    let marked = {
90        let _g = MarkGuard::on();
91        render(store, root)?
92    };
93    let mut text = String::with_capacity(marked.len());
94    let mut stack: Vec<(NodeHash, usize)> = Vec::new();
95    let mut spans: Vec<Span> = Vec::new();
96    let mut chars = marked.chars();
97    while let Some(c) = chars.next() {
98        match c {
99            LB => {
100                let mut h = String::new();
101                for hc in chars.by_ref() {
102                    if hc == SEP {
103                        break;
104                    }
105                    h.push(hc);
106                }
107                stack.push((NodeHash::parse(&h), text.len()));
108            }
109            RB => {
110                if let Some((hash, start)) = stack.pop() {
111                    spans.push(Span {
112                        hash,
113                        start,
114                        end: text.len(),
115                    });
116                }
117            }
118            _ => text.push(c),
119        }
120    }
121    Ok(Addressed { text, spans })
122}
123
124/// The deepest (most specific) node whose span contains `offset` — the
125/// node a cursor at `offset` edits. `None` if out of range.
126pub fn node_at(a: &Addressed, offset: usize) -> Option<NodeHash> {
127    a.spans
128        .iter()
129        .filter(|s| offset >= s.start && offset < s.end)
130        .min_by_key(|s| s.end - s.start)
131        .map(|s| s.hash.clone())
132}
133
134/// Render the subtree at `root` as Section 5 text. No trailing newline.
135pub fn render(store: &Store, root: &NodeHash) -> Result<String> {
136    let params = param_names(store, root)?;
137    let Some(node) = store.get(root)? else {
138        return Ok("<missing>".to_string());
139    };
140    Ok(match node {
141        Node::Module {
142            name,
143            types,
144            functions,
145        } => {
146            let mut parts = vec![format!("module {name}"), String::new()];
147            for th in &types {
148                parts.push(render_typedef(store, th)?);
149                parts.push(String::new());
150            }
151            for fh in &functions {
152                parts.push(render_function(store, fh, &params)?);
153                parts.push(String::new());
154            }
155            parts.push("end".to_string());
156            wrap(root, parts.join("\n"))
157        }
158        Node::Function { .. } => render_function(store, root, &params)?,
159        Node::RecordDef { .. } | Node::VariantDef { .. } => render_typedef(store, root)?,
160        _ => expr(store, root, &params),
161    })
162}
163
164/// Render a type definition: `type Name = record … end` or
165/// `type Name = variant … end` (Section 5).
166fn render_typedef(store: &Store, hash: &NodeHash) -> Result<String> {
167    match store.get(hash)? {
168        Some(Node::RecordDef { name, fields }) => {
169            let mut out = vec![format!("type {name} = record")];
170            for (fname, fty) in &fields {
171                out.push(format!("  {fname}: {}", ty(fty)));
172            }
173            out.push("end".to_string());
174            Ok(wrap(hash, out.join("\n")))
175        }
176        Some(Node::VariantDef { name, cases }) => {
177            let mut out = vec![format!("type {name} = variant")];
178            for (cname, payload) in &cases {
179                if payload.is_empty() {
180                    out.push(format!("  {cname}"));
181                } else {
182                    let fs: Vec<String> = payload
183                        .iter()
184                        .map(|(fn_, ft)| format!("{fn_}: {}", ty(ft)))
185                        .collect();
186                    out.push(format!("  {cname}({})", fs.join(", ")));
187                }
188            }
189            out.push("end".to_string());
190            Ok(wrap(hash, out.join("\n")))
191        }
192        _ => Ok(wrap(hash, "<missing>".to_string())),
193    }
194}
195
196/// Map of function name → ordered parameter names, for named-argument
197/// rendering. Built from a module, or the lone function (so self-calls render
198/// named).
199fn param_names(store: &Store, root: &NodeHash) -> Result<HashMap<String, Vec<String>>> {
200    let mut map = HashMap::new();
201    let mut add = |n: &Node| {
202        if let Node::Function { name, params, .. } = n {
203            map.insert(
204                name.clone(),
205                params.iter().map(|p| p.name.clone()).collect(),
206            );
207        }
208    };
209    match store.get(root)? {
210        Some(Node::Module { functions, .. }) => {
211            for fh in &functions {
212                if let Some(f) = store.get(fh)? {
213                    add(&f);
214                }
215            }
216        }
217        Some(f @ Node::Function { .. }) => add(&f),
218        _ => {}
219    }
220    Ok(map)
221}
222
223fn render_function(
224    store: &Store,
225    fh: &NodeHash,
226    pmap: &HashMap<String, Vec<String>>,
227) -> Result<String> {
228    let Some(Node::Function {
229        name,
230        type_params,
231        params,
232        produces,
233        requires,
234        on_failure,
235        body,
236        result,
237    }) = store.get(fh)?
238    else {
239        return Ok(wrap(fh, "<missing>".to_string()));
240    };
241
242    let header = if type_params.is_empty() {
243        format!("function {name}")
244    } else {
245        format!("function {name}<{}>", type_params.join(", "))
246    };
247    let mut out: Vec<String> = vec![header];
248
249    if !params.is_empty() {
250        out.push("  given".to_string());
251        for p in &params {
252            out.push(format!(
253                "    {}: {}{}",
254                p.name,
255                ty(&p.ty),
256                conf_suffix(p.min_confidence)
257            ));
258        }
259    }
260
261    out.push(format!(
262        "  produces {}{}",
263        ty(&produces.ty),
264        conf_suffix(produces.confidence)
265    ));
266
267    if requires.is_empty() {
268        out.push("  pure".to_string());
269    } else {
270        let names: Vec<String> = requires.iter().map(|e| effect(*e).to_string()).collect();
271        out.push(format!("  requires {}", names.join(", ")));
272    }
273
274    if !on_failure.is_empty() {
275        out.push("  on_failure".to_string());
276        for f in &on_failure {
277            out.push(format!("    {f}"));
278        }
279    }
280
281    out.push("do".to_string());
282    for step_hash in &body {
283        match store.get(step_hash)? {
284            Some(Node::Step { binding, value }) => match store.get(&value)? {
285                // Section 5 form: the step, then its `on V -> ...` handlers
286                // on indented lines.
287                Some(Node::Handle { body, handlers }) => {
288                    out.push(format!(
289                        "  step {} = {}",
290                        binding,
291                        expr(store, &body, pmap)
292                    ));
293                    for (v, r) in &handlers {
294                        out.push(format!("    on {} -> {}", v, expr(store, r, pmap)));
295                    }
296                }
297                _ => {
298                    out.push(format!(
299                        "  step {} = {}",
300                        binding,
301                        expr(store, &value, pmap)
302                    ));
303                }
304            },
305            Some(Node::Hole { expects }) => {
306                out.push(format!("  <hole: {expects}>"));
307            }
308            _ => out.push("  <missing>".to_string()),
309        }
310    }
311    out.push(format!("  yield {}", expr(store, &result, pmap)));
312    out.push("end".to_string());
313    Ok(wrap(fh, out.join("\n")))
314}
315
316/// Wraps the body render with the node's hash when marking is on
317/// (identity otherwise). Every recursive `expr` call routes through
318/// here, so nesting is automatic and exact.
319fn expr(store: &Store, hash: &NodeHash, pmap: &HashMap<String, Vec<String>>) -> String {
320    wrap(hash, expr_body(store, hash, pmap))
321}
322
323fn expr_body(store: &Store, hash: &NodeHash, pmap: &HashMap<String, Vec<String>>) -> String {
324    match store.get(hash) {
325        Ok(Some(Node::Lit(v))) => v.to_string(),
326        Ok(Some(Node::FloatLit(bits))) => format!("{}f", f64::from_bits(bits)),
327        Ok(Some(Node::FloatOp { op, lhs, rhs })) => format!(
328            "{} {} {}",
329            operand(store, &lhs, pmap),
330            op.symbol(),
331            operand(store, &rhs, pmap)
332        ),
333        Ok(Some(Node::IntToFloat(a))) => {
334            format!("to_float({})", expr(store, &a, pmap))
335        }
336        Ok(Some(Node::FloatToInt(a))) => {
337            format!("to_int({})", expr(store, &a, pmap))
338        }
339        Ok(Some(Node::DecimalLit(v))) => {
340            format!("{}.{:04}d", v / 10000, (v % 10000).abs())
341        }
342        Ok(Some(Node::DecimalOp { op, lhs, rhs })) => format!(
343            "{} {} {}",
344            operand(store, &lhs, pmap),
345            op.symbol(),
346            operand(store, &rhs, pmap)
347        ),
348        Ok(Some(Node::IntToDecimal(a))) => {
349            format!("to_decimal({})", expr(store, &a, pmap))
350        }
351        Ok(Some(Node::DecimalToInt(a))) => {
352            format!("decimal_to_int({})", expr(store, &a, pmap))
353        }
354        Ok(Some(Node::DecimalRaw(a))) => {
355            format!("decimal_raw({})", expr(store, &a, pmap))
356        }
357        Ok(Some(Node::Bool(b))) => b.to_string(),
358        Ok(Some(Node::Not(a))) => format!("!{}", operand(store, &a, pmap)),
359        Ok(Some(Node::Str(s))) => format!("{s:?}"),
360        Ok(Some(Node::Now)) => "now()".to_string(),
361        Ok(Some(Node::List(es))) => {
362            let parts: Vec<String> =
363                es.iter().map(|e| expr(store, e, pmap)).collect();
364            format!("[{}]", parts.join(", "))
365        }
366        Ok(Some(Node::ListEmpty { elem })) => {
367            format!("list_empty<{}>()", ty(&elem))
368        }
369        Ok(Some(Node::ListCons { head, tail })) => format!(
370            "cons({}, {})",
371            expr(store, &head, pmap),
372            expr(store, &tail, pmap)
373        ),
374        Ok(Some(Node::OptionSome(v))) => {
375            format!("some({})", expr(store, &v, pmap))
376        }
377        Ok(Some(Node::OptionNone { elem })) => {
378            format!("none<{}>()", ty(&elem))
379        }
380        Ok(Some(Node::OptionElse { opt, default })) => format!(
381            "{} else {}",
382            expr(store, &opt, pmap),
383            expr(store, &default, pmap)
384        ),
385        Ok(Some(Node::OptionMatch {
386            opt,
387            some_bind,
388            some_body,
389            none_body,
390        })) => format!(
391            "match {} {{ Some({some_bind}) -> {}, None -> {} }}",
392            expr(store, &opt, pmap),
393            expr(store, &some_body, pmap),
394            expr(store, &none_body, pmap)
395        ),
396        Ok(Some(Node::ListTryGet { list, index })) => format!(
397            "list_try_get({}, {})",
398            expr(store, &list, pmap),
399            expr(store, &index, pmap)
400        ),
401        Ok(Some(Node::ListLen(a))) => {
402            format!("list_len({})", expr(store, &a, pmap))
403        }
404        Ok(Some(Node::ListGet { list, index })) => format!(
405            "list_get({}, {})",
406            expr(store, &list, pmap),
407            expr(store, &index, pmap)
408        ),
409        Ok(Some(Node::Map(pairs))) => {
410            let parts: Vec<String> = pairs
411                .iter()
412                .map(|(k, v)| {
413                    format!("{}: {}", expr(store, k, pmap), expr(store, v, pmap))
414                })
415                .collect();
416            format!("{{{}}}", parts.join(", "))
417        }
418        Ok(Some(Node::MapGet { map, key })) => format!(
419            "map_get({}, {})",
420            expr(store, &map, pmap),
421            expr(store, &key, pmap)
422        ),
423        Ok(Some(Node::MapTryGet { map, key })) => format!(
424            "map_try_get({}, {})",
425            expr(store, &map, pmap),
426            expr(store, &key, pmap)
427        ),
428        Ok(Some(Node::MapLen(a))) => format!("map_len({})", expr(store, &a, pmap)),
429        Ok(Some(Node::Log(a))) => format!("log({})", expr(store, &a, pmap)),
430        Ok(Some(Node::Publish(a))) => {
431            format!("publish({})", expr(store, &a, pmap))
432        }
433        Ok(Some(Node::SetHeader { name, value })) => format!(
434            "set_header({}, {})",
435            expr(store, &name, pmap),
436            expr(store, &value, pmap)
437        ),
438        Ok(Some(Node::Rand)) => "rand()".to_string(),
439        Ok(Some(Node::MutNew(v))) => format!("cell({})", expr(store, &v, pmap)),
440        Ok(Some(Node::MutGet(cl))) => {
441            format!("cell_get({})", expr(store, &cl, pmap))
442        }
443        Ok(Some(Node::MutSet { cell, value })) => format!(
444            "cell_set({}, {})",
445            expr(store, &cell, pmap),
446            expr(store, &value, pmap)
447        ),
448        Ok(Some(Node::DiskWrite { path, content })) => format!(
449            "disk_write({}, {})",
450            expr(store, &path, pmap),
451            expr(store, &content, pmap)
452        ),
453        Ok(Some(Node::DiskRead(p))) => {
454            format!("disk_read({})", expr(store, &p, pmap))
455        }
456        Ok(Some(Node::NetGet(u))) => {
457            format!("net_get({})", expr(store, &u, pmap))
458        }
459        Ok(Some(Node::DbQuery { sql, params })) => format!(
460            "db_query({}, {})",
461            expr(store, &sql, pmap),
462            expr(store, &params, pmap)
463        ),
464        Ok(Some(Node::StrLen(a))) => {
465            format!("str_len({})", expr(store, &a, pmap))
466        }
467        Ok(Some(Node::StrLower(a))) => {
468            format!("str_lower({})", expr(store, &a, pmap))
469        }
470        Ok(Some(Node::StrFromCode(a))) => {
471            format!("str_from_code({})", expr(store, &a, pmap))
472        }
473        Ok(Some(Node::NumberToStr(a))) => {
474            format!("number_to_str({})", expr(store, &a, pmap))
475        }
476        Ok(Some(Node::StrToNumber(a))) => {
477            format!("str_to_number({})", expr(store, &a, pmap))
478        }
479        Ok(Some(Node::StrToNumberOpt(a))) => {
480            format!("str_to_number_opt({})", expr(store, &a, pmap))
481        }
482        Ok(Some(Node::StrConcat(a, b))) => format!(
483            "str_concat({}, {})",
484            expr(store, &a, pmap),
485            expr(store, &b, pmap)
486        ),
487        Ok(Some(Node::StrSlice { s, start, len })) => format!(
488            "str_slice({}, {}, {})",
489            expr(store, &s, pmap),
490            expr(store, &start, pmap),
491            expr(store, &len, pmap)
492        ),
493        Ok(Some(Node::StrEq(a, b))) => format!(
494            "str_eq({}, {})",
495            expr(store, &a, pmap),
496            expr(store, &b, pmap)
497        ),
498        Ok(Some(Node::StrContains { haystack, needle })) => format!(
499            "str_contains({}, {})",
500            expr(store, &haystack, pmap),
501            expr(store, &needle, pmap)
502        ),
503        Ok(Some(Node::StrStartsWith { s, prefix })) => format!(
504            "str_starts_with({}, {})",
505            expr(store, &s, pmap),
506            expr(store, &prefix, pmap)
507        ),
508        Ok(Some(Node::StrIndexOf { haystack, needle })) => format!(
509            "str_index_of({}, {})",
510            expr(store, &haystack, pmap),
511            expr(store, &needle, pmap)
512        ),
513        Ok(Some(Node::Ref(name))) => name,
514        Ok(Some(Node::Hole { expects })) => format!("<hole: {expects}>"),
515        Ok(Some(Node::Step { value, .. })) => expr(store, &value, pmap),
516        Ok(Some(Node::BinOp { op, lhs, rhs })) => {
517            format!(
518                "{} {} {}",
519                operand(store, &lhs, pmap),
520                op.symbol(),
521                operand(store, &rhs, pmap)
522            )
523        }
524        Ok(Some(Node::If {
525            cond,
526            then_branch,
527            else_branch,
528        })) => format!(
529            "if {} then {} else {} end",
530            expr(store, &cond, pmap),
531            expr(store, &then_branch, pmap),
532            expr(store, &else_branch, pmap)
533        ),
534        Ok(Some(Node::Fail(v))) => format!("fail {v}"),
535        Ok(Some(Node::Handle { body, handlers })) => {
536            let mut s = expr(store, &body, pmap);
537            for (v, r) in &handlers {
538                s.push_str(&format!(" on {v} -> {}", expr(store, r, pmap)));
539            }
540            s
541        }
542        Ok(Some(Node::Call { func, args })) => {
543            let rendered: Vec<String> =
544                args.iter().map(|a| expr(store, a, pmap)).collect();
545            match pmap.get(&func) {
546                Some(names) if names.len() == rendered.len() => {
547                    let pairs: Vec<String> = names
548                        .iter()
549                        .zip(&rendered)
550                        .map(|(n, a)| format!("{n}: {a}"))
551                        .collect();
552                    format!("{func}({})", pairs.join(", "))
553                }
554                _ => format!("{func}({})", rendered.join(", ")),
555            }
556        }
557        Ok(Some(Node::Lambda { params, body })) => {
558            let ps: Vec<String> = params.iter().map(|p| p.name.clone()).collect();
559            format!("|{}| {}", ps.join(", "), expr(store, &body, pmap))
560        }
561        Ok(Some(Node::FuncRef(name))) => format!("&{name}"),
562        Ok(Some(Node::CallValue { callee, args })) => {
563            let rendered: Vec<String> =
564                args.iter().map(|a| expr(store, a, pmap)).collect();
565            format!(
566                "{}({})",
567                operand(store, &callee, pmap),
568                rendered.join(", ")
569            )
570        }
571        Ok(Some(Node::Record { type_name, fields })) => {
572            let parts: Vec<String> = fields
573                .iter()
574                .map(|(n, h)| format!("{n}: {}", expr(store, h, pmap)))
575                .collect();
576            format!("{type_name} {{ {} }}", parts.join(", "))
577        }
578        Ok(Some(Node::Field { base, field, .. })) => {
579            format!("{}.{field}", operand(store, &base, pmap))
580        }
581        Ok(Some(Node::Variant {
582            type_name,
583            case,
584            fields,
585        })) => {
586            if fields.is_empty() {
587                format!("{type_name}.{case}")
588            } else {
589                let parts: Vec<String> = fields
590                    .iter()
591                    .map(|(n, h)| format!("{n}: {}", expr(store, h, pmap)))
592                    .collect();
593                format!("{type_name}.{case} {{ {} }}", parts.join(", "))
594            }
595        }
596        Ok(Some(Node::Match {
597            scrutinee, arms, ..
598        })) => {
599            let parts: Vec<String> = arms
600                .iter()
601                .map(|a| {
602                    let binds = if a.bindings.is_empty() {
603                        String::new()
604                    } else {
605                        format!("({})", a.bindings.join(", "))
606                    };
607                    format!(
608                        "{}{} -> {}",
609                        a.case,
610                        binds,
611                        expr(store, &a.body, pmap)
612                    )
613                })
614                .collect();
615            format!(
616                "match {} {{ {} }}",
617                expr(store, &scrutinee, pmap),
618                parts.join(", ")
619            )
620        }
621        Ok(Some(Node::Function { .. }))
622        | Ok(Some(Node::Module { .. }))
623        | Ok(Some(Node::RecordDef { .. }))
624        | Ok(Some(Node::VariantDef { .. })) => "<nested>".to_string(),
625        Ok(None) => "<missing>".to_string(),
626        Err(_) => "<error>".to_string(),
627    }
628}
629
630/// Render an operand, parenthesizing it only when it is itself a binary op,
631/// so nesting is unambiguous without a precedence model.
632fn operand(store: &Store, hash: &NodeHash, pmap: &HashMap<String, Vec<String>>) -> String {
633    let inner = expr(store, hash, pmap);
634    if matches!(
635        store.get(hash),
636        Ok(Some(Node::BinOp { .. }))
637            | Ok(Some(Node::FloatOp { .. }))
638            | Ok(Some(Node::DecimalOp { .. }))
639            | Ok(Some(Node::Not(_)))
640            // A function value used as a callee or operand must be
641            // parenthesized, or `&helper(n)` / `|x| x*2(n)` read
642            // ambiguously — found by reviewing a real v0.4 projection.
643            | Ok(Some(Node::FuncRef(_)))
644            | Ok(Some(Node::Lambda { .. }))
645            | Ok(Some(Node::CallValue { .. }))
646    ) {
647        format!("({inner})")
648    } else {
649        inner
650    }
651}
652
653fn ty(t: &Type) -> String {
654    match t {
655        Type::Number => "Number".into(),
656        Type::Float => "Float".into(),
657        Type::Decimal => "Decimal".into(),
658        Type::String => "String".into(),
659        Type::Bool => "Bool".into(),
660        Type::Bytes => "Bytes".into(),
661        Type::List(e) => format!("List<{}>", ty(e)),
662        Type::Map(k, v) => format!("Map<{}, {}>", ty(k), ty(v)),
663        Type::Option(e) => format!("Option<{}>", ty(e)),
664        Type::Result(o, e) => format!("Result<{}, {}>", ty(o), ty(e)),
665        Type::Named(n) => n.clone(),
666        Type::Cell(e) => format!("Cell<{}>", ty(e)),
667        Type::Fn { params, ret, .. } => {
668            let ps: Vec<String> = params.iter().map(ty).collect();
669            format!("Fn({}) -> {}", ps.join(", "), ty(ret))
670        }
671        Type::Var(v) => v.clone(),
672        Type::Never => "Never".into(),
673    }
674}
675
676/// Section 5: the `structural` baseline is elided; the others always render.
677fn conf_suffix(c: Confidence) -> &'static str {
678    match c {
679        Confidence::Structural => "",
680        Confidence::External => " @ external",
681        Confidence::Validated => " @ validated",
682        Confidence::Persisted => " @ persisted",
683    }
684}
685
686fn effect(e: Effect) -> &'static str {
687    match e {
688        Effect::Net => "Net",
689        Effect::Disk => "Disk",
690        Effect::Db => "Db",
691        Effect::Mut => "Mut",
692        Effect::Time => "Time",
693        Effect::Rand => "Rand",
694        Effect::Log => "Log",
695        Effect::Live => "Live",
696        Effect::Resp => "Resp",
697    }
698}
699
700#[cfg(test)]
701mod tests {
702    use super::*;
703    use crate::node::{Param, Produces};
704    use std::collections::BTreeSet;
705
706    // A test fixture mirroring the full function contract — arg count is
707    // inherent to what it builds, not a smell.
708    #[allow(clippy::too_many_arguments)]
709    fn f(
710        s: &Store,
711        name: &str,
712        params: Vec<Param>,
713        prod: Produces,
714        requires: BTreeSet<Effect>,
715        on_failure: Vec<&str>,
716        body: Vec<NodeHash>,
717        result: NodeHash,
718    ) -> NodeHash {
719        s.put(&Node::Function {
720            name: name.into(),
721            type_params: vec![],
722            params,
723            produces: prod,
724            requires,
725            on_failure: on_failure.into_iter().map(String::from).collect(),
726            body,
727            result,
728        })
729        .unwrap()
730    }
731
732    fn p(name: &str, ty: Type, c: Confidence) -> Param {
733        Param {
734            name: name.into(),
735            ty,
736            min_confidence: c,
737        }
738    }
739
740    #[test]
741    fn pure_constant_function() {
742        let s = Store::open_in_memory().unwrap();
743        let lit = s.put(&Node::Lit(42)).unwrap();
744        let answer = f(
745            &s,
746            "answer",
747            vec![],
748            Produces {
749                ty: Type::Number,
750                confidence: Confidence::Structural,
751            },
752            BTreeSet::new(),
753            vec![],
754            vec![],
755            lit,
756        );
757        let expected = "\
758function answer
759  produces Number
760  pure
761do
762  yield 42
763end";
764        assert_eq!(render(&s, &answer).unwrap(), expected);
765    }
766
767    #[test]
768    fn param_with_non_baseline_confidence_renders_at_suffix() {
769        let s = Store::open_in_memory().unwrap();
770        let nref = s.put(&Node::Ref("n".into())).unwrap();
771        let id = f(
772            &s,
773            "id",
774            vec![p("n", Type::Number, Confidence::External)],
775            Produces {
776                ty: Type::Number,
777                confidence: Confidence::Structural,
778            },
779            BTreeSet::new(),
780            vec![],
781            vec![],
782            nref,
783        );
784        let expected = "\
785function id
786  given
787    n: Number @ external
788  produces Number
789  pure
790do
791  yield n
792end";
793        assert_eq!(render(&s, &id).unwrap(), expected);
794    }
795
796    #[test]
797    fn effects_and_failures_render_in_the_header() {
798        let s = Store::open_in_memory().unwrap();
799        let zero = s.put(&Node::Lit(0)).unwrap();
800        let mut req = BTreeSet::new();
801        req.insert(Effect::Db);
802        req.insert(Effect::Net);
803        let risky = f(
804            &s,
805            "risky",
806            vec![],
807            Produces {
808                ty: Type::Number,
809                confidence: Confidence::Structural,
810            },
811            req,
812            vec!["Boom"],
813            vec![],
814            zero,
815        );
816        // BTreeSet order follows the Effect declaration order: Net before Db.
817        let expected = "\
818function risky
819  produces Number
820  requires Net, Db
821  on_failure
822    Boom
823do
824  yield 0
825end";
826        assert_eq!(render(&s, &risky).unwrap(), expected);
827    }
828
829    #[test]
830    fn calls_render_with_named_arguments_from_the_module() {
831        let s = Store::open_in_memory().unwrap();
832        let nref = s.put(&Node::Ref("n".into())).unwrap();
833        let id = f(
834            &s,
835            "id",
836            vec![p("n", Type::Number, Confidence::Structural)],
837            Produces {
838                ty: Type::Number,
839                confidence: Confidence::Structural,
840            },
841            BTreeSet::new(),
842            vec![],
843            vec![],
844            nref,
845        );
846        let seven = s.put(&Node::Lit(7)).unwrap();
847        let call = s
848            .put(&Node::Call {
849                func: "id".into(),
850                args: vec![seven],
851            })
852            .unwrap();
853        let step = s
854            .put(&Node::Step {
855                binding: "x".into(),
856                value: call,
857            })
858            .unwrap();
859        let xref = s.put(&Node::Ref("x".into())).unwrap();
860        let use_id = f(
861            &s,
862            "use_id",
863            vec![],
864            Produces {
865                ty: Type::Number,
866                confidence: Confidence::Structural,
867            },
868            BTreeSet::new(),
869            vec![],
870            vec![step],
871            xref,
872        );
873        let m = s
874            .put(&Node::Module {
875                name: "m".into(),
876                types: vec![],
877                functions: vec![id, use_id],
878            })
879            .unwrap();
880        let out = render(&s, &m).unwrap();
881        assert!(out.contains("module m"));
882        assert!(out.contains("step x = id(n: 7)"), "got:\n{out}");
883        assert!(out.trim_end().ends_with("end"));
884    }
885
886    #[test]
887    fn a_hole_renders_as_a_hole_marker() {
888        let s = Store::open_in_memory().unwrap();
889        let hole = s
890            .put(&Node::Hole {
891                expects: "Number".into(),
892            })
893            .unwrap();
894        let g = f(
895            &s,
896            "g",
897            vec![],
898            Produces {
899                ty: Type::Number,
900                confidence: Confidence::Structural,
901            },
902            BTreeSet::new(),
903            vec![],
904            vec![],
905            hole,
906        );
907        assert!(render(&s, &g).unwrap().contains("yield <hole: Number>"));
908    }
909
910    #[test]
911    fn a_record_def_and_field_access_render() {
912        let s = Store::open_in_memory().unwrap();
913        let pd = s
914            .put(&Node::RecordDef {
915                name: "Point".into(),
916                fields: vec![("x".into(), Type::Number)],
917            })
918            .unwrap();
919        let pref = s.put(&Node::Ref("pt".into())).unwrap();
920        let fx = s
921            .put(&Node::Field {
922                base: pref,
923                type_name: "Point".into(),
924                field: "x".into(),
925            })
926            .unwrap();
927        let get = f(
928            &s,
929            "get",
930            vec![p("pt", Type::Named("Point".into()), Confidence::Structural)],
931            Produces {
932                ty: Type::Number,
933                confidence: Confidence::Structural,
934            },
935            BTreeSet::new(),
936            vec![],
937            vec![],
938            fx,
939        );
940        let m = s
941            .put(&Node::Module {
942                name: "m".into(),
943                types: vec![pd],
944                functions: vec![get],
945            })
946            .unwrap();
947        let out = render(&s, &m).unwrap();
948        assert!(out.contains("type Point = record"), "got:\n{out}");
949        assert!(out.contains("  x: Number"));
950        assert!(out.contains("yield pt.x"));
951    }
952
953    #[test]
954    fn a_generic_function_header_renders() {
955        let s = Store::open_in_memory().unwrap();
956        let x = s.put(&Node::Ref("x".into())).unwrap();
957        let id = s
958            .put(&Node::Function {
959                name: "identity".into(),
960                type_params: vec!["T".into()],
961                params: vec![Param {
962                    name: "x".into(),
963                    ty: Type::Var("T".into()),
964                    min_confidence: Confidence::Structural,
965                }],
966                produces: Produces {
967                    ty: Type::Var("T".into()),
968                    confidence: Confidence::Structural,
969                },
970                requires: BTreeSet::new(),
971                on_failure: vec![],
972                body: vec![],
973                result: x,
974            })
975            .unwrap();
976        let out = render(&s, &id).unwrap();
977        assert!(out.contains("function identity<T>"), "got:\n{out}");
978        assert!(out.contains("x: T"));
979    }
980}
981
982#[cfg(test)]
983mod v04_review {
984    use super::*;
985    use crate::node::{BinOp, Node, Param, Produces};
986    use crate::store::Store;
987    use crate::ty::{Confidence, Type};
988
989    /// Found by reading a real v0.4 projection as a reviewer: a function
990    /// value used as a callee rendered ambiguously (`&helper(n)`,
991    /// `|x| x * 2(n)`). This locks the fix — the human-review half of the
992    /// thesis is only worth anything if the projection is unambiguous on
993    /// exactly the keystone constructs.
994    #[test]
995    fn v04_callee_projection_is_unambiguous() {
996        let s = Store::open_in_memory().unwrap();
997        let p = |v: i64| s.put(&Node::Lit(v)).unwrap();
998        let r = |n: &str| s.put(&Node::Ref(n.into())).unwrap();
999        let par = |n: &str, t: Type| Param { name: n.into(), ty: t, min_confidence: Confidence::External };
1000
1001        // step dbl  = |x| x * 2
1002        let dbl = s.put(&Node::Lambda {
1003            params: vec![par("x", Type::Number)],
1004            body: s.put(&Node::BinOp { op: BinOp::Mul, lhs: r("x"), rhs: p(2) }).unwrap(),
1005        }).unwrap();
1006        // step g    = &helper        (FuncRef)
1007        let g = s.put(&Node::FuncRef("helper".into())).unwrap();
1008        // step applied = g(n)        (CallValue on a FuncRef)
1009        let applied = s.put(&Node::CallValue { callee: g.clone(), args: vec![r("n")] }).unwrap();
1010        // step inline  = (|x| x*2)(n)  (CallValue on a Lambda)
1011        let inline = s.put(&Node::CallValue { callee: dbl.clone(), args: vec![r("n")] }).unwrap();
1012        // step picked  = match opt { Some(v) -> v + 1, None -> 0 }
1013        let opt = s.put(&Node::OptionSome(r("n"))).unwrap();
1014        let picked = s.put(&Node::OptionMatch {
1015            opt,
1016            some_bind: "v".into(),
1017            some_body: s.put(&Node::BinOp { op: BinOp::Add, lhs: r("v"), rhs: p(1) }).unwrap(),
1018            none_body: p(0),
1019        }).unwrap();
1020        // step money  = 19.99 + 0.01   (Decimal)
1021        let money = s.put(&Node::DecimalOp {
1022            op: BinOp::Add,
1023            lhs: s.put(&Node::DecimalLit(199900)).unwrap(),
1024            rhs: s.put(&Node::DecimalLit(100)).unwrap(),
1025        }).unwrap();
1026        // step ratio  = 1.5f * to_float(n)   (Float)
1027        let ratio = s.put(&Node::FloatOp {
1028            op: BinOp::Mul,
1029            lhs: s.put(&Node::FloatLit(1.5f64.to_bits())).unwrap(),
1030            rhs: s.put(&Node::IntToFloat(r("n"))).unwrap(),
1031        }).unwrap();
1032        // yield = applied + inline + picked + decimal_to_int(money) + to_int(ratio) + !(n == 0 && n < 0 || n >= 1)
1033        let logic = s.put(&Node::BinOp {
1034            op: BinOp::Or,
1035            lhs: s.put(&Node::BinOp {
1036                op: BinOp::And,
1037                lhs: s.put(&Node::BinOp { op: BinOp::Eq, lhs: r("n"), rhs: p(0) }).unwrap(),
1038                rhs: s.put(&Node::BinOp { op: BinOp::Lt, lhs: r("n"), rhs: p(0) }).unwrap(),
1039            }).unwrap(),
1040            rhs: s.put(&Node::BinOp { op: BinOp::Ge, lhs: r("n"), rhs: p(1) }).unwrap(),
1041        }).unwrap();
1042        let notlogic = s.put(&Node::Not(logic)).unwrap();
1043        let body = vec![
1044            s.put(&Node::Step { binding: "applied".into(), value: applied }).unwrap(),
1045            s.put(&Node::Step { binding: "inline".into(), value: inline }).unwrap(),
1046            s.put(&Node::Step { binding: "picked".into(), value: picked }).unwrap(),
1047            s.put(&Node::Step { binding: "money".into(), value: money }).unwrap(),
1048            s.put(&Node::Step { binding: "ratio".into(), value: ratio }).unwrap(),
1049        ];
1050        let yield_ = s.put(&Node::BinOp {
1051            op: BinOp::Add,
1052            lhs: r("applied"),
1053            rhs: s.put(&Node::BinOp {
1054                op: BinOp::Add,
1055                lhs: r("picked"),
1056                rhs: s.put(&Node::DecimalToInt(r("money"))).unwrap(),
1057            }).unwrap(),
1058        }).unwrap();
1059        let f = s.put(&Node::Function {
1060            name: "demo".into(),
1061            type_params: vec![],
1062            params: vec![par("n", Type::Number)],
1063            produces: Produces { ty: Type::Number, confidence: Confidence::External },
1064            requires: Default::default(),
1065            on_failure: vec![],
1066            body,
1067            result: s.put(&Node::BinOp { op: BinOp::Add, lhs: yield_, rhs: notlogic }).unwrap(),
1068        }).unwrap();
1069        let out = render(&s, &f).unwrap();
1070        // A FuncRef callee and a Lambda callee must be parenthesized so
1071        // application is unambiguous.
1072        assert!(
1073            out.contains("step applied = (&helper)(n)"),
1074            "FuncRef callee must render as `(&helper)(n)`, not `&helper(n)`:\n{out}"
1075        );
1076        assert!(
1077            out.contains("step inline = (|x| x * 2)(n)"),
1078            "Lambda callee must render parenthesized so the body is \
1079             delimited, not `|x| x * 2(n)`:\n{out}"
1080        );
1081        // The old ambiguous forms must not reappear.
1082        assert!(!out.contains("&helper(n)") || out.contains("(&helper)(n)"));
1083        assert!(!out.contains("step inline = |x|"));
1084        // OptionMatch reads as a match with bound payload.
1085        assert!(out.contains("match some(n) { Some(v) -> v + 1, None -> 0 }"));
1086        // Decimal/Float literals carry an unambiguous suffix.
1087        assert!(out.contains("19.9900d + 0.0100d") && out.contains("1.5f"));
1088    }
1089
1090    /// The addressable projection (v0.2 slice 1): `render_addressed`
1091    /// is byte-identical to `render`, spans nest, and `node_at` maps a
1092    /// cursor to the deepest (most specific) editable node.
1093    #[test]
1094    fn render_addressed_is_byte_identical_and_addressable() {
1095        let s = Store::open_in_memory().unwrap();
1096        let two = s.put(&Node::Lit(2)).unwrap();
1097        let three = s.put(&Node::Lit(3)).unwrap();
1098        let add = s
1099            .put(&Node::BinOp {
1100                op: BinOp::Add,
1101                lhs: two.clone(),
1102                rhs: three.clone(),
1103            })
1104            .unwrap();
1105        let g = s
1106            .put(&Node::Function {
1107                name: "g".into(),
1108                type_params: vec![],
1109                params: vec![],
1110                produces: Produces {
1111                    ty: Type::Number,
1112                    confidence: Confidence::Structural,
1113                },
1114                requires: std::collections::BTreeSet::new(),
1115                on_failure: vec![],
1116                body: vec![],
1117                result: add.clone(),
1118            })
1119            .unwrap();
1120
1121        let canon = render(&s, &g).unwrap();
1122        let a = render_addressed(&s, &g).unwrap();
1123
1124        // The one canonical form: addressed text == render(), byte for
1125        // byte (no sentinel leaks).
1126        assert_eq!(a.text, canon);
1127        assert!(!a.text.contains(['\u{1}', '\u{2}', '\u{3}']));
1128
1129        // The function wrapper is the root span: it covers everything.
1130        let root = a.spans.iter().find(|x| x.hash == g).unwrap();
1131        assert_eq!((root.start, root.end), (0, a.text.len()));
1132
1133        // Every span is well-formed and within the root.
1134        for sp in &a.spans {
1135            assert!(sp.start <= sp.end && sp.end <= a.text.len());
1136            assert!(root.start <= sp.start && sp.end <= root.end);
1137        }
1138
1139        // The `2 + 3` body: each operand is its own node, and the `+`
1140        // is in the BinOp but in neither operand → BinOp is deepest.
1141        let i2 = a.text.find("2 + 3").unwrap();
1142        let ip = a.text.find('+').unwrap();
1143        let i3 = a.text.find("3\n").unwrap(); // the operand `3`
1144        assert_eq!(node_at(&a, i2), Some(two.clone()));
1145        assert_eq!(node_at(&a, i3), Some(three.clone()));
1146        assert_eq!(node_at(&a, ip), Some(add.clone()));
1147
1148        // Cursor at the very start is in the outermost node only;
1149        // past the end addresses nothing (end is exclusive).
1150        assert_eq!(node_at(&a, 0), Some(g.clone()));
1151        assert_eq!(node_at(&a, a.text.len()), None);
1152
1153        // Strict nesting: Lit ⊂ BinOp ⊂ Function.
1154        let sp = |h: &NodeHash| a.spans.iter().find(|x| &x.hash == h).unwrap();
1155        let (s2, sa) = (sp(&two), sp(&add));
1156        assert!(sa.start <= s2.start && s2.end <= sa.end);
1157        assert!(sa.end - sa.start > s2.end - s2.start);
1158        assert!(root.start <= sa.start && sa.end <= root.end);
1159    }
1160}