Skip to content

fix: type-check CLI args against entry function params (ILO-517) - #786

Merged
danieljohnmorris merged 7 commits into
mainfrom
fix/cli-arg-coercion
Aug 5, 2026
Merged

fix: type-check CLI args against entry function params (ILO-517)#786
danieljohnmorris merged 7 commits into
mainfrom
fix/cli-arg-coercion

Conversation

@danieljohnmorris

Copy link
Copy Markdown
Collaborator

Summary

A shell string that did not match its declared parameter type was bound as-is. tri n:n>n invoked as ilo tri.@ main handed the n param Text("main"): tree and VM printed NaN, the Cranelift JIT echoed the raw string, and every engine exited 0. A silent wrong answer with a success exit code is worse than a crash for any caller that checks $?. The CLI was less safe than the language it fronts, where num "main" returns R n t and the checker forces the failure to be handled.

Manifesto framing: the silent NaN cost a benchmark model four retries on an otherwise-correct program because the repair loop's entire error signal was the string "NaN" (ILO-364 traces). A boundary that fails loudly is cheaper than one that fails quietly.

Repro before/after

printf 'tri n:n>n;s=+n 1;p=*n s;/p 2\n' > c.@
ilo c.@ main    # before: NaN, rc=0 (JIT: 'main', rc=0)   after: ILO-R600, rc=1
ilo c.@ 10      # 55, rc=0 - unchanged

What's in the diff (per commit)

  • cli: type-check args (6fbefe0) - new check_cli_arg_types, sibling of check_cli_arity, wired into all four dispatch sites (VM, interpreter, JIT, default) so the error contract cannot drift per engine. Rejects unambiguous mismatches (numeric family, bool, optional inner) with ILO-R600 naming the parameter and offending value - first allocation in the hundreds-block runtime range per SPEC (flat R0xx are historical-only, enforced by error_code_namespaces). Deliberately permissive elsewhere: _ takes anything, O T takes nil, structural and user-defined types wave through, so the ILO-182 single-fn pass-through (ilo greet.@ world) keeps working. Guard chosen over threading Result through parse_cli_arg_for_param because the AOT C-callable ilo_aot_parse_arg_list cannot propagate one.
  • tests (cd5eb6d) - 8 regression tests across default/--vm/--jit, verified to fail with the guard disabled; examples/cli-arg-types.ilo pins the pass-through shapes in the examples harness.
  • changelog (bdfc6af)
  • revert stray NaN sentinel (0c6f1cf) - a parallel half-finished commit (ce7e865) had made non-numeric input to a n param parse to Number(NAN), which sails straight through a type guard and broke the explicit-fn path; restored cli_parse.rs to main.

Test plan

  • tests/regression_cli_arg_type_mismatch.rs: 8/8 across three engines, rejection tests verified failing without the guard
  • Full suite green (31 suites); only the two pre-existing doctest failures on main (ILO-532)
  • cargo fmt + cargo clippy --all-targets clean
  • ilo --explain ILO-R600 serves the registry entry

Follow-ups

  • -5 binds to U32 without complaint - range validation vs type validation, separate ticket
  • Sum(_) membership is not validated at the CLI boundary (deliberate - the guard is not a second type checker)

parse_cli_arg_for_param now tries f64 parse when expected type is Number.
If parse fails, returns NaN sentinel. parse_cli_args_typed checks for NaN
on Number-typed params and emits ILO-R005 + exit 1 instead of silently
passing Text to a Number param.
A shell string that didn't match its declared param type was bound
as-is: tri n:n>n invoked as 'ilo tri.@ main' gave the n param
Text("main"). Tree and VM printed NaN, the JIT echoed the raw string,
and all exited 0. The CLI was less safe than the language, where
num "main" returns R n t and must be handled.

New check_cli_arg_types, sibling of check_cli_arity, wired into all
four dispatch sites so the error contract can't drift per engine.
Rejects unambiguous mismatches (numeric family, bool, optional inner)
with ILO-R600 naming the parameter and the offending value. Deliberately
permissive elsewhere: _ takes anything, O T takes nil, structural and
user types wave through, so the ILO-182 single-fn pass-through
(ilo greet.@ world) keeps working.

R600 is the first allocation in the hundreds-block runtime range; flat
R0xx codes are historical-only per SPEC and enforced by
error_code_namespaces.
Eight regression tests: rejection on default/--vm/--jit, valid args
unchanged, ILO-182 bare-ident-to-t pass-through, bool accept/reject,
any-type passthrough, optional nil-vs-text, diagnostic names the param
and value. Verified the rejection tests fail with the guard disabled.
examples/cli-arg-types.ilo pins the pass-through shapes in the examples
harness (rejections can't live there - it asserts on success output).
ce7e865 landed a parallel half-finished approach: non-numeric input to
a Number param became Number(NAN), which sails through the type guard
(NaN is a Number) and broke the explicit-fn dispatch path - two of the
branch's own regression tests failed. The guard-based fix rejects at
the boundary with ILO-R600 and needs no sentinel; restore cli_parse.rs
to main.
rust 1.97 adds useless_borrows_in_formatting; CI lint runs -D warnings
so the pre-existing borrow in this untouched test now blocks every PR.
@codecov

codecov Bot commented Aug 5, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 86.95652% with 9 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/main.rs 86.95% 9 Missing ⚠️

📢 Thoughts on this report? Let us know!

Second half of the ce7e865 cleanup: with the sentinel reverted, a NaN
can no longer reach this block from CLI parsing, and its raw eprintln
bypassed the diagnostic system with a code (ILO-R005) that belongs to
field-not-found. The type guard covers the case properly with ILO-R600.
@danieljohnmorris
danieljohnmorris merged commit b355d5a into main Aug 5, 2026
11 checks passed
@danieljohnmorris
danieljohnmorris deleted the fix/cli-arg-coercion branch August 5, 2026 20:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant