Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,4 @@ test_capi
test_capi
/mypyc/lib-rt/build/
/mypyc/lib-rt/*.so
.worktrees/
17 changes: 17 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -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/
21 changes: 19 additions & 2 deletions mypyc/irbuild/format_str_tokenizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
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
Expand All @@ -42,6 +42,7 @@ class FormatOp(Enum):

STR = "s"
INT = "d"
ASCII = "a"
BYTES = "b"


Expand All @@ -53,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)
Expand Down Expand Up @@ -152,6 +164,11 @@ def convert_format_expr_to_str(
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 isinstance(folded := constant_fold_expr(builder, x), int):
var_str = builder.load_literal_value(str(folded))
Expand Down
9 changes: 9 additions & 0 deletions mypyc/primitives/str_ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
17 changes: 17 additions & 0 deletions mypyc/test-data/irbuild-constant-fold.test
Original file line number Diff line number Diff line change
Expand Up @@ -192,9 +192,14 @@ L0:
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
Expand All @@ -204,6 +209,18 @@ L0:
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
Expand Down
7 changes: 6 additions & 1 deletion mypyc/test-data/irbuild-str.test
Original file line number Diff line number Diff line change
Expand Up @@ -291,14 +291,15 @@ 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):
var :: str
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 = '.'
Expand All @@ -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]
Expand Down