Skip to content
1 change: 1 addition & 0 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,7 @@ Called like functions, compiled to dedicated opcodes.
| `rnd` | random float in [0, 1) | `n` |
| `rnd a b` | random integer in [a, b] (inclusive) | `n` |
| `now` | current Unix timestamp (seconds) | `n` |
| `now-ms` | current Unix timestamp (milliseconds) | `n` |
| `get url` | HTTP GET | `R t t` |
| `get url headers` | HTTP GET with custom headers (`M t t` map) | `R t t` |
| `post url body` | HTTP POST with text body | `R t t` |
Expand Down
2 changes: 1 addition & 1 deletion ai.txt

Large diffs are not rendered by default.

19 changes: 19 additions & 0 deletions examples/timing.ilo
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
-- Per-phase timing with `now-ms`: bisect a perf regression in one run
-- instead of trimming the program and re-running. Pairs with `sleep` for
-- deterministic phase boundaries. Reports milliseconds since Unix epoch
-- as f64 (integer-precise for any realistic timestamp).

-- phase delta: how many ms did the call to `sleep ms` actually take?
delta ms:n>n;t0=now-ms;sleep ms;t1=now-ms;-t1 t0

-- Two-phase bisection helper: returns 1 if phase2 dominated phase1.
slower>n;a=delta 5;b=delta 20;c=>b a;?c{1}{0}

-- `now-ms` is always positive (epoch starts 1970), so this is a cheap
-- smoke test that gives a deterministic assertion target.
positive>b;>now-ms 0

-- run: positive
-- out: true
-- run: slower
-- out: 1
2 changes: 1 addition & 1 deletion skills/ilo/SKILL.md

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions src/builtins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ pub enum Builtin {
Rnd,
Rndn,
Now,
NowMs,
Dtfmt,
Dtparse,
Sleep,
Expand Down Expand Up @@ -222,6 +223,7 @@ impl Builtin {
"rnd" => Some(Builtin::Rnd),
"rndn" => Some(Builtin::Rndn),
"now" => Some(Builtin::Now),
"now-ms" => Some(Builtin::NowMs),
"dtfmt" => Some(Builtin::Dtfmt),
"dtparse" => Some(Builtin::Dtparse),
"sleep" => Some(Builtin::Sleep),
Expand Down Expand Up @@ -344,6 +346,7 @@ impl Builtin {
Builtin::Rnd => "rnd",
Builtin::Rndn => "rndn",
Builtin::Now => "now",
Builtin::NowMs => "now-ms",
Builtin::Dtfmt => "dtfmt",
Builtin::Dtparse => "dtparse",
Builtin::Sleep => "sleep",
Expand Down Expand Up @@ -521,6 +524,11 @@ impl Builtin {
// Named `ct` (not `cnt`) because `cnt` is reserved as the loop
// continue keyword — see src/parser/mod.rs:3507.
Builtin::Ct,
// Appended last to preserve existing on-wire tags. now-ms returns
// the current Unix epoch in milliseconds as f64 — paired with `now`
// (seconds) so per-phase timing has no rounding loss in agent
// perf-bisection workloads.
Builtin::NowMs,
];

/// On-wire 8-bit tag for cross-engine builtin dispatch. See `ALL`.
Expand Down Expand Up @@ -709,6 +717,7 @@ mod tests {
"mapr",
"rnd",
"now",
"now-ms",
"rd",
"rdl",
"rdb",
Expand Down Expand Up @@ -927,6 +936,7 @@ mod tests {
"rnd",
"rndn",
"now",
"now-ms",
"dtfmt",
"dtparse",
"rd",
Expand Down
8 changes: 8 additions & 0 deletions src/codegen/python.rs
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,14 @@ fn emit_expr(out: &mut String, level: usize, expr: &Expr) -> String {
if function == "now" && args.is_empty() {
return "(__import__('time').time())".to_string();
}
if function == "now-ms" && args.is_empty() {
// Mirrors `Builtin::NowMs` across the other engines.
// `time.time()` returns seconds-as-float; multiply by
// 1000 and floor to match `as_millis() as f64` truncation
// so cross-engine output agrees.
return "(__import__('math').floor(__import__('time').time() * 1000.0))"
.to_string();
}
if function == "sleep" && args.len() == 1 {
// sleep ms — emit a Python expression that blocks for ms
// milliseconds and evaluates to None (the ilo Nil sentinel).
Expand Down
11 changes: 11 additions & 0 deletions src/interpreter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1237,6 +1237,17 @@ fn call_function(env: &mut Env, name: &str, args: Vec<Value>) -> Result<Value> {
.as_secs_f64();
return Ok(Value::Number(ts));
}
if builtin == Some(Builtin::NowMs) && args.is_empty() {
// Unix epoch in milliseconds as f64. Paired with `now` (seconds)
// for perf-bisection workloads where seconds is too coarse to
// see a sub-second phase delta. f64 keeps integer precision
// for ms timestamps well past year 10000.
let ms = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis() as f64;
return Ok(Value::Number(ms));
}
if builtin == Some(Builtin::Sleep) && args.len() == 1 {
// sleep ms — blocks the current thread for `ms` milliseconds.
// Returns Nil so it composes as a statement inside loop bodies
Expand Down
24 changes: 22 additions & 2 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2727,8 +2727,10 @@ or write `({fmt_name} \"...\" ...)` so its args are grouped."
return self.parse_record(name);
}

// Zero-arg builtins: `rnd`/`now`/`mmap` with no args → Call with empty args
if (name == "rnd" || name == "now" || name == "mmap") && !self.can_start_operand() {
// Zero-arg builtins: `rnd`/`now`/`now-ms`/`mmap` with no args → Call with empty args
if (name == "rnd" || name == "now" || name == "now-ms" || name == "mmap")
&& !self.can_start_operand()
{
return Ok(Expr::Call {
function: name,
args: vec![],
Expand Down Expand Up @@ -3253,6 +3255,24 @@ results first: `r={first_op}a b;…r` keeps each step explicit."
Ok(Expr::Err(Box::new(inner)))
}
Some(Token::Dollar) => self.parse_dollar(),
// Zero-arg builtins (`now`, `now-ms`) in operand position auto-expand
// to a zero-arg Call. Without this, `>now-ms 0` parses `now-ms` as
// a bare Ref and the verifier emits ILO-T004 ("undefined variable
// 'now-ms'"). `mmap` is handled in `parse_atom` because it has no
// arity overload — `now` and `now-ms` are only kept out of
// `parse_atom` so the statement-head greedy-call shape `now x`
// still parses as `now(x)` and the verifier can surface its usual
// arity-mismatch error instead of a confusing ILO-P020 from a
// bare `x` at the next statement boundary.
Some(Token::Ident(name)) if name == "now" || name == "now-ms" => {
let name = name.clone();
self.advance();
Ok(Expr::Call {
function: name,
args: vec![],
unwrap: UnwrapMode::None,
})
}
_ => self.parse_atom(),
}
}
Expand Down
1 change: 1 addition & 0 deletions src/verify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,7 @@ const BUILTINS: &[(&str, &[&str], &str)] = &[
("rnd", &[], "n"),
("rndn", &["n", "n"], "n"),
("now", &[], "n"),
("now-ms", &[], "n"),
("sleep", &["n"], "_"),
("dtfmt", &["n", "t"], "R t t"),
("dtparse", &["t", "t"], "R n t"),
Expand Down
16 changes: 14 additions & 2 deletions src/vm/compile_cranelift.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ struct HelperFuncs {
rnd2: FuncId,
rndn: FuncId,
now: FuncId,
now_ms: FuncId,
env: FuncId,
get: FuncId,
spl: FuncId,
Expand Down Expand Up @@ -282,6 +283,7 @@ fn declare_all_helpers(module: &mut ObjectModule) -> HelperFuncs {
rnd2: declare_helper(module, "jit_rnd2", 2, 1),
rndn: declare_helper(module, "jit_rndn", 2, 1),
now: declare_helper(module, "jit_now", 0, 1),
now_ms: declare_helper(module, "jit_now_ms", 0, 1),
env: declare_helper(module, "jit_env", 1, 1),
get: declare_helper(module, "jit_get", 1, 1),
spl: declare_helper(module, "jit_spl", 3, 1),
Expand Down Expand Up @@ -1051,8 +1053,8 @@ fn compile_function_body(
OP_ADD_NN | OP_SUB_NN | OP_MUL_NN | OP_DIV_NN | OP_ADDK_N | OP_SUBK_N
| OP_MULK_N | OP_DIVK_N | OP_LEN | OP_LEN_HAS_K_COUNT | OP_ABS | OP_MIN
| OP_MAX | OP_FLR | OP_CEL | OP_ROU | OP_RND0 | OP_RND2 | OP_RNDN | OP_NOW
| OP_MOD | OP_CLAMP | OP_POW | OP_SQRT | OP_LOG | OP_EXP | OP_SIN | OP_COS
| OP_TAN | OP_LOG10 | OP_LOG2 | OP_ASIN | OP_ACOS | OP_ATAN | OP_ATAN2
| OP_NOWMS | OP_MOD | OP_CLAMP | OP_POW | OP_SQRT | OP_LOG | OP_EXP | OP_SIN
| OP_COS | OP_TAN | OP_LOG10 | OP_LOG2 | OP_ASIN | OP_ACOS | OP_ATAN | OP_ATAN2
| OP_MEDIAN | OP_MIN_LST | OP_MAX_LST | OP_QUANTILE | OP_STDEV | OP_VARIANCE
| OP_SUM | OP_AVG | OP_DOT | OP_DET | OP_ORD => {
num_write[a] = true;
Expand Down Expand Up @@ -2354,6 +2356,16 @@ fn compile_function_body(
builder.def_var(f64_vars[a_idx], rf);
}
}
OP_NOWMS => {
let fref = get_func_ref(&mut builder, module, helpers.now_ms);
let call_inst = builder.ins().call(fref, &[]);
let result = builder.inst_results(call_inst)[0];
builder.def_var(vars[a_idx], result);
if a_idx < reg_count && reg_always_num[a_idx] {
let rf = builder.ins().bitcast(F64, mf, result);
builder.def_var(f64_vars[a_idx], rf);
}
}
OP_ENV => {
let bv = builder.use_var(vars[b_idx]);
let fref = get_func_ref(&mut builder, module, helpers.env);
Expand Down
16 changes: 15 additions & 1 deletion src/vm/jit_cranelift.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ struct HelperFuncs {
rnd2: FuncId,
rndn: FuncId,
now: FuncId,
now_ms: FuncId,
env: FuncId,
get: FuncId,
spl: FuncId,
Expand Down Expand Up @@ -287,6 +288,7 @@ fn register_helpers(builder: &mut JITBuilder) {
("jit_rnd2", jit_rnd2 as *const u8),
("jit_rndn", jit_rndn as *const u8),
("jit_now", jit_now as *const u8),
("jit_now_ms", jit_now_ms as *const u8),
("jit_env", jit_env as *const u8),
("jit_get", jit_get as *const u8),
("jit_spl", jit_spl as *const u8),
Expand Down Expand Up @@ -475,6 +477,7 @@ fn declare_all_helpers(module: &mut JITModule) -> HelperFuncs {
rnd2: declare_helper(module, "jit_rnd2", 2, 1),
rndn: declare_helper(module, "jit_rndn", 2, 1),
now: declare_helper(module, "jit_now", 0, 1),
now_ms: declare_helper(module, "jit_now_ms", 0, 1),
env: declare_helper(module, "jit_env", 1, 1),
get: declare_helper(module, "jit_get", 1, 1),
spl: declare_helper(module, "jit_spl", 3, 1),
Expand Down Expand Up @@ -1127,7 +1130,7 @@ fn compile_function_body(
OP_ADD_NN | OP_SUB_NN | OP_MUL_NN | OP_DIV_NN
| OP_ADDK_N | OP_SUBK_N | OP_MULK_N | OP_DIVK_N
| OP_LEN | OP_LEN_HAS_K_COUNT | OP_ABS | OP_MIN | OP_MAX
| OP_FLR | OP_CEL | OP_ROU | OP_RND0 | OP_RND2 | OP_RNDN | OP_NOW
| OP_FLR | OP_CEL | OP_ROU | OP_RND0 | OP_RND2 | OP_RNDN | OP_NOW | OP_NOWMS
| OP_MOD | OP_CLAMP | OP_POW | OP_SQRT | OP_LOG | OP_EXP | OP_SIN | OP_COS
| OP_TAN | OP_LOG10 | OP_LOG2 | OP_ASIN | OP_ACOS | OP_ATAN | OP_ATAN2
| OP_MEDIAN | OP_MIN_LST | OP_MAX_LST | OP_QUANTILE
Expand Down Expand Up @@ -2499,6 +2502,17 @@ fn compile_function_body(
builder.def_var(f64_vars[a_idx], rf);
}
}
OP_NOWMS => {
let fref = get_func_ref(&mut builder, module, helpers.now_ms);
let call_inst = builder.ins().call(fref, &[]);
let result = builder.inst_results(call_inst)[0];
builder.def_var(vars[a_idx], result);
if a_idx < reg_count && reg_always_num[a_idx] {
let mf = cranelift_codegen::ir::MemFlags::new();
let rf = builder.ins().bitcast(F64, mf, result);
builder.def_var(f64_vars[a_idx], rf);
}
}
OP_ENV => {
let bv = builder.use_var(vars[b_idx]);
let fref = get_func_ref(&mut builder, module, helpers.env);
Expand Down
25 changes: 25 additions & 0 deletions src/vm/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ pub(crate) const OP_SLC: u8 = 55; // R[A] = slc(R[B], R[C], R[D]) (slice; D in
pub(crate) const OP_RND0: u8 = 57; // R[A] = random float in [0,1)
pub(crate) const OP_RND2: u8 = 58; // R[A] = random int in [R[B], R[C]]
pub(crate) const OP_NOW: u8 = 59; // R[A] = current unix timestamp (seconds, float)
pub(crate) const OP_NOWMS: u8 = 177; // R[A] = current unix timestamp (milliseconds, float)
pub(crate) const OP_ENV: u8 = 60; // R[A] = env(R[B]) (returns R t t)
pub(crate) const OP_JPTH: u8 = 61; // R[A] = jpth(R[B], R[C]) (JSON path lookup → R t t)
pub(crate) const OP_JDMP: u8 = 62; // R[A] = jdmp(R[B]) (value to JSON string → t)
Expand Down Expand Up @@ -3574,6 +3575,12 @@ impl RegCompiler {
self.reg_is_num[ra as usize] = true;
return ra;
}
(Builtin::NowMs, 0) => {
let ra = self.alloc_reg();
self.emit_abc(OP_NOWMS, ra, 0, 0);
self.reg_is_num[ra as usize] = true;
return ra;
}
(Builtin::Dtfmt, 2) => {
let rb = self.compile_expr(&args[0]);
let rc = self.compile_expr(&args[1]);
Expand Down Expand Up @@ -9532,6 +9539,14 @@ impl<'a> VM<'a> {
.as_secs_f64();
reg_set!(a, NanVal::number(ts));
}
OP_NOWMS => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let ms = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis() as f64;
reg_set!(a, NanVal::number(ms));
}
OP_DTFMT => {
let a = ((inst >> 16) & 0xFF) as usize + base;
let b = ((inst >> 8) & 0xFF) as usize + base;
Expand Down Expand Up @@ -13078,6 +13093,16 @@ pub(crate) extern "C" fn jit_now() -> u64 {
NanVal::number(ts).0
}

#[cfg(feature = "cranelift")]
#[unsafe(no_mangle)]
pub(crate) extern "C" fn jit_now_ms() -> u64 {
let ms = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis() as f64;
NanVal::number(ms).0
}

#[cfg(feature = "cranelift")]
#[unsafe(no_mangle)]
pub(crate) extern "C" fn jit_env(a: u64) -> u64 {
Expand Down
Loading
Loading