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
9 changes: 2 additions & 7 deletions ai.txt

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions src/diagnostic/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1874,7 +1874,7 @@ A function with explicit generic type parameters (`<a:comparable>`,
mn<a:comparable> x:a y:a>a
r=x;>(x) y{r=y};r

mn 1 "two" -- ILO-T044: 'a' bound to n then t
mn 1 "two" -- ILO-T045: 'a' bound to n then t
```

Fix: pass two values of the same type.
Expand All @@ -1884,7 +1884,7 @@ Fix: pass two values of the same type.
```
add-one<a:numeric> x:a>a;+x 1

add-one "hello" -- ILO-T044: text does not satisfy numeric
add-one "hello" -- ILO-T045: text does not satisfy numeric
```

Fix: pass a numeric argument.
Expand Down
7 changes: 6 additions & 1 deletion src/interpreter/json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,12 @@ impl Value {
Value::LazyStdinLines(_) => {
Err("stdin-lines iterator cannot be serialized".to_string())
}
Value::World { net, read, write, run } => {
Value::World {
net,
read,
write,
run,
} => {
let mut map = serde_json::Map::with_capacity(4);
map.insert("net".to_string(), serde_json::Value::Bool(*net));
map.insert("read".to_string(), serde_json::Value::Bool(*read));
Expand Down
63 changes: 51 additions & 12 deletions src/interpreter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -401,8 +401,16 @@ impl std::fmt::Display for Value {
}
Value::Ok(v) => write!(f, "~{}", v),
Value::Err(v) => write!(f, "^{}", v),
Value::World { net, read, write, run } => {
write!(f, "World {{net: {net}, read: {read}, write: {write}, run: {run}}}")
Value::World {
net,
read,
write,
run,
} => {
write!(
f,
"World {{net: {net}, read: {read}, write: {write}, run: {run}}}"
)
}
Value::FnRef(name) => write!(f, "<fn:{}>", name),
Value::Closure { fn_name, captures } => {
Expand Down Expand Up @@ -7234,15 +7242,31 @@ fn call_function(env: &mut Env, name: &str, args: Vec<Value>) -> Result<Value> {
if builtin == Some(Builtin::WorldCap) && args.is_empty() {
let (net, read, write, run) = match env.caps.as_ref() {
crate::caps::Caps::Permissive => (true, true, true, true),
crate::caps::Caps::Restricted { net, read, write, run } => {
crate::caps::Caps::Restricted {
net,
read,
write,
run,
..
} => {
let cap_allowed = |p: &crate::caps::Policy| {
matches!(p, crate::caps::Policy::All)
|| matches!(p, crate::caps::Policy::List(v) if !v.is_empty())
};
(cap_allowed(net), cap_allowed(read), cap_allowed(write), cap_allowed(run))
(
cap_allowed(net),
cap_allowed(read),
cap_allowed(write),
cap_allowed(run),
)
}
};
return Ok(Value::World { net, read, write, run });
return Ok(Value::World {
net,
read,
write,
run,
});
}

// world-no-net > World — construct a World with net=false.
Expand All @@ -7256,6 +7280,7 @@ fn call_function(env: &mut Env, name: &str, args: Vec<Value>) -> Result<Value> {
read,
write,
run,
..
} => {
let cap_allowed = |p: &crate::caps::Policy| {
matches!(p, crate::caps::Policy::All)
Expand Down Expand Up @@ -8562,7 +8587,12 @@ fn value_to_json(val: &Value) -> serde_json::Value {
serde_json::Value::Object(map)
}
Value::LazyStdinLines(_) => serde_json::Value::String("<stdin-lines>".to_string()),
Value::World { net, read, write, run } => {
Value::World {
net,
read,
write,
run,
} => {
let mut map = serde_json::Map::with_capacity(4);
map.insert("net".to_string(), serde_json::Value::Bool(*net));
map.insert("read".to_string(), serde_json::Value::Bool(*read));
Expand Down Expand Up @@ -9476,17 +9506,26 @@ fn eval_expr(env: &mut Env, expr: &Expr) -> Result<Value> {
)),
},
// World field access: .net .read .write .run → Bool
Value::World { net, read, write, run } => {
Value::World {
net,
read,
write,
run,
} => {
let v = match field.as_str() {
"net" => Value::Bool(net),
"read" => Value::Bool(read),
"write" => Value::Bool(write),
"run" => Value::Bool(run),
other if *safe => Value::Nil,
other => return Err(RuntimeError::new(
"ILO-R005",
format!("no field '{other}' on World (known: net, read, write, run)"),
)),
_other if *safe => Value::Nil,
other => {
return Err(RuntimeError::new(
"ILO-R005",
format!(
"no field '{other}' on World (known: net, read, write, run)"
),
));
}
};
Ok(v)
}
Expand Down
10 changes: 8 additions & 2 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1791,7 +1791,8 @@ statement boundary; bind the chain to a local first. For example, split \
Some(tok) => Err(self.error_hint(
"ILO-P007",
format!("expected type, got {}", tok.user_facing_name()),
"valid types: n, t, b, L n, R n t, F n>n, W (World), or a record type name".to_string(),
"valid types: n, t, b, L n, R n t, F n>n, W (World), or a record type name"
.to_string(),
)),
None => Err(self.error("ILO-P008", "expected type, got EOF".into())),
}
Expand Down Expand Up @@ -4158,7 +4159,12 @@ or write `({fmt_name} \"...\" ...)` so its args are grouped."
// `env-all!` / etc. Never consume args — return immediately as
// a 0-arg call. Without this guard the greedy args loop below
// would steal the first token of the next statement.
if name == "rdin" || name == "rdinl" || name == "env-all" || name == "world" || name == "world-no-net" {
if name == "rdin"
|| name == "rdinl"
|| name == "env-all"
|| name == "world"
|| name == "world-no-net"
{
return Ok(Expr::Call {
function: name,
args: vec![],
Expand Down
32 changes: 27 additions & 5 deletions src/verify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ pub enum Ty {
/// - `None` — dynamic (constructed via `world`, value is runtime-determined)
/// - `Some(false)` — statically known to deny net (constructed via `world-no-net`)
/// - `Some(true)` — statically known to allow net (future use)
World { net_known: Option<bool> },
World {
net_known: Option<bool>,
},
Unknown,
}

Expand Down Expand Up @@ -3939,7 +3941,12 @@ fn builtin_check_args(
// world-no-net > World — construct a World with net=false.
// Statically known to deny network access; the verifier emits
// ILO-T044 if this value flows into a scope that calls a net builtin.
(Ty::World { net_known: Some(false) }, errors)
(
Ty::World {
net_known: Some(false),
},
errors,
)
}
"run" => {
// run cmd:t args:L t > R (M t t) t
Expand Down Expand Up @@ -5451,12 +5458,27 @@ impl VerifyContext {
// dynamic worlds (world builtin, function parameters) are skipped.
if matches!(
callee.as_str(),
"get" | "pst" | "put" | "pat" | "del" | "hed" | "opt"
| "getx" | "pstx" | "get-many" | "get-to" | "pst-to"
"get"
| "pst"
| "put"
| "pat"
| "del"
| "hed"
| "opt"
| "getx"
| "pstx"
| "get-many"
| "get-to"
| "pst-to"
) {
let net_denied_var = scope.iter().rev().find_map(|frame| {
frame.iter().find_map(|(name, ty)| {
if matches!(ty, Ty::World { net_known: Some(false) }) {
if matches!(
ty,
Ty::World {
net_known: Some(false)
}
) {
Some(name.clone())
} else {
None
Expand Down
52 changes: 52 additions & 0 deletions src/vm/jit_cranelift.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7691,6 +7691,58 @@ mod tests {
assert_eq!(r, Some(Value::Bool(true)));
}

// ── ILO-387: Cranelift JIT regression tests for bounded generics ────────
// Generics are erased at compile time; the JIT sees monomorphic bytecode.
// These tests prove the JIT path executes bounded-generic functions correctly
// for each bound kind: comparable, numeric, and unbounded (any).

#[test]
fn cranelift_bounded_generic_comparable_min_numbers() {
// gmn<a:comparable>: lesser of two numbers.
let r = jit_run_numeric(
"gmn<a:comparable> x:a y:a>a\n r=x\n >(x) y{r=y}\n r",
"gmn",
&[7.0, 3.0],
);
assert_eq!(r, Some(3.0));
}

#[test]
fn cranelift_bounded_generic_comparable_max_numbers() {
// gmx<a:comparable>: greater of two numbers.
let r = jit_run_numeric(
"gmx<a:comparable> x:a y:a>a\n r=x\n <(x) y{r=y}\n r",
"gmx",
&[3.0, 7.0],
);
assert_eq!(r, Some(7.0));
}

#[test]
fn cranelift_bounded_generic_numeric_add() {
// gadd<a:numeric>: generic addition.
let r = jit_run_numeric("gadd<a:numeric> x:a y:a>a;+x y", "gadd", &[10.0, 20.0]);
assert_eq!(r, Some(30.0));
}

#[test]
fn cranelift_bounded_generic_any_identity_number() {
// gid<a>: unbounded identity with numeric arg.
let r = jit_run_numeric("gid<a> x:a>a;x", "gid", &[42.0]);
assert_eq!(r, Some(42.0));
}

#[test]
fn cranelift_bounded_generic_any_identity_text() {
// gid<a>: unbounded identity with text arg.
let r = jit_run(
"gid<a> x:a>a;x",
"gid",
&[Value::Text(Arc::new("hello".to_string()))],
);
assert_eq!(r, Some(Value::Text(Arc::new("hello".to_string()))));
}

// ── inline_chunk: CMPK_LT_N / CMPK_LE_N / CMPK_EQ_N / CMPK_NE_N arms ──

#[test]
Expand Down
14 changes: 12 additions & 2 deletions src/vm/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7971,14 +7971,24 @@ impl NanVal {
// can drain it one line at a time without buffering.
NanVal::heap_stdin_lines(handle.clone())
}
Value::World { net, read, write, run } => {
Value::World {
net,
read,
write,
run,
} => {
// World tokens are opaque at the VM level — represented as a
// tagged record so the VM can pass them through without special
// opcodes. Decode in `to_value` matches this layout.
use crate::vm::TypeInfo;
let type_info = Rc::new(TypeInfo {
name: "World".to_string(),
fields: vec!["net".to_string(), "read".to_string(), "write".to_string(), "run".to_string()],
fields: vec![
"net".to_string(),
"read".to_string(),
"write".to_string(),
"run".to_string(),
],
num_fields: 0,
});
let flat: Box<[NanVal]> = Box::new([
Expand Down
12 changes: 10 additions & 2 deletions tests/regression_world_builtin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ fn run_src(src: &str) -> (String, bool) {
let out = ilo().arg(src).output().expect("ilo binary not found");
let stdout = String::from_utf8_lossy(&out.stdout).trim().to_string();
let stderr = String::from_utf8_lossy(&out.stderr).trim().to_string();
let combined = if stderr.is_empty() { stdout } else { format!("{stdout}\n{stderr}") };
let combined = if stderr.is_empty() {
stdout
} else {
format!("{stdout}\n{stderr}")
};
(combined, out.status.success())
}

Expand All @@ -35,7 +39,11 @@ fn world_builtin_returns_world_type() {
fn world_field_net() {
let (out, ok) = run_src("main>b;w=world;w.net");
assert!(ok, "w.net should succeed: {out}");
assert_eq!(out.trim_end_matches(|c: char| !c.is_alphanumeric()).trim(), "true", "net={out}");
assert_eq!(
out.trim_end_matches(|c: char| !c.is_alphanumeric()).trim(),
"true",
"net={out}"
);
}

#[test]
Expand Down
Loading