From c0baf62f713a5d1781862626bc0058bdef6765f6 Mon Sep 17 00:00:00 2001 From: BobTheBuidler <70677534+BobTheBuidler@users.noreply.github.com> Date: Fri, 16 Jan 2026 22:12:39 -0400 Subject: [PATCH 1/6] Ignore .worktrees directory (#6) --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 9c325f3e29f8..880418969bd2 100644 --- a/.gitignore +++ b/.gitignore @@ -51,6 +51,7 @@ include/ pyvenv.cfg .tox +.worktrees pip-wheel-metadata From b5dfda62f4e05eea475fe6b1a40b7da76ca9b96d Mon Sep 17 00:00:00 2001 From: BobTheBuidler <70677534+BobTheBuidler@users.noreply.github.com> Date: Sat, 17 Jan 2026 00:28:47 -0400 Subject: [PATCH 2/6] chore: sync AGENTS rules to keep contributor guidance consistent (#8) --- .gitignore | 2 +- AGENTS.md | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 AGENTS.md diff --git a/.gitignore b/.gitignore index 880418969bd2..ed1e1ff7d572 100644 --- a/.gitignore +++ b/.gitignore @@ -51,7 +51,6 @@ include/ pyvenv.cfg .tox -.worktrees pip-wheel-metadata @@ -61,3 +60,4 @@ test_capi test_capi /mypyc/lib-rt/build/ /mypyc/lib-rt/*.so +.worktrees/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000000..2855e173a207 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,17 @@ +# Agent Requirements + +All agents must follow these rules: + +1) Fully test your changes before submitting a PR (run the full suite or all relevant tests). +2) PR titles must be descriptive and follow Conventional Commits-style prefixes: + - Common: `feat:`, `fix:`, `chore:`, `refactor:`, `docs:`, `test:`, `perf:` + - Support titles: `fix(docs):`, `fix(benchmarks):`, `fix(cicd):` +3) Commit messages must follow the same Conventional Commits-style prefixes and include a short functional description plus a user-facing value proposition. +4) PR descriptions must include Summary, Rationale, and Details sections. +5) Run relevant Python tests for changes (pytest/unittest or the repo's configured runner). +6) Follow formatting/linting configured in pyproject.toml, setup.cfg, tox.ini, or ruff.toml. +7) Update dependency lockfiles when adding or removing Python dependencies. +8) If the repo uses mypyc, verify tests run against compiled extensions (not interpreted Python) and note how you confirmed. +9) Keep base image tags pinned. + +Reference: https://www.conventionalcommits.org/en/v1.0.0/ From f910a54fd5ee55e2876dd38258f0202c61f7d9a8 Mon Sep 17 00:00:00 2001 From: BobTheBuidler <70677534+BobTheBuidler@users.noreply.github.com> Date: Sun, 18 Jan 2026 10:19:01 -0400 Subject: [PATCH 3/6] mypyc: add ascii format fast path (#17) * mypyc: add ascii format fast path * mypyc: extend ascii constant-fold tests --- mypyc/irbuild/format_str_tokenizer.py | 34 +++++++++++++++----- mypyc/primitives/str_ops.py | 11 ++++++- mypyc/test-data/irbuild-constant-fold.test | 36 ++++++++++++++++++++++ mypyc/test-data/irbuild-str.test | 7 ++++- 4 files changed, 79 insertions(+), 9 deletions(-) diff --git a/mypyc/irbuild/format_str_tokenizer.py b/mypyc/irbuild/format_str_tokenizer.py index 5a35900006d2..d34ecfe00801 100644 --- a/mypyc/irbuild/format_str_tokenizer.py +++ b/mypyc/irbuild/format_str_tokenizer.py @@ -12,7 +12,7 @@ ) from mypy.errors import Errors from mypy.messages import MessageBuilder -from mypy.nodes import Context, Expression, StrExpr +from mypy.nodes import Context, Expression from mypy.options import Options from mypyc.ir.ops import Integer, Value from mypyc.ir.rtypes import ( @@ -23,9 +23,10 @@ is_str_rprimitive, ) from mypyc.irbuild.builder import IRBuilder +from mypyc.irbuild.constant_fold import constant_fold_expr from mypyc.primitives.bytes_ops import bytes_build_op from mypyc.primitives.int_ops import int_to_str_op -from mypyc.primitives.str_ops import str_build_op, str_op +from mypyc.primitives.str_ops import ascii_op, str_build_op, str_op @unique @@ -41,6 +42,7 @@ class FormatOp(Enum): STR = "s" INT = "d" + ASCII = "a" BYTES = "b" @@ -52,14 +54,25 @@ def generate_format_ops(specifiers: list[ConversionSpecifier]) -> list[FormatOp] format_ops = [] for spec in specifiers: # TODO: Match specifiers instead of using whole_seq - if spec.whole_seq == "%s" or spec.whole_seq == "{:{}}": + # Conversion flags for str.format/f-strings (e.g. {!a}); only if no format spec. + if spec.conversion and not spec.format_spec: + if spec.conversion == "!a": + format_op = FormatOp.ASCII + else: + return None + # printf-style tokens and special f-string lowering patterns. + elif spec.whole_seq == "%s" or spec.whole_seq == "{:{}}": format_op = FormatOp.STR elif spec.whole_seq == "%d": format_op = FormatOp.INT + elif spec.whole_seq == "%a": + format_op = FormatOp.ASCII elif spec.whole_seq == "%b": format_op = FormatOp.BYTES + # Any other non-empty spec means we can't optimize; fall back to runtime formatting. elif spec.whole_seq: return None + # Empty spec ("{}") defaults to str(). else: format_op = FormatOp.STR format_ops.append(format_op) @@ -143,16 +156,23 @@ def convert_format_expr_to_str( for x, format_op in zip(exprs, format_ops): node_type = builder.node_type(x) if format_op == FormatOp.STR: - if is_str_rprimitive(node_type) or isinstance( - x, StrExpr - ): # NOTE: why does mypyc think our fake StrExprs are not str rprimitives? + if isinstance(folded := constant_fold_expr(builder, x), str): + var_str = builder.load_literal_value(folded) + elif is_str_rprimitive(node_type): var_str = builder.accept(x) elif is_int_rprimitive(node_type) or is_short_int_rprimitive(node_type): var_str = builder.primitive_op(int_to_str_op, [builder.accept(x)], line) else: var_str = builder.primitive_op(str_op, [builder.accept(x)], line) + elif format_op == FormatOp.ASCII: + if (folded := constant_fold_expr(builder, x)) is not None: + var_str = builder.load_literal_value(ascii(folded)) + else: + var_str = builder.primitive_op(ascii_op, [builder.accept(x)], line) elif format_op == FormatOp.INT: - if is_int_rprimitive(node_type) or is_short_int_rprimitive(node_type): + if isinstance(folded := constant_fold_expr(builder, x), int): + var_str = builder.load_literal_value(str(folded)) + elif is_int_rprimitive(node_type) or is_short_int_rprimitive(node_type): var_str = builder.primitive_op(int_to_str_op, [builder.accept(x)], line) else: return None diff --git a/mypyc/primitives/str_ops.py b/mypyc/primitives/str_ops.py index f6d3f722dd7b..2f217b77ad67 100644 --- a/mypyc/primitives/str_ops.py +++ b/mypyc/primitives/str_ops.py @@ -51,6 +51,15 @@ error_kind=ERR_MAGIC, ) +# ascii(obj) +ascii_op = function_op( + name="builtins.ascii", + arg_types=[object_rprimitive], + return_type=str_rprimitive, + c_function_name="PyObject_ASCII", + error_kind=ERR_MAGIC, +) + # translate isinstance(obj, str) isinstance_str = function_op( name="builtins.isinstance", @@ -180,7 +189,7 @@ name="rfind", arg_types=str_find_types[0 : i + 2], return_type=int_rprimitive, - c_function_name=str_find_functions[i], + c_function_name=str_rfind_functions[i], extra_int_constants=str_rfind_constants[i] + [(-1, c_int_rprimitive)], error_kind=ERR_MAGIC, ) diff --git a/mypyc/test-data/irbuild-constant-fold.test b/mypyc/test-data/irbuild-constant-fold.test index cd953c84c541..ba6cc3b73ae1 100644 --- a/mypyc/test-data/irbuild-constant-fold.test +++ b/mypyc/test-data/irbuild-constant-fold.test @@ -186,6 +186,42 @@ L0: big5 = r4 return 1 +[case testConstantFoldFormatArgs] +# This only tests that the callee and args are constant folded, +# it is not intended to test the result. +from typing import Any, Final + +FMT: Final = "{} {}" +FMT_A: Final = "{!a}" + +def f() -> str: + return FMT.format(400 + 20, "roll" + "up") +def g() -> str: + return FMT_A.format("\u00e9") +def g2() -> str: + return FMT_A.format("\u2603") +[out] +def f(): + r0, r1, r2, r3 :: str +L0: + r0 = CPyTagged_Str(840) + r1 = 'rollup' + r2 = ' ' + r3 = CPyStr_Build(3, r0, r2, r1) + return r3 +def g(): + r0, r1 :: str +L0: + r0 = "'\\xe9'" + r1 = CPyStr_Build(1, r0) + return r1 +def g2(): + r0, r1 :: str +L0: + r0 = "'\\u2603'" + r1 = CPyStr_Build(1, r0) + return r1 + [case testIntConstantFoldingFinal] from typing import Final X: Final = 5 diff --git a/mypyc/test-data/irbuild-str.test b/mypyc/test-data/irbuild-str.test index ee618bb34f65..16aed004e02f 100644 --- a/mypyc/test-data/irbuild-str.test +++ b/mypyc/test-data/irbuild-str.test @@ -291,6 +291,7 @@ def f(var: Union[str, NewStr], num: int) -> None: s2 = "I am %d years old." % num s3 = "Hi! I'm %s. I am %d years old." % (var, num) s4 = "Float: %f" % num + s5 = "Ascii: %a" % var [typing fixtures/typing-full.pyi] [out] def f(var, num): @@ -298,7 +299,7 @@ def f(var, num): num :: int r0, r1, r2, s1, r3, r4, r5, r6, s2, r7, r8, r9, r10, r11, s3, r12 :: str r13, r14 :: object - r15, s4 :: str + r15, s4, r16, r17, r18, s5 :: str L0: r0 = "Hi! I'm " r1 = '.' @@ -320,6 +321,10 @@ L0: r14 = PyNumber_Remainder(r12, r13) r15 = cast(str, r14) s4 = r15 + r16 = PyObject_ASCII(var) + r17 = 'Ascii: ' + r18 = CPyStr_Build(2, r17, r16) + s5 = r18 return 1 [case testDecode] From 8c9cd5bb29d2a1507738f7b00af01794e35696ac Mon Sep 17 00:00:00 2001 From: BobTheBuidler <70677534+BobTheBuidler@users.noreply.github.com> Date: Sun, 18 Jan 2026 10:41:07 -0400 Subject: [PATCH 4/6] Delete AGENTS.md --- AGENTS.md | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index 2855e173a207..000000000000 --- a/AGENTS.md +++ /dev/null @@ -1,17 +0,0 @@ -# Agent Requirements - -All agents must follow these rules: - -1) Fully test your changes before submitting a PR (run the full suite or all relevant tests). -2) PR titles must be descriptive and follow Conventional Commits-style prefixes: - - Common: `feat:`, `fix:`, `chore:`, `refactor:`, `docs:`, `test:`, `perf:` - - Support titles: `fix(docs):`, `fix(benchmarks):`, `fix(cicd):` -3) Commit messages must follow the same Conventional Commits-style prefixes and include a short functional description plus a user-facing value proposition. -4) PR descriptions must include Summary, Rationale, and Details sections. -5) Run relevant Python tests for changes (pytest/unittest or the repo's configured runner). -6) Follow formatting/linting configured in pyproject.toml, setup.cfg, tox.ini, or ruff.toml. -7) Update dependency lockfiles when adding or removing Python dependencies. -8) If the repo uses mypyc, verify tests run against compiled extensions (not interpreted Python) and note how you confirmed. -9) Keep base image tags pinned. - -Reference: https://www.conventionalcommits.org/en/v1.0.0/ From 14c890d5509ace66ed6e870aa9b8ae7d87a59db3 Mon Sep 17 00:00:00 2001 From: BobTheBuidler <70677534+BobTheBuidler@users.noreply.github.com> Date: Sun, 18 Jan 2026 10:41:34 -0400 Subject: [PATCH 5/6] Update .gitignore --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index ed1e1ff7d572..9c325f3e29f8 100644 --- a/.gitignore +++ b/.gitignore @@ -60,4 +60,3 @@ test_capi test_capi /mypyc/lib-rt/build/ /mypyc/lib-rt/*.so -.worktrees/ From 36e29deb45db7c51d3344d37b238e0446048be0e Mon Sep 17 00:00:00 2001 From: BobTheBuidler <70677534+BobTheBuidler@users.noreply.github.com> Date: Sun, 18 Jan 2026 10:44:40 -0400 Subject: [PATCH 6/6] Update str_ops.py --- mypyc/primitives/str_ops.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypyc/primitives/str_ops.py b/mypyc/primitives/str_ops.py index 2f217b77ad67..6a178accca8a 100644 --- a/mypyc/primitives/str_ops.py +++ b/mypyc/primitives/str_ops.py @@ -189,7 +189,7 @@ name="rfind", arg_types=str_find_types[0 : i + 2], return_type=int_rprimitive, - c_function_name=str_rfind_functions[i], + c_function_name=str_find_functions[i], extra_int_constants=str_rfind_constants[i] + [(-1, c_int_rprimitive)], error_kind=ERR_MAGIC, )