Skip to content

t27o Python debt census (measure first for #8995): lines, functions and the share that fits a convertible subset, ranked #11168

Closed

Description

@gHashTag

Goal

Measure the hand-written Python in our repositories, as the measure-first step of the Python front end (#8995, #9006): how many lines and functions, and what share fits a subset t27 can express today, so the first Python port is chosen from numbers. This is analysis only.

Why

Four measurements on 2026-10-19/20 show that converting existing Rust deletes at most about 1% of the hand-written lines: 3,006 lines in whole-converting files (0.69%, #11149) and 1,375 lines in movable fns of partly converting files (0.32%, #11159), because the converted share is bounded by what t27 cannot say (heap collections, owned strings and paths, iterators with closures, tuples). #9000's baseline puts Python at 16% of the t27 repository (against Rust 25%), and #8995 names scripts/*.py and tools/*.py as the largest hand-written foreign debt in our own repos. Nobody has counted it with the discipline used for Rust.

Boundary

  • docs/now/2026-10-20-python-debt.md

User Scenarios

  • Given the tracked .py files of the five repositories, when the census runs, then it prints per file the physical lines, the functions, the share of functions in the subset and whether the file has tests.
  • Given a file with only pure functions over numbers, bytes and lists, when it is ranked, then it is marked as a whole-file candidate with its line count.
  • Given a function that reads files, runs a process, uses a class with state or a dynamic feature, when it is classified, then the reason is named.

Requirements

  • FR-001: The census MUST parse with Python's own ast module (no regexes), exclude vendored, generated and test-only paths with the reason, and count lines the way the own-language gate does.
  • FR-002: The subset rules MUST be written down in the document (allowed types, calls and imports) so the number can be reproduced, and 30 functions classified by hand MUST be compared with the scanner to report its precision.
  • FR-003: This issue is analysis only: no Python is deleted and no spec is added before the table is on the issue.

Success Criteria

  • The table is on this issue and in docs/now/2026-10-20-python-debt.md, with at least 20 rows ranked by lines.
  • It names the first whole-file candidate of at least 150 lines of Python whose functions all fit the subset, or states in numbers why none does.

Activity

  1. gHashTag commented on Oct 19, 2026

    @gHashTag
    OwnerAuthor

    Python debt census for #11168 (analysis only; no Python is deleted and no spec is added). The same tables, the full ranked list (64 typed-subset files, 25 shape-subset files, the 20 largest files) and the 30-row hand-check table are in docs/now/2026-10-20-python-debt.md, PR #11176.

    Measured at t27 master 0e00974 (this repository), trios main 78671ba794, trinity main 8e535bc9bb, trinity-fpga main c9b180bf78, 999-multibots-telegraf main fcdbb2f7dc: the commits #11149 and #11159 used. The local checkouts of the four sibling repositories were not used (stale feature branches); the commits were fetched into scratch. python3 -I on Python 3.14.6, ast and tokenize only: no regular expression classifies code.

    Answer

    • Prize, in one sentence: 812 of 331,183 distinct hand-written, non-test Python lines (0.25%) sit in the 105 distinct functions that fit the typed subset today, and 4,090 lines (1.23%) in the 454 that fit once their parameter types are inferred from traces; no file is a whole-file candidate of 150 lines or more, so a Python port deletes functions, not files.
    • Scale: 2,126 tracked .py files hold 434,986 gate-counted lines (t27 alone 153,503, which equals git diff --numstat of deleting every tracked .py there, checked). After 23 generated, 22 vendored and 138 test-only files, the census holds 1,943 files and 412,066 lines; 1,906 files parse (12,547 functions, 1,001 classes) and 37 do not. Counting identical contents once (230 of trinity's 306 scanned files are byte-identical files of trinity-fpga) that is 1,618 files, 331,183 lines and 8,501 distinct function bodies.
    • Typed subset (T): 230 fns / 1,793 lines raw, 105 distinct bodies / 812 lines (0.25% of 331,183). Shape subset (S, parameter types still to be inferred from traces, t27o: Python dynamics -- a static subset with trace-based type inference (RightTyper, MonkeyType, Codon) #9006): 654 fns / 5,444 lines raw, 454 distinct bodies / 4,090 lines (1.23%). For comparison the Rust measurements were 0.69% (whole files, t27o golden PRs (step 4 of #9000), measured first: the 85 Rust files that convert whole, ranked by hand lines a port would delete #11149) and 0.32% (movable fns, t27o golden functions (step 4 of #9000), measured first: hand lines in converted fns of partly converting files, and which can move without changing a caller #11159).
    • The functions that fit are small: the median in-subset function has 6 lines (T) and 6 lines (S), the largest 34 (T) and 89 (S); 4 of the 105 T bodies and 34 of the 454 S bodies have 20 lines or more. The median function of the whole census has 13 lines.
    • Ceiling if t27 could say heap values: a further 1,293 distinct fns / 16,577 lines (5.0%) are pure but blocked only by owned strings, dict/set, comprehensions, union or None returns, or a pure standard-library import (re, struct, hashlib, fractions, ...). That is the same wall that bounded the Rust census: what t27 cannot say, not what a converter has not learned.
    • Whole-file candidates: T none; S one, tools/ternary_model.py (93 lines, 12 fns, no test, imported by tools/verify_exhaustive.py, and it already has a spec twin specs/port/tools/ternary_model.t27). The first whole-file candidate of 150 lines or more whose functions all fit: none. Of 737 distinct files with 150 lines or more, 0 have every function in T and 1 has every function in S (archive/experiments/kinetic_sacred_pure.py, 256 lines, 3 functions) but it also runs 82 module-level statements (a report script made of top-level print calls), so it is not whole. The closest real candidate is tools/gft_train_demo.py (201 lines): 16 of 18 functions fit the shape subset (130 of 201 lines), but none is typed and main (33 lines) reads random and prints.
    • Oracles: of the 64 distinct files that hold a T function, 3 have a test file that imports them and none has a doctest; 20 contain assert statements or inline test functions. 62 of the 64 are scripts with an if __name__ guard. The original itself, run on sampled inputs, is the only oracle for most of them (t27o: Python front end, with types inferred from oracle traces #8995).
    • Hand check: 30 functions classified by hand against the written rules before looking at the scanner's verdict. First round 29 of 30 agreed (one false positive: a function returning None or bytes; the rule "optional return" was added). Fresh sample of 30 with the final scanner: 30 of 30 agree (15 of 15 in-subset, 15 of 15 out; 95% lower bound for the in-class precision, one-sided, 82%).
    • Entry points: no whole-file candidate means a port deletes functions, and t27c gen-python lowers no function (it emits consts, enums and structs only; a fn block is announced in a comment and no def is ever written, see bootstrap/src/codegen_python.rs). A script whose functions move to a spec therefore keeps calling them through a hand-written loader such as scripts/tri_loop/t27b_rules.py (compiles the generated C and calls it with ctypes), or its callers change. Section "Callers and entry points" has the numbers.

    Scope and exclusions

    rule files lines reason
    generated root (gen/, bootstrap/gen/, bootstrap/src/memory/generated/) 0 0 the own-language gate's generated roots (specs/policy/own_language.t27); no tracked .py sits there
    generated header 23 1,699 a comment line among the first 8 lines says one of: do not edit, auto-generated, autogenerated, this file is generated, generated by, @generated, code generated, automatically generated. 9 VIBEE multi-language generator outputs in each of trinity and trinity-fpga, plus 5 *_params.py copies of a spec in trinity-fpga. A docstring that says "generated by t27c" is prose and does not count (2 hand-written t27 files had it)
    vendored 22 8,180 a path component in node_modules, vendor, vendored, third_party, third-party, site-packages, venv, .venv, pycache, zig-pkg, dist, build, target. All 22 are the zig-pkg/<hash>/ emsdk checkout (11 files, 4,090 lines in each of trinity and trinity-fpga)
    test-only 138 22,920 a component tests, test, __tests__, or a file named test_*.py, *_test.py, conftest.py. Excluded from the ranking (a port deletes the tests together with the code or not at all), used as oracles in the "oracle" column
    parse failures 37 (+1 test-only) 8,084 ast.parse raises (Jupyter magics such as !pip and %choose, a line of JavaScript in a .py file, a truncated try block, unmatched brackets, an em dash in code, an unescaped brace in an f-string). They stay in the line counts, because the gate counts them, and carry no classified function
    census 1,943 412,066 what is left: 1,906 parsed files (403,982 lines) and 37 unparsed (8,084)

    The subset (what "fits" means)

    Rules are applied to every function in source order; the first violation by position is the "first blocker" (signature first; optional return is positioned at the last line so that it is first only when nothing else blocks). A function is in the typed subset T when no rule fires and every parameter is typed; in the shape subset S when the same holds but a parameter may be untyped.

    • R1 kind. A plain def. Out: async, yield, await, lambda, a nested def or class, global/nonlocal, del, the walrus, match, try, with, a decorator other than staticmethod (@property, @classmethod, @functools.lru_cache, @torch.no_grad() and every framework decorator), *args/**kwargs. Keyword-only parameters are allowed.
    • R2 types. A parameter is typed when its annotation is int, float, bool, bytes, str, list/List/Sequence/tuple/Tuple of those (recursively), or a value class of the same file (R7); or when it has no annotation and a literal int/float/bool/str/bytes default (the type is inferred from the literal), or a default that is a module constant of such a type. An annotation outside that grammar (Optional, Union, X | None, dict, set, Any, Callable, Path, Fraction, a class with state, np.ndarray) is out as param type; the return annotation follows the same grammar plus None (return type). A function without a return annotation that returns None on one path and a value on another, or falls off the end after returning a value elsewhere, is out as optional return. A parameter that has no annotation and no literal default is untyped: the function is in S only.
    • R3 names. A name that is read must be a parameter, a local, a module-level constant, a same-module function or value class (R9), math.*, or one of the builtins len abs min max sum range int float bool bytes bytearray round pow divmod ord chr list tuple sorted reversed enumerate zip all any. A module-level constant is a name assigned at module level from literals, lists/tuples of literals, arithmetic and comparisons over them, math.* calls, or other constants (tuple targets allowed); a constant that a function mutates (TABLE.append, TABLE[i] = x) is module state. Everything else is out with a named reason: io builtin (open, print, input, exit), dynamic (eval, exec, compile, getattr, setattr, hasattr, globals, locals, vars, type, id, isinstance, issubclass, callable, dir, super, hash), dict/set (dict, set, frozenset, defaultdict, Counter, deque, a dict or set literal or comprehension, a dict or set method), iterator (map, filter, iter, next), module state (a module-level name that is not a constant, __file__ included), unknown name.
    • R4 imports. Only math, typing, dataclasses, enum, collections.abc (and __future__). A function that reads a name bound by any other import is out: io module when the module is one of os, sys, subprocess, socket, requests, urllib, http, pathlib, shutil, glob, tempfile, io, logging, argparse, getpass, sqlite3, asyncio, threading, multiprocessing, signal, json, csv, pickle, time, datetime, random, secrets and a few more network, archive and config modules; import use for every other module (numpy, torch, re, struct, hashlib, fractions, a local module). An import the function never reads is ignored.
    • R5 strings are read-only text. Allowed: indexing, slicing, len, comparison, in, iteration, ord, chr, startswith, endswith, find, rfind, index, count and the is* tests. Out as owned string: an f-string with a field, str(), repr, format, hex, bin, oct, +, * or % on a str, and the methods lower upper strip lstrip rstrip replace join split rsplit splitlines format zfill ljust rjust center title capitalize swapcase hex decode encode removeprefix removesuffix partition and their kin. The message of a raise or an assert is exempt.
    • R6 lists and tuples are allowed: literals, indexing, slicing, element assignment, append/extend/pop/insert/sort/reverse/copy, list comprehensions, tuple unpacking, returning a tuple. Out: generator expressions (generator expression), set and dict comprehensions, starred expressions. Use of list growth, tuple values and element writes is recorded as a flag and does not exclude (they are the features the Rust census found t27 cannot say, so they are reported below as a second number).
    • R7 methods and attributes. A method is classified like a function only when its class is a value class: @dataclass(frozen=True) with annotated fields that fit R2, an Enum, a NamedTuple, or a namespace class that holds only staticmethods. self.x may be read when x is a declared field. Any assignment to self.x, a @property, a @classmethod, or a method of any other class is out (stateful class method, self state, decorator). An attribute or method on any other value is out as attribute or method outside table; the allowed methods are the string and list ones named above plus bit_length, bit_count, to_bytes and int.from_bytes.
    • R8 exceptions. A plain raise of a built-in exception (ValueError, TypeError, IndexError, KeyError, ZeroDivisionError, AssertionError, NotImplementedError, OverflowError, RuntimeError, ArithmeticError, Exception) is allowed. try, a re-raise, raise ... from and every other exception class are out (try, exceptions).
    • R9 calls. A call to a same-module function or value class is allowed only when the callee is in the same tier (a fixpoint over the module; calls a function that is out). A call through a local name or a computed callee is out (call).
    • R10 whole file. Every function and class of the file is in the tier, the module level holds only a docstring, imports, constants (R3), defs and in-subset classes, and there is no if __name__ == "__main__". Near-whole: the same, with main and its nested defs left out.
    • Line counts. Physical lines are what git diff --numstat reports for a deleted file (newline-terminated lines plus an unterminated last line), i.e. what the own-language gate counts. Code lines carry at least one token other than a comment or layout token (tokenize), docstring lines included. A function's lines are its span from the first decorator line to end_lineno (blank lines, comments and docstring included). Functions are every def (module-level, method, nested); lambdas are not counted as functions.

    Totals per repository

    repo commit tracked .py gate-counted lines generated / vendored parse failures test-only scanned files lines / code fns f+m+n classes in subset / all T fns / lines S fns / lines whole T / S files (lines) near-whole S files (lines)
    t27 0e00974 655 153,503 0 / 0 7 52 596 141,796 / 116,879 3245+509+289 5 / 143 29 / 289 163 / 1,210 0 (0) / 1 (93) 1 (201)
    trios 78671ba794 12 2,382 0 / 0 0 0 12 2,382 / 1,975 43+0+3 0 / 0 0 / 0 1 / 32 0 (0) / 0 (0) 0 (0)
    trinity 8e535bc9bb 378 79,821 9 / 11 15 37 306 71,005 / 56,402 1082+1059+52 27 / 346 86 / 672 133 / 946 0 (0) / 0 (0) 0 (0)
    trinity-fpga c9b180bf78 1059 194,623 14 / 11 16 46 972 184,498 / 150,269 4254+1585+292 53 / 503 114 / 830 352 / 3,220 0 (0) / 0 (0) 4 (351)
    999-multibots fcdbb2f7dc 22 4,657 0 / 0 0 2 20 4,301 / 3,518 107+23+4 0 / 9 1 / 2 5 / 36 0 (0) / 0 (0) 0 (0)
    five, raw 2,126 434,986 23 / 22 38 137 1906 403,982 / 329,043 8731+3176+640 85 / 1001 230 / 1,793 654 / 5,444 0 (0) / 1 (93) 5 (552)
    five, distinct 348,524 114 1594 325,897 / 267,886 8501 bodies 105 / 812 454 / 4,090 0 (0) / 1 (93) 5 (552)

    Columns: gate-counted lines is every tracked .py including generated, vendored and test files, counted as git diff --numstat would; generated / vendored and parse failures and test-only are file counts; lines / code are the scanned (parsed, non-test) files; fns are module-level functions + methods + nested defs; classes in subset / all are module-level classes; 999-multibots is 999-multibots-telegraf; whole T / S and near-whole S count whole-file candidates (R10) with their physical lines in parentheses; near-whole leaves main out. The raw row repeats copies (trinity and trinity-fpga share byte-identical files); the distinct row counts identical files once and identical function bodies once, so its T/S counts are bodies, and its line count adds up only the parsed files (325,897; with 5,286 lines in unparsed files the census is 331,183 lines).

    Files ranked by the lines of their typed-subset (T) functions (top 30 of 64 distinct files)

    # repo path lines / code fns f+m+n / classes T fns / lines S fns / lines oracle entry callers copies verdict
    1 trinity archive/experiments/golden_key_proof.py 442 / 317 12+0+0 / 0 8 / 96 8 / 96 none script amb 1 3 partial
    2 trinity archive/experiments/trinity_benchmarks.py 389 / 283 11+9+0 / 2 6 / 51 6 / 51 none script none found 3 partial
    3 t27 conformance/gen_structural_instances.py 190 / 169 8+0+0 / 0 2 / 47 2 / 47 none script doc 3 0 partial
    4 trinity deploy/scripts/multi_provider_hybrid.py 743 / 563 12+12+0 / 5 6 / 44 6 / 44 none script py 3 3 partial
    5 trinity tools/scripts/scripts/multi_provider_hybrid.py 743 / 563 12+12+0 / 5 6 / 44 6 / 44 none script py 3 1 partial
    6 t27 tools/trinity_tri_api.py 2334 / 2004 103+4+13 / 3 5 / 43 9 / 65 1 inline test fn, 1 asserts script py 3, code 4, doc 3 0 partial
    7 t27 tools/queen/propose_boundary.py 606 / 507 18+0+0 / 0 3 / 42 3 / 42 1 inline test fn script py 1, code 1, doc 5 0 partial
    8 trinity-fpga conformance/wasm_board_vectors.py 261 / 222 14+2+0 / 2 3 / 27 4 / 29 none script py 1 0 partial
    9 t27 scripts/cocotb_ref_model.py 1616 / 1336 51+12+1 / 3 4 / 23 4 / 23 none script code 1, doc 198 0 partial
    10 t27 tools/trinity_manifest.py 1048 / 935 27+0+3 / 0 1 / 22 1 / 22 3 inline test fn script code 53, doc 7 1 partial
    11 trinity deploy/trinity-nexus/tools/bindings/python/igla.py 293 / 224 3+14+0 / 2 2 / 22 2 / 22 none script amb 4 3 partial
    12 trinity-fpga research/verify_lucas_product.py 207 / 178 6+0+0 / 0 4 / 21 4 / 21 none script none found 0 partial
    13 trinity-fpga conformance/gf16_conformance_ax7203.py 130 / 99 6+0+0 / 0 2 / 20 2 / 20 1 asserts script none found 0 partial
    14 t27 tools/trinity_build_receipt.py 586 / 529 16+0+1 / 0 1 / 19 1 / 19 1 inline test fn script code 1, doc 3 0 partial
    15 trinity docs/notebooks/quantum/e8_particles.py 487 / 352 7+17+1 / 8 2 / 19 2 / 19 none script none found 1 partial
    16 t27 tools/dupe_scan.py 445 / 385 12+0+0 / 0 1 / 19 1 / 19 1 inline test fn script ci 1, code 11, doc 16 0 partial
    17 trinity-fpga research/spectrum/spectrum.py 227 / 185 11+0+0 / 0 4 / 18 4 / 18 none script py 2 0 partial
    18 t27 contrib/backend/github/auth.py 90 / 70 0+3+0 / 1 1 / 17 1 / 17 1 test file module py 2, doc 1 0 partial
    19 trinity-fpga conformance/decimal_ref.py 477 / 352 14+10+2 / 2 1 / 16 1 / 16 1 inline test fn, 1 asserts script py 5 0 partial
    20 t27 scripts/trinity-pellis-pipeline/task1_monte_carlo/monte_carlo_exact_pvalue.py 299 / 202 7+0+0 / 0 1 / 16 1 / 16 none script doc 1 0 partial
    21 trinity-fpga conformance/trinet_mac32_conformance_ax7203.py 431 / 329 16+0+1 / 0 3 / 14 3 / 14 1 inline test fn, 2 asserts script ci 3, py 7 0 partial
    22 t27 tools/trinity_vsa_compat.py 749 / 650 35+0+2 / 0 3 / 12 11 / 50 1 inline test fn script code 1, doc 3 0 partial
    23 trinity-fpga research/synth_check.py 171 / 144 6+0+0 / 0 1 / 12 1 / 12 none script py 1 0 partial
    24 trinity-fpga conformance/fp8_ref.py 295 / 238 9+13+1 / 2 1 / 11 1 / 11 1 inline test fn, 1 asserts script py 22, amb 1 1 partial
    25 trinity deploy/trinity-nexus/docs/book/generator/book_generator_v2.py 286 / 260 5+0+0 / 0 2 / 11 2 / 11 none script none found 1 partial
    26 trinity docs/book/book/generator/book_generator_v2.py 286 / 260 5+0+0 / 0 2 / 11 2 / 11 none script none found 1 partial
    27 trinity deploy/trinity-nexus/docs/book/generator/templates.py 448 / 397 4+0+0 / 0 1 / 10 1 / 10 none script none found 1 partial
    28 trinity docs/book/book/generator/templates.py 448 / 397 4+0+0 / 0 1 / 10 1 / 10 none script none found 1 partial
    29 trinity deploy/trinity-nexus/docs/book/generator/book_generator.py 420 / 338 8+0+0 / 3 2 / 10 2 / 10 none script none found 1 partial
    30 trinity docs/book/book/generator/book_generator.py 420 / 338 8+0+0 / 3 2 / 10 2 / 10 none script none found 1 partial

    Files ranked by the lines of their shape-subset (S) functions (top 15 of 275)

    # repo path lines / code fns f+m+n / classes T fns / lines S fns / lines oracle entry callers copies verdict
    1 trinity-fpga fpga/witness/gf_decode/rtl_bit_model.py 423 / 317 13+0+0 / 0 0 / 0 6 / 169 1 asserts script py 1 0 partial
    2 trinity-fpga formal/verify_adder_oracle.py 329 / 259 6+0+0 / 0 0 / 0 3 / 144 none script ci 1 0 partial
    3 t27 tools/gft_train_demo.py 201 / 168 17+0+1 / 0 0 / 0 16 / 130 none script py 1, doc 3 0 near-whole (S)
    4 trinity-fpga conformance/corona_decode_host_ax7203.py 381 / 304 16+0+0 / 0 0 / 0 8 / 129 1 inline test fn script ci 1, sh 3, py 2 0 partial
    5 t27 tools/gft_backprop_microcode.py 967 / 684 19+0+24 / 0 0 / 0 13 / 114 1 inline test fn, 38 asserts script ci 1, py 4, code 1, doc 10 0 partial
    6 trinity archive/experiments/golden_key_proof.py 442 / 317 12+0+0 / 0 8 / 96 8 / 96 none script amb 1 3 partial
    7 trinity-fpga conformance/gf96_bitexact_oracle.py 326 / 249 7+0+0 / 0 0 / 0 2 / 69 none script none found 0 partial
    8 t27 tools/trinity_tri_api.py 2334 / 2004 103+4+13 / 3 5 / 43 9 / 65 1 inline test fn, 1 asserts script py 3, code 4, doc 3 0 partial
    9 trinity-fpga conformance/gf128_bitexact_oracle.py 273 / 226 7+0+0 / 0 0 / 0 2 / 62 none script none found 0 partial
    10 trinity-fpga conformance/gf48_bitexact_oracle.py 295 / 218 9+0+0 / 0 0 / 0 2 / 59 none script py 1 0 partial
    11 trinity-fpga conformance/gf256_bitexact_oracle.py 253 / 222 7+0+0 / 0 0 / 0 2 / 59 none script none found 0 partial
    12 trinity-fpga conformance/gf512_bitexact_oracle.py 223 / 196 7+0+0 / 0 0 / 0 2 / 59 none script none found 0 partial
    13 trinity-fpga conformance/gf1024_bitexact_oracle.py 221 / 194 7+0+0 / 0 0 / 0 2 / 59 none script none found 0 partial
    14 trinity-fpga conformance/top_decode_conformance_ax7203.py 134 / 110 7+0+0 / 0 0 / 0 4 / 55 none script py 1 0 partial
    15 trinity archive/experiments/trinity_benchmarks.py 389 / 283 11+9+0 / 2 6 / 51 6 / 51 none script none found 3 partial

    Reading the ranked tables: lines / code are physical and code lines; fns f+m+n are module-level functions, methods and nested defs; classes are module-level classes; T and S are the distinct-file's own in-subset function counts and spans; oracle lists test files that import the module, inline test functions, assert statements in the file, doctests; entry is script when the file has an if __name__ guard; callers counts other files: ci workflows and other YAML, sh shell, Makefile, Dockerfile, py other .py files that import it or name it, code Rust, t27, Zig, TypeScript, JavaScript, Go and C sources, doc Markdown and text, amb mentions by a basename that other files of the repository share; copies is the number of other paths or repositories with byte-identical content (counted once in the table and in the distinct totals). For trios, trinity, trinity-fpga and 999-multibots-telegraf only the checked-out file types were searched for callers (.py, shell, YAML, Makefile, Dockerfile, package.json, pyproject.toml), so code and doc are undercounts there.

    First blockers (distinct function bodies)

    first blocker distinct fns lines share of fns
    stateful class method 1,195 22,315 14.1%
    io module 973 37,553 11.4%
    import use 968 18,421 11.4%
    owned string 717 25,750 8.4%
    param type 624 15,805 7.3%
    nested def 588 9,791 6.9%
    dict/set 427 12,671 5.0%
    return type 353 7,480 4.2%
    shape fits, parameter untyped 349 3,278 4.1%
    io builtin 343 15,601 4.0%
    module state 333 7,684 3.9%
    method outside table 245 2,678 2.9%
    decorator 191 1,791 2.2%
    attribute 178 3,767 2.1%
    • Methods of classes with state are the largest wall (1,195 distinct functions, 14.1%), then imports of a module outside the allowed five (1,941, 22.8%; numpy 259, argparse 231, os 183, sys 179, random 105, re 102, serial 95, subprocess 94, torch 77, fractions 65), owned strings (717, 8.4%), annotations outside the grammar (977), nested defs (588) and dict/set (427).
    • 349 distinct functions (3,278 lines) pass every structural rule and fail only because a parameter has no annotation and no literal default: that is the whole gap between T and S, and what trace-based typing (t27o: Python dynamics -- a static subset with trace-based type inference (RightTyper, MonkeyType, Codon) #9006) would buy.
    • Input and output dominate: imports of other modules, io builtin (343), with and try (226) are the first blocker of 29.5% of the distinct functions.

    The functions that fail for exactly one category (allowing that category alone would admit them):

    the only blocker distinct fns lines
    owned string 166 2,075
    decorator 88 268
    dict/set 76 649
    generator expression 64 477
    method outside table 57 238
    attribute 40 510
    module state 39 292
    import use (pure stdlib module) 35 617
    nested def 31 117
    optional return 25 355
    stateful class method 25 166
    import use 19 133

    First whole-file candidate

    • Distinct files of 150 lines or more: 737. With every function in T: 0. With every function in S: 1 (archive/experiments/kinetic_sacred_pure.py, which runs 82 module-level statements and is therefore not whole). Files of any size with every function in S: 4 (tools/ternary_model.py (93 lines), archive/experiments/kinetic_sacred_pure.py (256 lines), parameter_golf_gf8_ablation/gfplus_line/testE_v3.py (57 lines), research/arxiv_tnf/gen_figures_prior.py (129 lines)); only the first has no module-level effect (the others run 82, 21 and 46 module-level statements).
    • Largest share of a 150-line-plus file inside S: tools/gft_train_demo.py 16 of 18 fns, 130 of 201 lines, formal/verify_adder_oracle.py 3 of 6 fns, 144 of 329 lines, fpga/witness/gf_decode/rtl_bit_model.py 6 of 13 fns, 169 of 423 lines, conformance/corona_decode_host_ax7203.py 8 of 16 fns, 129 of 381 lines.
    • Near-whole in S (everything but main fits): 5 files, tools/gft_train_demo.py (201 lines), conformance/binary256_decode_conformance_ax7203.py (91 lines), conformance/bram_test_conformance_ax7203.py (43 lines), research/arxiv_tnf/measurements/gen_topaligned_cost.py (71 lines), research/block/verify_phase_theorem.py (146 lines). Only tools/gft_train_demo.py is above 150 lines, and it is imported by tools/gft_deep_demo.py (six of its functions) and described in docs/GFT_TRAINING_DEMO.md; it has a spec twin specs/port/tools/gft_train_demo.t27 and no test.
    • Already ported: in t27, 57 of 596 scanned .py files have a spec of the same path under specs/port/ (published by the port bees, 93 docs/now/*-published-port-*-py-python-* entries, 92 of which name a script; that script is still tracked for 41 of them); 7 of the 76 t27 files that hold S functions have one (204 S lines). Equivalence of those twins was not checked here.

    Hand check (30 functions)

    Method. Sample drawn with a seeded generator (seed 20261021) from the functions of the distinct files (byte-identical copies counted once): 15 from the in-subset (T) set, 4 uniformly from the out set, 5 from the shape-fits-but-untyped set, 6 from out functions that have exactly one violation category (the borderline ones, where a scanner bug would flip the verdict). The 30 were shuffled, printed as source with their imports and no verdict, and classified by hand against R1 to R10 (the callee, class and module constant were looked up when the rule needed them); the verdicts were written to a file, then compared with the scanner's.

    • Round 1 (seed 20261020, scanner before the fix): 29 of 30 agreed. The disagreement was extract_field in tools/trinity_tri_api.py, which returns None on some paths and bytes on others: a union that the scanner passed because it checks annotations only. Rule optional return (R2) was added; one more rule expected to matter was added in the same edit, not found by the sample (a module list mutated through TABLE.append or TABLE[i] = x is module state). The classification of the 12,547 functions moved by 20 S and 6 T functions.
    • Round 2 (seed 20261021, final scanner, the table below): 30 of 30 verdicts agree on both tiers. In-class precision 15 of 15 (95% one-sided lower bound 82%); out-class 15 of 15; first-blocker reason agrees in 14 of 15 out functions (in accumulate the scanner found an import I had missed; the verdict agreed).
    • What the hand check does not show: the sample is small; the rules and the check are by the same author; recall is measured on 15 out functions (4 uniform, 5 shape-fits-but-untyped, 6 one-category), not over the 8,400 out functions.

    Callers and entry points

    • Of the 64 distinct files that hold a T function: 62 are scripts (an if __name__ guard), 9 are run or named by a workflow, shell script, Makefile or Dockerfile, 15 are imported by another .py, and 29 have no reference found at all. Of the 275 files that hold an S function: 224 are scripts.
    • A port has to keep the entry point of a script (python3 path.py in a workflow, a shell script or a doc) and the import surface of a module (from tools.x import f). t27c gen-python cannot give back a function: bootstrap/src/codegen_python.rs states that a fn is "announced, not stubbed", and its test asserts the output contains no def . The only existing way for Python to call spec logic is a hand-written loader: scripts/tri_loop/t27b_rules.py (315 lines) compiles gen/c/tri/t27b/steward.c and calls it with ctypes. Of the 105 T bodies, 60 (361 lines) are flat numeric code with no list growth, no tuple value and no element write; the others use at least one of those, which a plain scalar C signature does not carry (my reading, not measured).
    • What a caller would call after a port: for the module-level pure functions, the same name through a loader shim that is itself hand-written Python and counts against the foreign-line budget (no net-negative diff unless the functions deleted outnumber the shim); or, if the caller is a script that only runs the spec, tri/t27c directly with the caller changed. With a median in-subset function of 6 lines, a shim of 15 to 40 lines costs more than it deletes unless several functions of one file move together. This is the reason to measure a file before choosing it, and why the whole-file number above is the one that counts.

    What this does not establish

    • Not run: no t27c, no cargo, no tri build, no converter. There is no Python front end yet, so "fits the subset" means "passes the written rules", not "converts and verifies". t27c gen-python was read, not executed. No oracle replay and no equivalence check of any port twin.
    • The subset is ours (t27o: Python dynamics -- a static subset with trace-based type inference (RightTyper, MonkeyType, Codon) #9006 may choose another). T and S, the import list, and the treatment of list growth and tuples are judgment calls; the first-blocker histogram and the "only blocker" table above show how far each choice moves the number.
    • Near-duplicates (copies that differ by a few bytes) are collapsed only when their function bodies are identical by ast.dump; files are collapsed only when byte-identical.
    • Callers in the four sibling repositories were searched in the checked-out file types only; basename matches are labelled amb when another file of the same repository shares the name. Dynamic imports (importlib, runpy) are found only when the path or file name appears as a string token.
    • The local checkouts under the user's home were not used (stale feature branches); the numbers are for the pinned commits. A run on a later master will differ.
    • The hand check was done by the author of the rules; an independent reviewer would be a stronger check.
  2. added a commit that references this issue on Oct 19, 2026
    0aeee2a
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions