Skip to content

feat(sprintf): support full %q format specifications - #810

Open
Vitalii Tverdokhlib (vitaliytv) wants to merge 4 commits into
microsoft:mainfrom
vitaliytv:codex/sprintf-format-spec
Open

feat(sprintf): support full %q format specifications#810
Vitalii Tverdokhlib (vitaliytv) wants to merge 4 commits into
microsoft:mainfrom
vitaliytv:codex/sprintf-format-spec

Conversation

@vitaliytv

Copy link
Copy Markdown
Contributor

Depends on #809.

This follow-up completes %q support with a dedicated sprintf format-spec parser instead of extending the original PR's single-verb parsing incrementally.

What this adds

  • Parses Go-style flags, width, precision, dynamic * operands, and explicit argument indexes such as %[3]*.[2]*[1]q.
  • Supports %+q (QuoteToASCII behavior), %#q (backquoted strings when valid), left/right alignment, zero padding, and combined width/precision.
  • Applies width and precision in Unicode code points, matching Go/OPA rather than UTF-8 byte length.
  • Handles %q operands using OPA-compatible value conversion for strings, booleans, null, collections, integral runes, invalid runes, fractional numbers, and out-of-range integers.
  • Preserves the existing behavior of non-q verbs. Newly parsed flags that those verbs do not support are rejected explicitly.
  • Keeps width and precision bounded by the existing memory limit strategy.

Compatibility boundary

Regorus currently normalizes numbers into its internal Number variants and does not retain the original Rego numeric token spelling. OPA's %q behavior can depend on that spelling: for example, OPA distinguishes 65.0, 65e0, and 65, while Regorus may normalize them to different shared numeric variants. Exact parity for those lexical-number edge cases requires a separate change to the number representation; this PR does not disguise or broaden that architectural change.

The common %q matrix and parser edge cases were compared directly with OPA, including ASCII quoting, raw quoting and fallback, width/precision combinations, dynamic and indexed operands, runes, invalid runes, collections, and diagnostics.

Validation

  • Full repository pre-push suite passed.
  • cargo test: 298 passed, 2 ignored.
  • OPA compatibility suite: 2875 passed, 0 failed.
  • cargo test --locked --no-default-features --lib: 220 passed, 2 ignored.
  • cargo xtask test-no-std passed.
  • cargo clippy --locked --all-targets --all-features -- -D warnings passed.

This is intentionally stacked on #809. Until #809 is merged, GitHub's diff against main includes that prerequisite commit as well; afterward this PR reduces to the format-spec extension.

Go's `%q` verb (strconv.Quote) is used by OPA's sprintf, but regorus
currently bails on it unconditionally. Add support for `Value::String`
arguments only: wrap in double quotes, escape `"` and `\`, use the
short escapes for the common control characters (\a \b \f \n \r \t \v),
fall back to \xNN/\uNNNN/\UNNNNNNNN for other non-printable characters,
and leave printable (including non-ASCII) characters untouched. Unlike
json.marshal, %q does not HTML-escape < > &.

%q on non-string values (numbers, bools, etc.) still bails as before -
Go's %q on those produces different, single-quoted output that is out
of scope for this change.

Verified byte-for-byte against real OPA output (strconv.Quote
semantics) for quotes, backslashes, control characters, DEL, non-ASCII
printable text, and the < > & non-escaping case.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@anakrish

Copy link
Copy Markdown
Collaborator

Vitalii Tverdokhlib (@vitaliytv) Thank you for the contribution. Going to take a deeper look at this today/tomorrow.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Format compatibility gaps and unchecked allocation paths can produce incorrect output or substantially exceed configured memory limits.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Completes Go/OPA-compatible %q formatting for sprintf.

Changes:

  • Adds format-spec parsing, quoting flags, dynamic/indexed operands, and value conversions.
  • Ports Go Unicode printability tables.
  • Adds compatibility and memory-limit tests.
File summaries
File Description
src/builtins/strings.rs Implements %q formatting and integration.
src/builtins/strings/sprintf_format.rs Parses Go-style format specifications.
src/builtins/strings/go_is_print.rs Provides Go-compatible Unicode classification.
tests/interpreter/cases/builtins/strings/sprintf.yaml Adds interpreter compatibility cases.
tests/memory_limits.rs Tests quoting memory-limit propagation.
LICENSE Attributes Go-derived source.
Review details

Suppressed comments (1)

src/builtins/strings/sprintf_format.rs:119

  • A negative dynamic precision must produce Go's %!(BADPREC) diagnostic and then format without precision. With sprintf("%.*q", [-1, "x"]), this silently returns "x" instead of %!(BADPREC)"x", so a valid integer * operand has incompatible OPA behavior. Preserve a bad-precision marker in the parsed spec and emit it before formatting the value.
            let precision = take_integer(args, args_idx, args_span)?;
            if precision >= 0 {
                spec.precision = Some(checked_dynamic_value(precision as u64, args_span)?);
            }
  • Files reviewed: 6/6 changed files
  • Comments generated: 7
  • Review effort level: Balanced

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/builtins/strings.rs
Comment thread src/builtins/strings.rs Outdated
Comment thread src/builtins/strings.rs Outdated
Comment thread src/builtins/strings.rs
Comment thread src/builtins/strings/sprintf_format.rs
Comment thread src/builtins/strings/go_is_print.rs Outdated
Comment thread tests/interpreter/cases/builtins/strings/sprintf.yaml
@vitaliytv

Copy link
Copy Markdown
Contributor Author

Also addressed the suppressed review note in 3568dac: negative dynamic precision now emits Go/OPA-compatible %!(BADPREC) and then formats the value without precision. Added an interpreter regression case for %.*q with -1.

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.

3 participants