Skip to content
1 change: 1 addition & 0 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,7 @@ Called like functions, compiled to dedicated opcodes.
| `flt fn xs` | keep elements where `fn x` is true | `L a` |
| `fld fn xs init` | left fold: `fn (fn (fn init x0) x1) ...` | accumulator |
| `flatmap fn xs` | map then flatten one level | `L b` |
| `mapr fn xs` | map with short-circuit Result propagation: collects Ok values, returns first Err | `R (L b) e` |
| `partition fn xs` | split list into `[passing, failing]` by predicate | `L (L a)` |
| `chunks n xs` | non-overlapping chunks of size `n` (final chunk may be shorter) | `L (L a)` |
| `window n xs` | sliding windows of size `n` (drops trailing partial; empty if n > len) | `L (L a)` |
Expand Down
2 changes: 1 addition & 1 deletion ai.txt

Large diffs are not rendered by default.

43 changes: 43 additions & 0 deletions examples/mapr.ilo
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
-- mapr fn xs : R (L b) e — short-circuiting Result-aware map.
--
-- For each item, fn must return R b e. Encounter an Ok value (~v) and
-- mapr accumulates v; encounter an Err value (^e) and the whole call
-- returns ^e immediately, skipping the rest of the list. Pair with `!`
-- to thread the err up into a Result-returning caller without per-item
-- match boilerplate.
--
-- Before mapr, the persona idiom was:
-- ton s:t>n;r=num s;?r{~v:v;^_:0} -- swallow-err helper, ~30 tokens
-- ns=map ton xs -- and the error is lost
-- mapr replaces both with three tokens and preserves the err shape:
-- ns=mapr num xs -- R (L n) t

-- Happy path: every string parses cleanly, returns ~[1,2,3].
ok>R (L n) t;mapr num ["1","2","3"]

-- Short-circuit on the first Err: parsing "bad" surfaces ^bad and the
-- trailing "3" is never visited. Matches Rust's
-- collect::<Result<Vec<_>, _>>() semantics.
bail>R (L n) t;mapr num ["1","bad","3"]

-- Pair with `!` to keep a fallible pipeline flat. Caller's return type
-- must accept Err so the propagation is well-typed.
total xs:L t>R n t;ns=mapr! num xs;~len ns

-- Empty input is the trivial Ok case.
none>R (L n) t;mapr num []

-- run: ok
-- out: [1, 2, 3]

-- run: bail
-- err: ^bad

-- run: total ["1","2","3"]
-- out: 3

-- run: total ["1","bad","3"]
-- err: ^bad

-- run: none
-- out: []
14 changes: 10 additions & 4 deletions skills/ilo/SKILL.md

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions src/builtins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ pub enum Builtin {
Partition,
Frq,
Flatmap,
Mapr,

// Random / time
Rnd,
Expand Down Expand Up @@ -208,6 +209,7 @@ impl Builtin {
"partition" => Some(Builtin::Partition),
"frq" => Some(Builtin::Frq),
"flatmap" => Some(Builtin::Flatmap),
"mapr" => Some(Builtin::Mapr),
"rnd" => Some(Builtin::Rnd),
"rndn" => Some(Builtin::Rndn),
"now" => Some(Builtin::Now),
Expand Down Expand Up @@ -324,6 +326,7 @@ impl Builtin {
Builtin::Partition => "partition",
Builtin::Frq => "frq",
Builtin::Flatmap => "flatmap",
Builtin::Mapr => "mapr",
Builtin::Rnd => "rnd",
Builtin::Rndn => "rndn",
Builtin::Now => "now",
Expand Down Expand Up @@ -446,6 +449,7 @@ impl Builtin {
Builtin::Partition,
Builtin::Frq,
Builtin::Flatmap,
Builtin::Mapr,
Builtin::Rnd,
Builtin::Rndn,
Builtin::Now,
Expand Down Expand Up @@ -671,6 +675,7 @@ mod tests {
"partition",
"frq",
"flatmap",
"mapr",
"rnd",
"now",
"rd",
Expand Down Expand Up @@ -882,6 +887,7 @@ mod tests {
"partition",
"frq",
"flatmap",
"mapr",
"rnd",
"rndn",
"now",
Expand Down
53 changes: 53 additions & 0 deletions src/interpreter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2732,6 +2732,59 @@ fn call_function(env: &mut Env, name: &str, args: Vec<Value>) -> Result<Value> {
}
return Ok(Value::List(Arc::new(result)));
}
// mapr fn xs: short-circuiting Result-aware map.
//
// The callee must return R b e. On each item:
// ~v → unwrap to v and accumulate
// ^e → return ^e immediately (whole call short-circuits)
// any → runtime error (callee broke its contract)
//
// Final return on the all-Ok path is ~(L b). Pair with `!` to thread
// the err up into a Result-returning caller. Retires the
// `ton s:t>n;r=num s;?r{~v:v;^_:0}` helper that html-scraper and
// CSV-parsing personas kept writing. See ilo_assessment_feedback.md
// line 2541 for the originating entry.
//
// Deliberately 2-arity only: no closure-bind ctx variant yet. If a
// workload turns up that needs it, add it the same way `Map`/`Flt`
// have it. Keeping the surface tight until a real need surfaces.
if builtin == Some(Builtin::Mapr) && args.len() == 2 {
let fn_name = resolve_fn_ref(&args[0]).ok_or_else(|| {
RuntimeError::new(
"ILO-R009",
format!(
"mapr: first arg must be a function reference, got {:?}",
args[0]
),
)
})?;
let captures = closure_captures(&args[0]);
let items = match &args[1] {
Value::List(l) => l.clone(),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("mapr: list arg must be a list, got {:?}", other),
));
}
};
let mut result = Vec::with_capacity(items.len());
for item in items.iter().cloned() {
let mut call_args = vec![item];
call_args.extend(captures.iter().cloned());
match call_function(env, &fn_name, call_args)? {
Value::Ok(inner) => result.push(*inner),
Value::Err(e) => return Ok(Value::Err(e)),
other => {
return Err(RuntimeError::new(
"ILO-R009",
format!("mapr: fn must return a Result (~v or ^e), got {:?}", other),
));
}
}
}
return Ok(Value::Ok(Box::new(Value::List(Arc::new(result)))));
}
if builtin == Some(Builtin::Flt) && (args.len() == 2 || args.len() == 3) {
let fn_name = resolve_fn_ref(&args[0]).ok_or_else(|| {
RuntimeError::new(
Expand Down
1 change: 1 addition & 0 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3007,6 +3007,7 @@ fn builtin_arity_tables() -> (HashMap<String, usize>, HashMap<String, Vec<bool>>
("uniqby", 2, &[0]),
("partition", 2, &[0]),
("flatmap", 2, &[0]),
("mapr", 2, &[0]),
// I/O
("prnt", 1, &[]),
("wr", 2, &[]),
Expand Down
54 changes: 54 additions & 0 deletions src/verify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,7 @@ const BUILTINS: &[(&str, &[&str], &str)] = &[
("rdjl", &["t"], "L (R ? t)"),
// Higher-order: map/flt/fld take a function ref as first arg (special-cased in builtin_check_args)
("map", &["fn", "list"], "list"),
("mapr", &["fn", "list"], "result"),
("flt", &["fn", "list"], "list"),
("fld", &["fn", "list", "any"], "any"),
("grp", &["fn", "list"], "map"),
Expand Down Expand Up @@ -1655,6 +1656,59 @@ fn builtin_check_args(
};
(Ty::List(Box::new(ret_elem)), errors)
}
"mapr" => {
// mapr fn:F a (R b e) xs:L a → R (L b) e
//
// Short-circuiting parallel to `map`. The fn must return a Result;
// on the first ^err encountered the whole call returns that ^err,
// otherwise the unwrapped Ok values are collected into a list and
// wrapped in a single outer ~. Pairs with `!` to thread the err
// up into a Result-returning caller without per-item match noise.
// Retires the persona-written `ton s:t>n;r=num s;?r{~v:v;^_:0}`
// helper that html-scraper + CSV-parsing rerun3s kept writing.
if let Some(fn_ty) = arg_types.first()
&& !matches!(fn_ty, Ty::Fn(_, _) | Ty::Unknown)
{
errors.push(VerifyError {
code: "ILO-T013",
function: func_ctx.to_string(),
message: format!("'mapr' first arg must be a function (F ...), got {fn_ty}"),
hint: Some("pass a function name: mapr num xs".to_string()),
span,
is_warning: false,
});
}
// fn must return a Result. Catches the obvious misuse where the
// caller picked `mapr` over `map` for a non-fallible fn.
if let Some(Ty::Fn(_, ret)) = arg_types.first()
&& !matches!(ret.as_ref(), Ty::Result(_, _) | Ty::Unknown)
{
errors.push(VerifyError {
code: "ILO-T013",
function: func_ctx.to_string(),
message: format!(
"'mapr' fn must return a Result (R _ _), got {ret}; use 'map' for non-fallible fns"
),
hint: Some(
"mapr short-circuits on the first ^err; for non-fallible fns use map".to_string(),
),
span,
is_warning: false,
});
}
// Return type: R (L b) e from fn's R b e, or R (L Unknown) Unknown.
let (ok_elem, err_ty) = match arg_types.first() {
Some(Ty::Fn(_, ret)) => match ret.as_ref() {
Ty::Result(ok, err) => ((**ok).clone(), (**err).clone()),
_ => (Ty::Unknown, Ty::Unknown),
},
_ => (Ty::Unknown, Ty::Unknown),
};
(
Ty::Result(Box::new(Ty::List(Box::new(ok_elem))), Box::new(err_ty)),
errors,
)
}
"flt" => {
// flt fn:F a b xs:L a → L a
// flt fn:F a c b ctx:c xs:L a → L a (closure-bind variant)
Expand Down
8 changes: 7 additions & 1 deletion src/vm/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,12 @@ pub(crate) fn is_tree_bridge_eligible(b: crate::builtins::Builtin, argc: usize)
(Builtin::Uniqby, 2) => true,
(Builtin::Partition, 2) => true,
(Builtin::Srt, 2) => true,
// mapr fn xs: short-circuit Result-aware map. Tree bridge routes
// through the tree interpreter's Mapr arm, which handles the
// ~v/^e dispatch and ACTIVE_AST_PROGRAM user-fn callbacks the same
// way grp/uniqby/partition/srt do (PR 3b precedent). Returns
// R (L b) e; the bridge unwrap epilogue handles `!` propagation.
(Builtin::Mapr, 2) => true,
// Closure-bind ctx variants. The fn receives an extra ctx arg the
// native emitters don't shape today — bridge keeps semantics aligned
// with the tree interpreter and adds VM/Cranelift coverage in PR 3c.
Expand All @@ -422,7 +428,7 @@ pub(crate) fn is_tree_bridge_eligible(b: crate::builtins::Builtin, argc: usize)
/// the auto-unwrap (`!`) protocol when called via the tree bridge.
pub(crate) fn tree_bridge_returns_result(b: crate::builtins::Builtin) -> bool {
use crate::builtins::Builtin;
matches!(b, Builtin::Rd | Builtin::Rdb)
matches!(b, Builtin::Rd | Builtin::Rdb | Builtin::Mapr)
}

pub(crate) const OP_GETMANY: u8 = 136; // R[A] = get_many(R[B]) (L t → L (R t t), concurrent fan-out)
Expand Down
Loading
Loading