Skip to content

Parity batch 2, group A: *args/**kwargs in filters and tests, given undefined arguments, divisibleby of a str (0306, 0310, 0308) - #728

Merged
rmorozov merged 6 commits into
masterfrom
claude/project-thread-ca4t45
Nov 1, 2026
Merged

rmorozov merged 6 commits into
masterfrom
claude/project-thread-ca4t45

Conversation

@rmorozov

@rmorozov rmorozov commented Nov 1, 2026 •

Copy link
Copy Markdown
Member

Requested by Ruslan · project thread

Closes tasks 0306, 0310 and 0308 (left in-progress for the steward's train).

Problem

  • 0306. Jinja2 accepts *args/**kwargs in filter calls, test calls and {% filter %} blocks ('a'|replace(*['a','b'])), because it binds them at call time. Jinja2C++ binds filter and test arguments at Load, one expression node per parameter, so it rejected unpacking with Unexpected token.
  • 0310. A filter could not tell an argument passed as an undefined value from one left out. As a result 'xyx'|trim(nope) stripped whitespace, while Python raises a TypeError. A sweep of every optional argument of every builtin against Python 3.12 / Jinja2 3.1.6 found 15 such arguments.
  • 0308. 'a' is divisibleby 2 answered False. Python raises, because the test is value % num == 0 and str % n is printf formatting.

What changes

  • Render-time binding, only for calls that unpack (0306).
    • The parser reads filter and test arguments with ParseCallArgs.
    • A call that unpacks becomes filters::UnpackingFilter / testers::UnpackingTester. On each evaluation these evaluate the spliced arguments (helpers::EvaluateCallParams, which raises Python's errors for a non-iterable *x or a non-mapping **x) and build the filter or test from those values. A call that does not fit the parameters is reported the same way as a written-out call.
    • FunctionBase binds either the Load-time nodes or the values:
      • FunctionArgs is a view over CallParamsInfo or CallParams.
      • BoundArguments has a value mode.
      • ForwardedArgs holds the extras that map, select/reject, applymacro, format and environment filters and tests pass on.
    • So map(*['replace','a','z']), select('divisibleby', *[3]) and format(*args) work too.
    • A call without unpacking is bound at Load exactly as before. CallArgsInfo now derives from CallParamsInfo, so the parsed arguments are passed through without a copy.
  • Given vs omitted (0310). FunctionBase::GetArgumentValue(name, context, given) reports whether the call passes the parameter, and IsArgumentSet (Python's is not None) uses that. They cover:
    • trim chars and replace count: TypeError;
    • truncate end/leeway, urlize trim_url_limit and wordwrap wrapstring: UndefinedError where Python uses the value;
    • attribute of join/max/min/sort/sum/unique: becomes an [Undefined] key path;
    • sum start: given values are used as is, so start=None now raises as in Python;
    • batch/slice fill_with: fills with the Undefined.
  • 0308. For a str, divisibleby runs PythonPercentFormat, which raises Python's TypeErrors, and otherwise answers False.
  • Found by the verifier.
    • map('map', 'map', …) nests one native call per argument with no stack check. Unpacking made that reachable from a 40-byte template or from context data. Map::Filter now calls CheckStack(), with a regression test in recursion_limits_test.cpp.
    • truncate now refuses a StrictUndefined end where Python takes len(end).

Files outside the batch brief's ownership:

  • function_base.cpp (new) and CMakeLists.txt
  • serialize_filters.cpp: StringFormat only, so format takes ForwardedArgs.
  • expression_evaluator.h: CallArgsInfo now derives from CallParamsInfo.
  • filters.h and testers.h.
    The batch lead and the steward know about these.

Validation

  • Corpus: 41 new cases (calls 23, filters 17, tests 2), regenerated under Python 3.12 + Jinja2 3.1.6 (generate.py --check clean). Six allow-list lines are removed (calls 4, filters 1, tests 1). ctest passes 5/5.
  • Merge with master: on a scratch merge with master acc3129, which includes the chat-template suite from test: Hugging Face chat template suite against transformers (0360 phase 1) #720, ctest passes 6/6, jinja2cpp_chat_templates included. No chat-template divergence line went stale. That merge was not pushed.
  • scripts/preflight.sh --perf: every check passes, including the instruction gate.
    • The first version cost +7–10% on Load/filters and Load/strings. After the CallArgsInfo and in-place changes, Load is at or below baseline.
    • The perf track held a9fb776 for Render/filters +2.2%, strings +1.2% and config_file +1.2% in CI. 696c314 fixes this. ForwardedArgs is now a plain CallParamsInfo with the unpacking values behind a pointer, so there is no variant visit. Whether an argument was given is read in the same lookup as its value, so the table is no longer scanned twice. ParseParams and the Map/Tester constructors moved out of filters.cpp, because there they had changed GCC's inlining of Join and Map (found with callgrind per-function diffs and nm -S).
    • Now every Render row is within +0.18% of base, and Load is at or below it.
  • Verifier: 2 rounds. Round 1 found one blocking issue, the nested-map stack overflow, which is fixed. Round 2: ready. ASan+UBSan (clang) ran the full jinja2cpp_tests, 8378 tests, with no reports. About 340 Python-oracle probes, narrow and wide, under every undefined policy. The perf rework in 696c314 was not run through the verifier again. It changes no behaviour, and ctest (corpus included) and preflight pass on it.
  • Roles: main session plus verifier (2 rounds). Red CI pushes: none. CI was green on a9fb776, and the perf track then held it for Render cost. CI is green on 696c314 (49 success, CodeQL neutral, 2 skipped), and the instruction-count job passed.
  • Codex: 0 findings, 0 fixed, 0 declined (no major issues on a9fb776 or on 696c314).

Remaining limits

  • Some error messages for unpacked calls are worded differently from Python. The corpus compares error against error.
  • divisibleby with a huge % width hits the existing 1 MiB formatting cap, which format shares, so it does not raise MemoryError.
  • 0367 (m is escaped of an imported module) is left for after B's module repr lands.

Generated by Claude Code

…ivisibleby of a str

0306: a filter or test call with *args or **kwargs (a filter block too) is
bound when it runs, as Python does: the parser keeps the call's arguments and
UnpackingFilter/UnpackingTester make the filter or test from their values on
each evaluation. FunctionBase binds either the Load-time expressions or those
values (FunctionArgs, BoundArguments), and the filters that pass arguments on
(map, select/reject, applymacro, format, environment filters and tests) keep
them as ForwardedArgs. A call without unpacking is bound at Load as before.

0310: FunctionBase tells an argument passed as an undefined value from one
left out (IsArgumentGiven, IsArgumentSet), and the filters that test an
optional argument with `is not None` treat a given Undefined as Python does:
trim, replace count, truncate end/leeway, urlize trim_url_limit, wordwrap
wrapstring, the attribute of join/max/min/sort/sum/unique, sum's start and
the fill_with of batch/slice. sum(start=None) now adds to None as Python does.

0308: `str is divisibleby n` formats the string with Python's %, raising its
TypeErrors, and is False otherwise.

Corpus: 40 new cases; six allow-list lines removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AWNL7oejcYgZdf6X4vtqUK
…t fixes

The verifier found that map('map', 'map', ...) nests a native call per argument with no
stack check; unpacked arguments made that a 40-byte template. Map::Filter now calls
CheckStack. truncate checks a StrictUndefined end where Python takes len(end), and reads
end once. FunctionBase::ParseParams no longer allocates a vector to check the bound
parameters; include and tidy fixes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AWNL7oejcYgZdf6X4vtqUK
CallArgsInfo extends CallParamsInfo, so a call that unpacks nothing is passed to the
filter as it was parsed; the filter's arguments are parsed in place (assigning an
expected swapped them); forwarded arguments are filled in place.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AWNL7oejcYgZdf6X4vtqUK
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AWNL7oejcYgZdf6X4vtqUK
@rmorozov rmorozov self-assigned this Nov 1, 2026

rmorozov commented Nov 1, 2026

Copy link
Copy Markdown
Member Author

@codex review

@codex security review (the change touches the expression parser and binds filter and test arguments at render time)


Generated by Claude Code

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-01T18:57:23.911948Z 696c314 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🚀

Reviewed commit: a9fb776934

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@rmorozov
rmorozov marked this pull request as ready for review November 1, 2026 18:07

rmorozov commented Nov 1, 2026

Copy link
Copy Markdown
Member Author

@codex security review


Generated by Claude Code

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. What shall we delve into next?

Reviewed commit: a9fb776934

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

The perf track measured +1-2% on Render/filters, strings and config_file.
- ForwardedArgs holds plain CallParamsInfo and the values of an unpacking
  call behind a pointer, so a call bound at Load visits no variant.
- GetArgumentValue(name, context, given) reads whether the call passes the
  parameter in the same lookup as its value; IsArgumentSet takes that bit,
  so there is no second scan of the table.
- ParseParams, and the Map and Tester constructors that Visit both kinds of
  arguments, move out of filters.cpp (function_base.cpp,
  filter_factories.cpp): there they changed GCC's inlining of Join and Map.

Every Render row is within +0.2% of base in callgrind; Load is at or below.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AWNL7oejcYgZdf6X4vtqUK
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AWNL7oejcYgZdf6X4vtqUK

rmorozov commented Nov 1, 2026

Copy link
Copy Markdown
Member Author

@codex review

696c314 reworks the render path for performance. ForwardedArgs no longer uses a variant, the given bit is read in the same lookup as the value, and ParseParams and the Map/Tester constructors move out of filters.cpp.


Generated by Claude Code

rmorozov commented Nov 1, 2026

Copy link
Copy Markdown
Member Author

@codex review

The previous run on 696c314 failed before it completed, so this is a retry.


Generated by Claude Code

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 👍

Reviewed commit: 696c3142a2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

rmorozov added a commit that referenced this pull request Nov 1, 2026
…defined arguments, divisibleby of a str (#728, 0306/0308/0310)

Stacked on master a074f49 as train #731; CI green, perf verdict approved on this head. Codex code review found no issues on 696c314; the Codex security review did not run on the final head (earlier head found none).
@rmorozov
rmorozov merged commit 7fdd9dd into master Nov 1, 2026
52 checks passed
rmorozov pushed a commit that referenced this pull request Nov 1, 2026
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTQTriEsFrrBgcYNKDUUF4
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.

2 participants