Skip to content

fix: preserve raw text for t-typed CLI args#242

Merged
danieljohnmorris merged 3 commits into
mainfrom
fix/cli-text-param-no-coerce
May 13, 2026
Merged

fix: preserve raw text for t-typed CLI args#242
danieljohnmorris merged 3 commits into
mainfrom
fix/cli-text-param-no-coerce

Conversation

@danieljohnmorris
Copy link
Copy Markdown
Collaborator

Summary

CLI args declared as t used to be silently coerced to Number when the raw input parsed as a finite float. f arg:t with input 2 arrived at runtime as Number(2.0), so num arg returned nil, which then collapsed the surrounding ?r{~i:..;^e:..} match into a silent nil return because there is no nil arm. Static types were fine, so the verifier never caught it.

This was the longstanding 🔴 friction documented in ilo_assessment_feedback.md since v0.10, and the same friction surfaced again in the v0.11.1 interactive-CLI rerun. Highest-leverage manifesto win here: zero retries for any agent that declares arg:t and passes digit-shaped input. Pre-fix the agent had to invent a workaround (a separate i:n param threaded through the dispatcher purely to keep the type honest). Post-fix the declared type does what it says.

Repro

f arg:t>n;r=num arg;?r{~i:i;^e:0 - 1}

Before:

$ ilo prog.ilo f 2
nil

After:

$ ilo prog.ilo f 2
2
$ ilo prog.ilo f abc
-1

Same result on --run-tree, --run-vm, --run-cranelift.

What's in the diff

  • fix: preserve raw text for t-typed CLI args — adds parse_cli_args_typed which consults declared param types during CLI parsing. t params keep the raw string verbatim as Text (the existing quoted-string strip is preserved for back-compat); L _ params still wrap a scalar; everything else parses by the same rules as before. Switches the six production callsites that used to do parse-then-coerce-by-type to the new typed parser. The legacy coerce_cli_args is kept under #[cfg(test)] so its list-wrap tests keep pinning that behaviour from pre-parsed values, but has no remaining production callers. Unit tests cover text-preservation across numeric, bool, nil, and list-shaped inputs, plus number passthrough, list wrap, mixed signatures, and the no-func-name fallback.
  • test: cross-engine regression for t-typed CLI argstests/regression_cli_text_arg.rs exercises all four engines for the num round-trip (both digit and non-digit input), identity on bool-shaped and nil-shaped inputs, and a sanity check that n-typed params still parse as numbers.
  • examples: t-typed CLI args preserve raw inputexamples/cli-text-arg.ilo documents the now-correct behaviour and is picked up by tests/examples_engines.rs for a third regression layer across every engine.

Test plan

  • cargo test --release --features cranelift — full suite green (2866 lib tests + integration crates)
  • cargo clippy --release --features cranelift --all-targets -- -W clippy::all — clean
  • cargo fmt --check — clean
  • Manual repro across all four engines (default, --run-tree, --run-vm, --run-cranelift) — all return 2 for f arg:t with input 2

Follow-ups

None. The fix is surgical: type-aware parsing for t only, every other declared type's behaviour is byte-for-byte unchanged.

CLI args declared as `t` used to be silently coerced to Number when the
raw input parsed as a finite float. `f arg:t` with input `2` arrived
at runtime as `Number(2.0)`, so `num arg` returned nil (it expects
text), which then collapsed the surrounding `?r{~i:..;^e:..}` match
into a silent nil return because there is no nil arm. The static types
were fine, so the verifier never saw it.

Add `parse_cli_args_typed` which consults the declared param types
during parsing: `t` params keep the raw CLI string as Text (with the
existing quoted-string strip preserved for back-compat); `L _` params
still wrap a scalar as a one-element list; everything else parses by
the same rules as before. The six production callsites that used to
do parse-then-coerce-by-type switch to the new typed parser.

The legacy `coerce_cli_args` is kept under #[cfg(test)] so its
list-wrap test cases keep pinning that behaviour starting from
pre-parsed Values, but it has no remaining production callers.

Adds unit tests covering text-preservation across numeric, bool, nil,
and list-shaped inputs, plus regression coverage for non-text params
(number passthrough, list wrap, mixed signatures, no-func-name
fallback).
Exercises all four engines (default JIT, --run-tree, --run-vm,
--run-cranelift) for the t-param coercion bug. Pre-fix all four
returned nil for `f arg:t` with input `2`; post-fix all four return
the parsed number.

Covers the num round-trip (digit and non-digit input), identity on
bool-shaped and nil-shaped inputs, and a sanity check that n-typed
params still parse as numbers.
Adds a worked example showing the now-correct behaviour: digit-shaped,
bool-shaped, and nil-shaped CLI inputs all arrive at t-typed params as
literal Text, so `num arg` unwraps cleanly and identity returns the
verbatim string.

Picked up by tests/examples_engines.rs so it acts as a third regression
layer across every engine.
@danieljohnmorris danieljohnmorris merged commit 90af70d into main May 13, 2026
4 checks passed
@danieljohnmorris danieljohnmorris deleted the fix/cli-text-param-no-coerce branch May 13, 2026 17:01
@codecov
Copy link
Copy Markdown

codecov Bot commented May 13, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ All tests successful. No failed tests found.

📢 Thoughts on this report? Let us know!

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