Skip to content

fmt: support .Nf / :N / :Nd / :<N placeholder specs (#16c)#554

Merged
danieljohnmorris merged 3 commits into
mainfrom
fix/fmt-format-specs
May 21, 2026
Merged

fmt: support .Nf / :N / :Nd / :<N placeholder specs (#16c)#554
danieljohnmorris merged 3 commits into
mainfrom
fix/fmt-format-specs

Conversation

@danieljohnmorris
Copy link
Copy Markdown
Collaborator

Summary

Pending entry #16c from `ilo_assessment_feedback.md`. `fmt` used to support only bare `{}` placeholders, which meant agents reaching for "two decimal places" or "right-align to width 5" had to compose `fmt2` + `padl` by hand:

```
fmt "{}%" (fmt2 (* r 100) 1) -- before
fmt "{.1f}%" (* r 100) -- now
```

Token-wise this is the difference between one builtin call per formatted field and three. The spec is intentionally lean: numeric precision, integer width, and string width are the cases that show up in every persona run, and that's all this adds. Zero-pad, sign-prefix, and hex stay out of scope so the surface doesn't grow into Python's `str.format` over time. Agents wanting those still compose via `padl` / `fmt2`.

Repro before / after

Before (`{:.3f}` rejected at verify-time with ILO-T013):
```
fmt "pi={:.3f}" 3.14159 -- ILO-T013: only supports bare {}
```

After:
```
fmt "pi={:.3f}" 3.14159 -- "pi=3.142"
fmt "{:5d}" 42 -- " 42"
fmt "[{:<5}]" "hi" -- "[hi ]"
fmt "{:06d}" 42 -- still ILO-T013 (zero-pad out of scope)
```

What's in the diff

  • `src/interpreter/mod.rs`: `Builtin::Fmt` arm now parses each `{...}` placeholder through a new `parse_fmt_spec` + `apply_fmt_spec` pair. Width digits with a leading zero are rejected so `{:06d}` doesn't silently become a 6-wide space pad. Single source of truth for tree, VM, and Cranelift via the existing tree-bridge.
  • `src/verify.rs`: literal-template scanner reuses `parse_fmt_spec` for slot counting and unknown-spec rejection. ILO-T013 hint now lists the supported set instead of saying "bare `{}` only".
  • `tests/regression_fmt_format_spec.rs`: cross-engine coverage (tree / VM / Cranelift) for each supported spec, the rejection paths (literal `{:06d}`, literal `{:.3}`, computed-template runtime spec), and the lone-brace JSON case.
  • `examples/fmt-format-spec.ilo`: in-context demo across the four shapes plus a multi-spec row. Picked up by `tests/examples_engines.rs`, so it's also an engine-level regression.
  • `SPEC.md` / `ai.txt`: fmt row updated to match the table that already shipped in `site/.../text.md` (commit 83fd702).

Test plan

  • `cargo test --release --features cranelift --test regression_fmt_format_spec` — 36 passed
  • `cargo test --release --features cranelift --test examples_engines` — passed
  • `cargo test --release --features cranelift` — full suite green except for the pre-existing `skill_md::body_is_thin_bootstrap` failure (also failing on main; SKILL.md is 9548 bytes, cap is 8000, unrelated to this PR)
  • `cargo clippy --release --features cranelift --all-targets -- -D warnings` — clean
  • `cargo fmt --all -- --check` — clean

Follow-ups

  • `skills/ilo/SKILL.md` is already over the 8 KB bootstrap cap on main; needs a separate trim PR.
  • If width-padding-char (other than space) becomes a recurring request, the natural extension is `padl s n c` / `padr s n c` rather than adding `{:0<5}` to the spec — keeps the printf surface lean.

Adds a small printf-style spec layer to fmt so simple numeric / width
formatting doesn't need a fmt2 + padl composition every time.

Supported:
  {}        bare placeholder (Display)
  {.Nf}     N decimal places (no colon)
  {:.Nf}    N decimal places (long form)
  {:N}      right-align Display to width N (space-pad)
  {:Nd}     integer right-align to width N (truncate toward zero)
  {:<N}     left-align Display to width N

Out of scope, still rejected:
  {:06d}    zero-pad widths (leading-zero width digits)
  {:+}      sign
  {:x}      hex
  {:.N}     precision without the f suffix

Width parsing rejects leading-zero multi-digit widths so {:06d} surfaces
as 'unsupported spec' rather than silently rendering as a 6-wide space
pad. The interpreter is the single source of truth; verify.rs reuses
parse_fmt_spec for static slot counting and unknown-spec rejection so
ILO-T013 and ILO-R009 stay in lockstep.
Cross-engine (tree, VM, Cranelift) regression for every supported spec
plus the rejection paths: zero-pad {:06d} (verify), precision without
f {:.3}, and computed-template runtime spec errors (ILO-R009).

The example file ships the same shapes so tests/examples_engines.rs
exercises them at the harness level too, and so agents loading
'ilo skill get ilo-examples' see the idiomatic forms.
site/text.md already documents the supported specs from a prior
docs-only commit; SPEC and ai.txt now match so the four canonical
docs (SPEC, ai.txt, skill, site) describe the same surface.
@codecov
Copy link
Copy Markdown

codecov Bot commented May 21, 2026

❌ 1 Tests Failed:

Tests completed Failed Passed Skipped
4931 1 4930 0
View the full list of 1 ❄️ flaky test(s)
ilo::regression_calendar_arithmetic::add_mo_result_out_of_calendar_range

Flake rate in main: 100.00% (Passed 0 times, Failed 5 times)

Stack Traces | 0.017s run time
thread 'main' (43731) panicked at src/interpreter/mod.rs:2019:21:
attempt to add with overflow
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

@danieljohnmorris
Copy link
Copy Markdown
Collaborator Author

CI status: 3 failing checks (lint, build, coverage), all pre-existing on main:

  • lint: skill-token-budget exceeds caps for ilo-builtins-core / -math / -io (unrelated to fmt change)
  • build: add_mo_result_out_of_calendar_range panics in calendar-arithmetic code merged in feature: calendar arithmetic builtins #495 (unrelated to fmt change)
  • coverage: same calendar-arithmetic panic, plus a version must be X.Y.Z Codecov uploader error
  • skill-validate passes

The only check that ran my changes (build) passed every fmt-related test (36 new tests across tree / VM / Cranelift, plus the example file via examples_engines). The single panicking test is in tests/regression_calendar_arithmetic.rs, which I haven't touched.

Confirmed by comparing run 26232609248 (this PR) against run 26232029355 (main HEAD a few minutes ago) — same failure signature, same line numbers, both unrelated to the fmt placeholder work.

@danieljohnmorris danieljohnmorris merged commit be3de85 into main May 21, 2026
1 of 4 checks passed
@danieljohnmorris danieljohnmorris deleted the fix/fmt-format-specs branch May 21, 2026 14:39
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