From 18f3c101b2c5487781a09deb298d09dde0b80d11 Mon Sep 17 00:00:00 2001 From: Timothy Place Date: Fri, 2 Oct 2026 19:48:10 +0000 Subject: [PATCH 1/5] Design and plan tap.python, a Python class as a Max object without audio MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit docs/TAP-PYTHON-PLAN.md: the decisions (D7-D11), the class contract — what a method returns is what the object outputs, its return hint the outlet count — the thread model, what changes in the core, the shared glue, the package, and the plan as Phase 9 of the production plan, which now points to it. The new plan is kept out of the shipped package like the others, and CLAUDE.md names it. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01WnyasLjC6poziMpwguw4in --- CLAUDE.md | 5 +- docs/PRODUCTION-PLAN.md | 28 ++++ docs/TAP-PYTHON-PLAN.md | 284 ++++++++++++++++++++++++++++++++++++ scripts/assemble-package.py | 5 +- 4 files changed, 319 insertions(+), 3 deletions(-) create mode 100644 docs/TAP-PYTHON-PLAN.md diff --git a/CLAUDE.md b/CLAUDE.md index f7fe930..dfcbfcb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -9,7 +9,10 @@ user's Python class as an audio object. `[tap.python~ name]` loads `python/name. `class name`, turns its annotated public fields into Max attributes and its public methods into Max messages, calls its `process()` on the signal, and hot-reloads on save. `ReadMe.md` is the user-facing contract; **`docs/PRODUCTION-PLAN.md` is the authoritative roadmap** — its settled decisions (D1–D6), -its phases, and the audit findings behind them. Tick its items (with the PR) as they land. +its phases, and the audit findings behind them. Tick its items (with the PR) as they land. Its +Phase 9 is a second object, **`tap.python`**, a Python class as a Max object without audio (what a +method returns is what it outputs): designed in `docs/TAP-PYTHON-PLAN.md` (decisions D7–D11), not +built yet. ## Layout (D6: a host-independent core plus a thin Max wrapper) diff --git a/docs/PRODUCTION-PLAN.md b/docs/PRODUCTION-PLAN.md index 4add8b4..b0a3909 100644 --- a/docs/PRODUCTION-PLAN.md +++ b/docs/PRODUCTION-PLAN.md @@ -33,6 +33,7 @@ note the PR that closed them. | D4 | Python version policy | **Pin 3.13; upgrade deliberately.** One CPython minor per release; a move (e.g. to 3.14's deferred annotations) is its own PR with tests. No free-threaded or subinterpreter builds until numpy supports them. | | D5 | Compatibility before 1.0 | **Breaking changes to the class contract are allowed** where they buy correctness (reserved names, file-based loading, signature dispatch). Each is recorded in a `CHANGELOG.md`; the shipped examples are updated in the same PR. | | D6 | Architecture | **A host-independent core plus a thin Max wrapper**, the family's kernel/wrapper split (TapTools / TapTools-Max). Everything that talks to CPython — interpreter start-up, thread state, module loading, class introspection, value conversion, `process()` binding and reload, exception handling — lives in `core/` (`tap::python`, plain C++20 + CPython, no Max or min-api). The external maps the core's attribute and message descriptions onto `object_addattr`/`object_addmethod` and owns only Max concerns (package paths, the file watcher, atoms). Linux is the first test platform *for the core*: real audio/main threads and sanitizers in CI and in cloud sessions. A plugin front end (CLAP or VST3) is optional later work over the same core (Phase 7), not a test vehicle — the Max glue still needs its own tests. | +| D7–D11 | A second object, `tap.python`, without audio | **Decided 2026-10-02 in `docs/TAP-PYTHON-PLAN.md`** (Phase 9 below): the same core, folder, loader and class contract minus audio; a method's return value is the output, its return hint the outlet count; one inlet; a message runs on the thread it arrives on; the shared Max glue moves to `source/shared/`. | ## Phase 0 — the core split and a test foundation that can fail @@ -756,6 +757,30 @@ draft until signing exists, as `release.yml` has it. parameter lists — e.g. a fixed bank of N parameters the Python class declares — and to sharing one interpreter with other plugins that embed Python in the same host process. +## Phase 9 — `tap.python`, a Python class as a Max object without audio (after 1.0) + +The design — decisions D7–D11, the class contract, the output rules, what changes in the core, the +glue and the package — is `docs/TAP-PYTHON-PLAN.md`; keep it current where a PR decides +differently. One PR per item, in this order, each against tests that fail before it: + +- [ ] **9.1 The core:** output items and their conversion in `value.h`; `message_info::return_count`, + `outlet_count()`, `call()` returning the output; the `bind_audio` option; a last parameter hinted + `list[…]` or `np.ndarray` taking the remaining atoms (both objects); the `Loaded` line per + binding. `test_output.cpp`. `tap.python~` unchanged and green. +- [ ] **9.2 The shared glue:** the attribute, message, trampoline, file-watcher and package headers + to `source/shared/tap/python_max/`, templated on the host object; the runtime CMake block to + `source/cmake/embedded-python.cmake`; `style.yml` follows. A pure move. +- [ ] **9.3 The object:** `source/projects/tap.python/` — ports with a dumpout, the output mapping, + `anything` forwarding, dynamic outlets on reload, reserved names, `MIN_DESCRIPTION`; the glue + test; the examples (`euclid.py`, `scale.py`, `note_name.py`, `default.py`'s `bang`); CI checks and + packaging over both externals; CHANGELOG 1.1.0 started. +- [ ] **9.4 Documentation:** the ReadMe section and output table, CLAUDE.md, the help patcher, the + reference page. +- [ ] **9.5 Runtime tests:** the `tap.python.*` patchers in `make_patchers.py`; `run.py` aware of + both externals. +- [ ] **9.6 The Mac session:** the whole suite for both objects, the help patcher checked and + re-saved, Max's pages committed, the hand checks, then tag `v1.1.0`. + ## Sequencing (one PR each) 1. Phase 0.1–0.2 — the core split and the Linux battery; then 0.3–0.6. @@ -771,6 +796,9 @@ draft until signing exists, as `release.yml` has it. 11. Phase 8 — the audit's findings, in the order above: 8.1 (docs and hardening, first, so the ReadMe stops overclaiming while the fixes land), 8.2, 8.3, 8.4, 8.5, 8.6, 8.7 (#35), then the Mac session 8.8 — decided on 2026-10-01 to follow 1.0.0 rather than gate it. +12. Phase 9 — `tap.python`: 9.1 (the core, no Max change), 9.2 (the shared glue, a pure move), + 9.3 (the object), 9.4 and 9.5 (docs and runtime tests, independent of each other), then the + Mac session 9.6 and `v1.1.0`. After 8.8, or beside it: nothing in it touches what 8.8 checks. ## External prerequisites diff --git a/docs/TAP-PYTHON-PLAN.md b/docs/TAP-PYTHON-PLAN.md new file mode 100644 index 0000000..319e94c --- /dev/null +++ b/docs/TAP-PYTHON-PLAN.md @@ -0,0 +1,284 @@ +# tap.python — a Python class as a Max object without audio: design and plan + +`tap.python~` runs a Python class as an audio object. `tap.python` runs one as an ordinary Max +object: messages in, messages out, no signal. This is its design — the decisions (D7–D11, +continuing `PRODUCTION-PLAN.md`'s D1–D6), what a class looks like, how the core and the package +change — and the plan to build it (Phase 9 of the production plan, which points here). Drafted +2026-10-02 against 1.0.0; nothing in it is built yet. Tick the items below (with the PR) as they +land, and keep the design current where a PR decides differently. + +## What it is for + +In Max, most objects are not audio objects: they transform messages — a list comes in, a list goes +out; a bang asks for the next value; an int becomes a name. Max gives three ways to write such an +object in a language: `js`, `node.script`, and a C external. `tap.python` is a fourth, with what the +`tap.python~` contract already gives — Python's numeric stack (numpy, and whatever is installed into +the runtime), attributes and messages from type hints, hot reload on save, tracebacks in the +console, and no way for the class to take Max down — and one thing more that an audio object does +not need: **what a method returns is what the object outputs.** + +```python +from attrs import define, field + +@define +class euclid: + steps: int = field(default = 8) + pulses: int = field(default = 3) + + def bang(self) -> list[int]: + return [int((i * self.pulses) % self.steps < self.pulses) for i in range(self.steps)] +``` + +``` +[tap.python euclid] ← loads python/euclid.py; bang outputs a list of 0s and 1s; @steps and @pulses +``` + +Two things it is not. It is not a replacement for `js`: no access to the patcher, the Max API or +UI; it is for computing. And it is not an async object like `node.script`: a message runs to +completion, on the thread that sent it, and its result is output before the message returns — the +ordinary Max object model, which is what makes it composable with `trigger`, `metro` and the rest. + +## Decisions + +| # | Decision | Choice | +|---|---|---| +| D7 | What `tap.python` is | **A second external in the same package, over the same core, with the same class contract minus audio, plus output.** Same `python/` folder, same loader (a file saved once is executed once however many objects of either kind share it), same attributes, messages, hot reload, console and error guards. A class written for one object loads in the other: in `tap.python`, `process()` and `prepare()` are ordinary methods (sending `process 0.5` calls it and outputs the result — a way to test a filter sample by sample); in `tap.python~`, a method's return value is dropped, as now. | +| D8 | How output works | **A method's return value is output from the object's outlets, by its type; its return *hint* fixes how many outlets, at bind time.** No injected outlet API in 1.x (nothing for the class to import, so it still runs in a notebook): `None` outputs nothing; a number or a string one atom; a sequence a list; a method hinted `-> tuple[A, B]` outputs one value per outlet, right to left. The object has as many outlets as the widest return hint among its methods (at least one), plus a dumpout outlet at the right, as Max objects with attributes do; a save that changes the count changes the outlets in place, as 2.4 does for `tap.python~`. The rules are in [Output](#output-what-a-method-returns). | +| D9 | Inlets | **One inlet in the first release.** Messages are the methods; state is the attributes (`steps 8`, `@pulses 3`, attrui), which is what a right inlet is for in most Max objects. More inlets are a later option (how, in [Later](#later-not-planned)), not a 1.x promise. | +| D10 | Threads | **A message runs on the thread it arrives on — Max's main thread, or the scheduler thread under Overdrive — holding the GIL until it returns, and outputs on that thread.** As every ordinary Max object does, and as `tap.python~`'s messages already do. No worker. The honest limits: a long computation in a message from a `metro` holds Max's scheduler for its duration (send it through `deferlow` to run on the main thread instead); and while a message runs, a `tap.python~` in direct mode waits for the GIL in 0.5 ms slices (2.6) — worker mode (2.5) is the answer when both are in one patch and the computation is heavy. | +| D11 | Code layout | **The Max glue the two objects share moves out of `tap.python_tilde/` into `source/shared/`, templated on the host object; the core gains one option.** The dynamic attribute and message registration, the C trampolines, the file watcher and the package paths are the same for both; min's `SUBDIRLIST` makes a target per folder of `source/projects/` only, so a shared folder beside it is safe. The core's `processor` takes `bind_audio` (true for `tap.python~`): with it off, `process` and `prepare` are plain methods and nothing is prepared. Behavior-preserving for `tap.python~`: its tests do not change. | + +## The class contract + +Everything the ReadMe's *Writing a class* says holds, less the *Audio*, *Worker mode* and *Audio +settings* paragraphs, plus output: + +- **Loading.** `[tap.python name]` loads `python/name.py` and instantiates `class name`; no + argument loads `default`. File-based loading (D2), helpers (8.5), UTF-8 (6.6), one execution per + save shared by every object of either kind (3.6, 6.7), one report per broken save (6.10). +- **Attributes** — as `tap.python~` (3.2, 8.6). With a dumpout outlet, `getsteps` outputs + `steps 8` from it, as a Max object with attributes does. +- **Messages** — public methods, called by signature (3.1). `int`, `float`, `symbol`, `bang` and + `list` answer those standard messages; a method named `anything` answers every message the class + has no method for, with the selector as its first argument: `def anything(self, selector: str, + *args)`. (In `tap.python~`, `anything` is reserved; here the object forwards it.) A parameter + hinted `list[float]`, `list[int]`, `list[str]` or `np.ndarray`, last in the signature, takes all + the remaining atoms as one list (or a float64 array) — `def list(self, values: np.ndarray) -> + np.ndarray` is a list in and a list out. This last mapping lands in the core, so `tap.python~`'s + messages get it too. +- **Output** — below. +- **Hot reload, errors, console** — as `tap.python~`: a save reloads keeping attribute values; a + broken save is reported once and the object outputs nothing until a save fixes it; an exception + in a method prints its traceback and the message outputs nothing; `sys.exit()` is reported, not + honored; what is below Python (`os._exit()`, a crashing extension, an endless loop — which + freezes the thread the message came on) is outside the guard (8.1). +- **Reserved names** — Max's and min's own (8.2's list and the `answered_by_max()` guard), without + the audio ones: `dsp`, `dsp64`, `dspsetup`, `dspstate`, `inputchanged`, `multichanneloutputs`, + `signal`, `mode`, `latency` and `latencysamples` are free; `anything` is taken by the forwarder + above, as a method a class may define. + +### Output: what a method returns + +A method's return value goes out of the object, converted by the rules below, before the message +returns. The rules reproduce how Max objects and `js` behave, so a patch sees what it expects. + +| The method returns | The object outputs | +|---|---| +| `None` | nothing | +| `bool` | an int, 0 or 1 | +| an `int` (anything with `__index__`: `np.int64`, an `IntEnum`) | an int (`t_atom_long`, 64-bit; a value past it is reported) | +| a `float` (anything with `__float__` and no `__index__`: `np.float32`) | a float (non-finite values pass through, as `js` passes them) | +| a `str` | the message named by it, with no arguments (`"hello world"` is one symbol, as in `js`) | +| a sequence (`list`, a `tuple` with no `tuple[…]` hint, a 1-D `np.ndarray`, any non-`str` iterable) | a list, each element an atom by the rules above; if the first element is a `str`, the message it names with the rest as arguments (Max's own rule: `["note", 60, 100]` outputs `note 60 100`); an empty sequence outputs nothing; an element that is not an atom (`None`, a nested sequence) is reported and the list is not output | +| a sequence of *n* values, from a method hinted `-> tuple[…]` with *n* members | one value per outlet, outlets *n*…1, right to left, each by these rules (`None` in a slot outputs nothing from that outlet); a result that is not a sequence of exactly *n* values is reported | +| anything else (a `dict`, an object) | reported, with its type; nothing output (a `dict` as a Max dictionary is a later item) | + +The *hint* decides the outlet count when the class loads; the *value* fills them when the method +runs. A method hinted `-> tuple[int, str]` has two outlets, and so does the object if no method is +hinted wider; a method hinted `-> list[float]`, `-> np.ndarray`, `-> float` or not at all has one, +and an unhinted method that happens to return a tuple outputs it as a list from the first outlet. +An unhinted tuple return is a list, a hinted one is several outlets: this is the one place where +the hint changes what a value does, so the ReadMe says it in those words. A `tuple[…]` hint of +unsaid length (`tuple`, `tuple[int, ...]`) cannot name an outlet count: the method is not exposed, +and the console says why (as 2.4 does for `process()`). + +A `print()` in a method posts to the console at once from the main thread and a moment later from +the scheduler thread (8.4); it is not output. + +### Examples + +`default.py` stays shared by both objects and gains a `bang` that returns the gain, so +`[tap.python]` with no argument outputs something. New examples, each a thing Max users write by +hand today: + +- `euclid.py` (above): a Euclidean rhythm from two attributes, a list out of a bang. +- `scale.py`: a list in, scaled and offset by attributes with numpy, a list out — `def list(self, + values: np.ndarray) -> np.ndarray`. +- `note_name.py`: an int in, two outlets out — `def int(self, note: int) -> tuple[str, int]` + returns the pitch class and the octave, so `60` outputs `C` and `4`. + +## Threads and the GIL + +Nothing new is load-bearing, and that is the point of D10. A message takes a `gil_lock` on the +thread it arrives on (`processor::call()` already does), converts the arguments, calls the method, and +converts the result to atoms, all under the GIL; the `outlet_*` calls come after the GIL is +released. Not for reentrancy — `gil_lock` is `PyGILState_Ensure`, which nests, so a downstream +object sending a message back into this one (a loop through `trigger`) simply takes it again on the +same thread — but because an outlet call runs the whole downstream chain before it returns, and +holding the GIL across it would keep every other Python thread (a `tap.python~`'s audio thread in +direct mode) waiting for a cascade of Max objects that has nothing to do with Python. Reloads stay +on the main thread; a message that arrives on the scheduler thread while a reload runs waits for +the GIL, as a `tap.python~` message does now. +Output from a thread Python started (`threading.Thread` in the class) is not supported: a method +runs and returns on Max's thread, and there is no outlet to reach from anywhere else — stated in +the ReadMe as an honest limit, with the later item that would change it. + +## The core + +`core/include/tap/python/` grows, host-independent and tested on Linux first, as everything else +did (D6): + +- **`value.h`** — beside `value` (one atom): `output_item`, a message to output — a selector or + none, and its atoms (what `outlet_anything` / `outlet_list` / `outlet_int` take) — and + `output`, the per-outlet items of one call. The conversion from a Python object to an + `output_item` lives in the core (the rules above), with its errors as diagnostics through the + processor's log, so the Max side only maps items onto `outlet_*` calls. +- **`processor.h`** — `message_info` gains `return_count` (from `describe()`'s `return_shape`, + which the signature already carries); `outlet_count()` is the widest among the messages (at least + one); `call()` returns the converted `output` (empty for `None` or a failure) instead of a bool, + with a `call()` overload keeping the old shape for `tap.python~`; a `processor_options` (or a + constructor flag) `bind_audio` — off, `process`/`prepare` are bound as messages, `has_process()` + is false, `prepare()` is a no-op; argument conversion for a last parameter hinted `list[…]` or + `np.ndarray`. The `Loaded` line (6.7) says what the loading object bound — "4 messages, 2 + outlets" for a `tap.python`, "process() bound, one call per vector" for a `tap.python~` — so + with a file shared by both kinds it describes whichever ran the save; an honest limit of 6.7's + rule, written down. +- **Tests** — `test_output.cpp`: every row of the table above, from a fixture class whose methods + return each kind; the outlet count from the hints; an unhinted tuple as a list against a hinted + one as outlets; an unsaid-length tuple hint not exposed; the `list[…]`/`np.ndarray` parameter; + `bind_audio` off making `process` a message; a method raising outputs nothing and reports; a + call from a second thread racing a reload on the first (as `test_threads.cpp` does for audio). + +## The Max object + +`source/projects/tap.python/` — `tap.python.h`, `tap.python.cpp`, `tap.python_test.cpp` — a min +`object<>` without `vector_operator<>`: + +- **Ports.** One `inlet<>`; `outlet<>`s for the class's `outlet_count()` plus a last `outlet<> + m_dumpout{this, "dumpout"}` stored in the obex (`object_obex_store(maxobj(), gensym("dumpout"), + …)`) so that `get` outputs from it. On a reload that changes the count, dynamic outlets as + 2.4 does, without `dsp_resize`: between the box's `dynlet_begin`/`dynlet_end`, `outlet_delete` + for the surplus and `outlet_insert_after` the last value outlet for the new ones (never + `outlet_append`, which would land them after the dumpout), min's lists following; without a box, + the object keeps its outlets and says so once. *To check in Max:* that min's `outlet<>` named + `dumpout` is enough for `get` or whether the obex store is needed (min stores one for jit + objects only, `max_jit_class_wrap_standard`). +- **Messages.** The shared `python_message` registration; `message_gimme()` calls the processor and + maps each `output_item` onto `outlet_int`/`outlet_float`/`outlet_anything`/`outlet_list` on the + right outlet, right to left. A min `message<> m_anything{this, "anything", …}` forwards an + unknown selector with its atoms to the class's `anything` method when it has one, and otherwise + posts that the object does not understand it (Max's own wording). +- **Reserved names.** 8.2's list less the audio names, and the `answered_by_max()` guard; `anything` + is answered by the object (the forwarder), so it must not be reserved by the guard — the guard + excludes it by name, and a glue test asserts a class's `anything` is exposed. +- **The rest** is the shared glue: the file watcher (`filechanged`, 6.1's nobox helper), the console + (8.4's qelem), the package paths, attributes (3.4's reconciliation), `reserved_messages()` and + `answered_by_max()`. `MIN_DESCRIPTION` is the contract (5.1): min writes + `docs/tap.python.maxref.xml` from it. +- **Glue test** (mock kernel): `[tap.python euclid]` has one inlet, two outlets (one plus dumpout); + a bang's list is in `object_getoutput(maxobj, 0)` (the mock records outlet sequences); a + `note_name` int fills outlet 1 then outlet 0; a str return arrives as an `anything`; `getsteps` + reaches the dumpout (if the mock routes it; else a runtime test); a `tuple[…]` save that widens + the class records the dynlet calls as 2.4's test does. + +## The package + +- **Build.** The runtime discovery block of `tap.python_tilde/CMakeLists.txt` (support/, weak link, + delay-load, rpaths per slice, bundle identifier, the `.mxo` touch) moves to + `source/cmake/embedded-python.cmake`, included by both objects' `CMakeLists.txt`; each object's + stays a page. Both externals build and test on Linux (the mock kernel), macOS and Windows. +- **CI.** `build.yml`: the data-import check (4.2), the bundle identifier, `lipo`/`otool` and the + rpath checks, and the Windows delay-load check run over both externals (a loop over + `externals/`, so a third would need nothing). `style.yml`: both objects' TUs in the clang-tidy + list and its header filter, and `source/shared/` added to clang-format's file list (today it + lists `source/projects/` and `core/` only). `scripts/tidy.sh` is TapHouse's and takes a repo's + own TUs — unchanged. +- **Packaging.** `assemble-package.py`'s `EXTERNALS` becomes a list per platform; `--merge` already + copies every external it finds. `package-info.json.in`'s description names both objects. The + release zips carry both; nothing else in `release.yml` names an external. +- **Docs.** The ReadMe: a `tap.python` section after the audio one — the loading line, the output + table, the honest limits (D10, threads, no output from Python's own threads, no dictionaries + yet) — and its performance note is one sentence: the cost is the method's Python, plus the + `call()` bridge measured once by `core/bench`. `help/tap.python.maxhelp`, by hand in Max, with + the three examples. `docs/tap.python.maxref.xml` from min (6.8's rule: commit Max's). CLAUDE.md: + the second object in the layout and the shared glue. `CHANGELOG.md`: 1.1.0 — a new object, no + change to `tap.python~`'s contract beyond the `list[…]` parameter, which only adds. +- **Runtime tests in Max** (`runtime-tests/`): `make_patchers.py` gains a non-signal `python()` + box; new patchers `tap.python.*.maxtest.maxpat`: load (no argument, each example); every output + row through `[print]`-free checks (a list into `test.assert`, an `anything` through `route`, two + outlets in order through `trigger`); `getsteps` from the dumpout; a save that widens the return + hint changes the outlets and the new one's cord carries (2.4's `channels` test, for control + outlets); one file shared by a `tap.python~` and a `tap.python`, saved: one `Loaded` line, both + reload; a message from a `metro` under Overdrive (the scheduler thread) outputs correctly and in + order; `sys.exit()` and an exception reported, the object alive; `anything` forwarded. The Mac + session that runs them is the phase's last item, with the help patcher. + +## Plan — Phase 9 of the production plan + +One PR each, in this order; each lands against tests that fail before it. + +- [ ] **9.1 The core: output, outlets and the audio option.** `value.h`'s `output_item`/`output` + and the Python-to-output conversion; `message_info::return_count`, `outlet_count()`, `call()` + returning the output; `bind_audio`; the `list[…]`/`np.ndarray` final parameter (for both + objects); the `Loaded` line per binding. `test_output.cpp` and fixtures. No Max code changes; + `tap.python~`'s battery and glue test unchanged and green, the bench numbers unchanged (the + audio path does not touch the new code). +- [ ] **9.2 The shared glue.** `tap.python_tilde_{attribute,message,cglue,filewatch,package}.h` + move to `source/shared/tap/python_max/` as `python_glue` (the trampolines instantiated in + each object's `.cpp` through `wrapper_find_self`); the runtime CMake block to + `source/cmake/embedded-python.cmake`; `style.yml`'s list and header filter follow. Pure move: + `tap.python~`'s behavior, tests and the data-import check unchanged. *Decide in the PR:* whether + `reserved_messages()` and `answered_by_max()` move too, parameterized by the audio names, or each + object keeps its list (the audio names are the only difference). +- [ ] **9.3 The object.** `source/projects/tap.python/`: ports with the dumpout, output mapping, + `anything` forwarding, dynamic outlets on reload, the reserved names, `MIN_DESCRIPTION`; the + glue test; the examples (`euclid.py`, `scale.py`, `note_name.py`, `default.py`'s `bang`); the + CI checks over both externals; `assemble-package.py` and `package-info.json.in`. CHANGELOG 1.1.0 + started. +- [ ] **9.4 Documentation.** The ReadMe section and the output table; CLAUDE.md; the help patcher + (JSON by hand, as 5.2 was, checked in Max in 9.6); the reference page from min against the mock + kernel (5.1's way), to be replaced by Max's in 9.6. +- [ ] **9.5 Runtime tests.** The patchers above, in `make_patchers.py`, and `run.py` aware of the + second external (its `EXTERNAL` check, the `--package` mode, the reference-page rule for both + pages). +- [ ] **9.6 The Mac session.** Build, run the whole runtime suite (both objects), check the help + patcher and re-save it, commit the pages Max writes, the hand checks the tests cannot make + (`get` through the dumpout in a patcher, attrui on a `tap.python`, a `tap.python~` and a + `tap.python` on one file saved while audio runs), then tag `v1.1.0`. + +## Later (not planned) + +Written down so they are decided rather than rediscovered; none is promised by 1.1. + +- **Dictionaries.** A `dict` return as a Max dictionary out (`dictionary ` through a + `t_dictionary` the object owns), and a `dictionary` message in as a `dict` argument. The natural + next step, and a real design: ownership of the named dictionary, nested values, and `jit`-style + `dictobj` registration. +- **Timers.** A class that wants to run on its own — a sequencer — needs a clock. Two shapes: an + attribute `@interval` ms calling a method `tick()` on the scheduler thread (nothing to import, + consistent with the rest), or a `self`-side API (a `schedule(ms, method)` injected at + construction, which breaks "runs in a notebook"). The first fits; measure a Python call per tick + against Max's own `metro` before promising timing. +- **More inlets.** min's `inlet<>` list makes proxies, and `proxy_getinlet()` says which one a + message came in; a mapping would be `inlets: ClassVar[int]` plus the inlet number as a first + argument to `anything`, or a method per inlet. Not until a use needs it: attributes cover the + cold-inlet idiom. +- **Output from Python's own threads.** A queue the class could post to from a `threading.Thread`, + drained by a qelem on the main thread — `node.script`'s shape. It needs the injected API the + timers item weighs. +- **`buffer~` and `jit.matrix` as numpy arrays.** Attractive and Max-specific: a `buffer~` name as + an attribute, its samples as an `np.ndarray` view under `buffer_locksamples()`. The locking rules + make it its own design. +- **Deferring to the main thread.** An `@defer` attribute running every message on the main + thread (`defer_low`), as `js` effectively does. `deferlow` in the patch does the same today; add + it only if users ask. diff --git a/scripts/assemble-package.py b/scripts/assemble-package.py index 13eae98..6d7cd9e 100644 --- a/scripts/assemble-package.py +++ b/scripts/assemble-package.py @@ -56,9 +56,10 @@ # Never shipped from the copied folders. # maxtest_*.py: the runtime tests' fixtures, copied into python/ while runtime-tests/run.py runs -# PRODUCTION-PLAN.md, AUDIT-*.md: the development roadmap and audits in docs/, beside the reference page Max reads +# PRODUCTION-PLAN.md, *-PLAN.md, AUDIT-*.md: the development roadmap, plans and audits in docs/, beside the +# reference pages Max reads IGNORED = shutil.ignore_patterns("__pycache__", "*.pyc", ".ipynb_checkpoints", ".DS_Store", "maxtest_*", - "PRODUCTION-PLAN.md", "AUDIT-*.md") + "PRODUCTION-PLAN.md", "*-PLAN.md", "AUDIT-*.md") # Each platform's folder for its runtime in a package for every platform (plan 4.8); the external From 26f1ece30f310cb06581fba60c3d76e40a8f7038 Mon Sep 17 00:00:00 2001 From: Timothy Place Date: Fri, 2 Oct 2026 20:18:20 +0000 Subject: [PATCH 2/5] Fix 1.0.0 in Max: every attribute and message was reserved by the host Plan 8.2's guard asked object_getmethod() whether the Max object already answers a Python method's or field's name and took anything but null for yes. For a name the object does not have, Max returns method_false(), a function, as the SDK documents, so in Max every field and method of every class was "reserved by the host" and the object had no attributes and no messages; only audio still worked. The mock kernel answers null, which is why the unit tests passed. found_method() now treats null and method_false() alike as not found. The glue test's kernel answers as Max does (method_false() for an unknown name, a method for the names min registers and for one the test's class defines on purpose), and the reserved-name scenario checks a field and that name: it fails with the null test and passes with the fix. CHANGELOG 1.0.1, the plan's record under 8.2, and CLAUDE.md's rule about reading a Max call's return contract. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01WnyasLjC6poziMpwguw4in --- CHANGELOG.md | 13 +++++++ CLAUDE.md | 5 +++ docs/PRODUCTION-PLAN.md | 10 +++++- .../tap.python_tilde/tap.python_tilde.h | 10 +++++- .../tap.python_tilde_test.cpp | 34 ++++++++++++++++++- 5 files changed, 69 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2dfbbc8..4756ee0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,19 @@ breaking changes to the contract were allowed where they bought correctness (D5 `docs/PRODUCTION-PLAN.md`); each is recorded here. From 1.0.0, a change to the class contract is a major version. +## 1.0.1 — unreleased + +### Fixed + +- **In Max, the object had no attributes and no messages.** 1.0.0's guard against a Python method + or field named like a message the Max object already answers (plan 8.2) asked Max's + `object_getmethod()` and took anything but null for an answer — but for a name the object does + not have, Max returns `method_false()`, a function, as the SDK documents, so every field and + method of every class was "reserved by the host" and the console said so for each; only audio + still worked. The mock kernel the unit tests run against answers null, which is why they passed. + The guard now recognizes `method_false()`, the unit test's kernel answers as Max does, and the + Mac session (8.8) is what would have caught it before the tag. + ## 1.0.0 — 2026-10-01 The first stable release: the class contract as the ReadMe states it, the production plan's diff --git a/CLAUDE.md b/CLAUDE.md index dfcbfcb..6cf1056 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -134,6 +134,11 @@ attaches all the zips + SHA256s to a release — a pre-release for 0.x, a draft which such a call crashes — so the watcher is owned by a nobox helper with the SDK's signature (`tap.python_tilde_filewatch.h`). Check the SDK's calling convention before exposing a Max-called method as a `message<>`. + And read the SDK's return contract before testing a Max call's result: `object_getmethod()` + answers `method_false()`, a function, not null, for a name an object does not have — a null + test reserved every Python name in 1.0.0 (`found_method()` in the object; its glue test's kernel + answers as Max does). The mock kernel is thinner than Max: when a stub decides a behavior, make + the test's stub faithful to the SDK (as `attr_args_offset` and `object_getmethod` are). - **Say what is true of a class once, what is true of an instance per instance.** Many objects can share one class file; only the processor whose `load()` ran the file (`load_script` says so) announces the class — its `Loaded` line and its diagnostics — through `announce()` (plan 6.7). diff --git a/docs/PRODUCTION-PLAN.md b/docs/PRODUCTION-PLAN.md index b0a3909..aa38ef6 100644 --- a/docs/PRODUCTION-PLAN.md +++ b/docs/PRODUCTION-PLAN.md @@ -612,7 +612,15 @@ down rather than discovered again. *This plan was itself audited before being ad cord connected by `thispatcher`, with those methods in the class) — to run in the Mac session (8.8), as is Build Collective for `fileusage`. In the mock kernel `object_getmethod()` always answers null, so the guard's own effect is seen only in Max: the existing - `attributes-and-messages` runtime test is what would show it over-reserving. + `attributes-and-messages` runtime test is what would show it over-reserving. *And it did + over-reserve (found 2026-10-02 in Max, 1.0.0 released with it):* for a name the object does not + answer Max returns `method_false()`, a function — the SDK says so — not null, so the guard + reserved every field and method of every class; the object loaded, audio ran, and nothing else + worked. Fixed in 1.0.1: `found_method()` treats null and `method_false()` alike as "not found", + and the glue test's kernel now answers as Max does (`method_false()` for an unknown name, a + method for the names min registers and for one the test's class defines on purpose), so the + guard's effect is pinned without Max. The lesson for 8.8: a tag with a Mac session outstanding + ships what only Max can show. - [x] **8.3 Worker mode never hangs Max (A1, A6 — `worker.h`).** *A1:* `stop()` bounds its join. The worker records its OS thread identifier (`PyThread_get_thread_ident()`, no GIL needed) as it starts; if the thread has not finished diff --git a/source/projects/tap.python_tilde/tap.python_tilde.h b/source/projects/tap.python_tilde/tap.python_tilde.h index 8f6ade6..85b3cde 100644 --- a/source/projects/tap.python_tilde/tap.python_tilde.h +++ b/source/projects/tap.python_tilde/tap.python_tilde.h @@ -386,6 +386,14 @@ class python : public object, public vector_operator<> { return names; } + /// Whether object_getmethod() found a method. For a name the object does not answer, Max + /// returns method_false() — a function, as the SDK documents — not null; the mock kernel + /// returns null. 1.0.0 tested for null alone, so in Max every field and method of a class was + /// "answered by Max", and the object had no attributes and no messages (1.0.1). + static bool found_method(const c74::max::method found) { + return found != nullptr && found != reinterpret_cast(c74::max::method_false); + } + private: string m_python_source{}; std::filesystem::path m_scripts_dir{}; @@ -477,7 +485,7 @@ class python : public object, public vector_operator<> { if (m_python_messages.find(name) != m_python_messages.end()) { return false; } - return c74::max::object_getmethod(maxobj(), c74::max::gensym(name.c_str())) != nullptr; + return found_method(c74::max::object_getmethod(maxobj(), c74::max::gensym(name.c_str()))); } /// The worker thread's scheduling (plan 2.5): the real-time class an audio thread has, so that a diff --git a/source/projects/tap.python_tilde/tap.python_tilde_test.cpp b/source/projects/tap.python_tilde/tap.python_tilde_test.cpp index 12cf7ea..cb4bdc5 100644 --- a/source/projects/tap.python_tilde/tap.python_tilde_test.cpp +++ b/source/projects/tap.python_tilde/tap.python_tilde_test.cpp @@ -51,7 +51,24 @@ namespace c74 { } return ac; } - void filewatcher_start(void*) {} + void filewatcher_start(void*) {} + // What Max's own does, which the mock's (always null) does not: method_false() for a name + // the object does not answer. 1.0.0 took that for "found" and reserved every Python name + // (plan 8.2's guard, found broken in Max; fixed in 1.0.1). The pretend class answers the + // names min registers for this object, plus one a test's class defines on purpose. + t_atom_long method_false(void*) { + return 0; + } + method object_getmethod(void*, t_symbol* s) { + static const char* const k_answered[] = {"dsp64", "assist", "notify", + "filechanged", "dspsetup", "maxtest_host_answers"}; + for (const auto* name : k_answered) { + if (s == gensym(name)) { + return reinterpret_cast(+[](void*) -> void* { return nullptr; }); + } + } + return reinterpret_cast(method_false); + } void* qelem_new(void*, method) { static int s_qelem; return &s_qelem; @@ -419,11 +436,16 @@ SCENARIO("Methods named like messages Max sends with C arguments are not exposed ext_main(nullptr); const auto file = tap::python::package_root() / "python" / "maxtest_mock_reserved.py"; write_file(file, "class maxtest_mock_reserved:\n" + // a field, and a method, which must be exposed whatever the guard answers + " gain: float = 0.75\n" // what Max calls with C arguments: a Python method of the name would crash Max " def dspstate(self, on: int) -> None:\n pass\n" " def fileusage(self) -> None:\n pass\n" " def patchlineupdate(self) -> None:\n pass\n" " def inputchanged(self) -> None:\n pass\n" + // a name only the guard reserves: the test's object_getmethod answers it as + // Max's would for a method the class registered + " def maxtest_host_answers(self) -> None:\n pass\n" // what the ReadMe promises stays a message, whatever the guard answers " def int(self, n: int) -> None:\n pass\n" " def float(self, x: float) -> None:\n pass\n" @@ -441,6 +463,16 @@ SCENARIO("Methods named like messages Max sends with C arguments are not exposed CHECK(my_object.python_message_names() == std::vector{"bang", "float", "greet", "int", "list", "symbol"}); } + THEN("the field is an attribute: a name Max does not answer is not reserved by the guard (1.0.1)") { + const auto got = get(my_object, "gain"); + CHECK(c74::max::atom_gettype(&got) == c74::max::A_FLOAT); + CHECK(c74::max::atom_getfloat(&got) == 0.75); + } + THEN("the method's name decides nothing by itself: what object_getmethod() answers does") { + CHECK(python::found_method(nullptr) == false); + CHECK(python::found_method(reinterpret_cast(c74::max::method_false)) == false); + CHECK(python::found_method(reinterpret_cast(c74::max::object_getmethod)) == true); + } THEN("audio is bound regardless") { CHECK(all_equal(render(my_object, 0.5), 0.5)); } From 2501bd615d9b4c1e6217e47737d0a6f8d79fa8f4 Mon Sep 17 00:00:00 2001 From: Timothy Place Date: Fri, 2 Oct 2026 20:53:32 +0000 Subject: [PATCH 3/5] Audit the tap.python plan before building it docs/AUDIT-TAP-PYTHON-PLAN.md: two blockers, six major and nine minor findings, each checked against the source. The blockers: two externals each compile in their own copy of the header-only core, and the second one's initialize() aborts the process (reproduced with two shared objects in one process); and the plan verifies Max-only behavior last, as 1.0.0 did. The plan itself is unchanged, pending review. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01WnyasLjC6poziMpwguw4in --- docs/AUDIT-TAP-PYTHON-PLAN.md | 329 ++++++++++++++++++++++++++++++++++ 1 file changed, 329 insertions(+) create mode 100644 docs/AUDIT-TAP-PYTHON-PLAN.md diff --git a/docs/AUDIT-TAP-PYTHON-PLAN.md b/docs/AUDIT-TAP-PYTHON-PLAN.md new file mode 100644 index 0000000..0a31b9d --- /dev/null +++ b/docs/AUDIT-TAP-PYTHON-PLAN.md @@ -0,0 +1,329 @@ +# Audit of the tap.python plan (2026-10-02) + +An adversarial read of `docs/TAP-PYTHON-PLAN.md` as committed in `18f3c10`, before any of it is +built. It asks where the plan is wrong, unverified, inconsistent with itself or the codebase, or +would fail in a real Max the way 1.0.0 did (a guard that the mock kernel passed and Max broke: PR +#37). Two reviewers read it independently, and every finding kept here was checked against the +source by hand. Each is marked **verified** (shown against the code, the SDK headers or a run) or +**unverified** (plausible, but only Max can show it). + +## Verdict + +**Do not start 9.1 as planned.** The design's user-facing contract (return values as output, +hints as outlet counts, one inlet, messages on their own thread) survives the audit. Its +architecture does not. Two findings block: + +- **The second external kills Max** (B1). Two externals each compile in their own copy of the + header-only core, and the second one's start-up aborts the process. +- **The order of work repeats 1.0.0** (B2). The plan builds five PRs before anything runs in Max, + yet most of its riskiest assumptions are behaviors only Max can show. + +Six major findings change the design: outlets, concurrency, `anything`, Scheduler in Audio +Interrupt, announce-once, and the contract. + +| | Blocker | Major | Minor | +|---|---:|---:|---:| +| Findings | 2 | 6 | 9 | + +## Blockers + +### B1. A second external aborts Max when it starts the interpreter (verified) + +**The plan says** (D7, D11): "A second external in the same package, over the same core", with +"the same loader (a file saved once is executed once however many objects of either kind share +it)", and "the core gains one option". + +**The problem.** The core is header-only, and its process-wide state lives in function-local +statics: `initialize()`'s `std::once_flag` (`runtime.h:791`), the support module's globals +(`support_globals()`), the console, the scripts folder, and `init_thread()`. Each external +compiles in its own copy of all of them, but both load the same libpython. The second external's +`call_once` runs again, and calls `PyImport_AppendInittab()` after `Py_Initialize()`. + +**Reproduced here.** Two shared objects, each built with the core and `-fvisibility=hidden`, were +loaded into one process with `dlopen(RTLD_LOCAL)` and each called `initialize()`: + +``` +./ext_a.so: initialize ok=1 +Fatal Python error: PyImport_AppendInittab: PyImport_AppendInittab() may not be called after Py_Initialize() +Aborted (exit 134) +``` + +**On each platform:** + +- **Windows.** Each `.mxe64` is a DLL with its own statics, so this happens as shown: a patch + with both objects aborts Max. +- **macOS.** It depends on whether dyld merges the two bundles' weak inline statics, and only Max + can show which. If dyld does not merge them, Max aborts as above. If it does, the two externals + share the core's state by accident, including each one's console sink and thread checks, which + is no design either. +- **Every test binary links one copy of the core**, so no existing test can see this. + +**Even without the abort**, a second support module means a second source cache. A shared file +would then be executed once per kind, and 6.7 (announce once) and 6.10 (report once) would break +across kinds. + +**It also contradicts the ReadMe**, which calls two embedded copies sharing process-wide state +"untested and unsupported". + +**Recommend.** Decide this in D11 before anything else: + +- **(a) One core, two front ends** (preferred). The core becomes one shared library in the + package (`support/` or beside the externals), which both externals link, so there is one copy of + every static. +- **(b) Attach instead of re-initialize.** `initialize()` detects `Py_IsInitialized()` and attaches + to state the first binary published in `sys.modules`: the support module, the console module and + the sink. The second binary skips `AppendInittab`, pre-initialization and stream rebinding, and + keeps its own thread-state handling. +- **(c) One external.** A single `tap.python~` binary also registers the `tap.python` class (a + second `class_new` in its `ext_main`), if Max can find a class that is not named for its file. + This is the smallest change if Max allows it; the 9.0 spike can say. + +Whichever is chosen, add a core test that starts the runtime from two shared objects in one +process, and a runtime test with both objects in one patch, created in both orders. + +### B2. The order of work repeats the 1.0.0 failure (verified, plan text) + +**The plan says:** 9.1 to 9.5 land first, and "The Mac session that runs them is the phase's last +item" (9.6). The production plan adds that Phase 9 can run "after 8.8, or beside it: nothing in it +touches what 8.8 checks." + +**The problem.** The plan marks one thing "to check in Max" (the dumpout). Yet B1, M1, M2, M3 and +M4 below are all behaviors only Max shows. And 9.2 moves `answered_by_max()`, `found_method()` and +the file watcher into shared code: exactly what 8.8, still open, has to verify. The plan's own +record of 1.0.0 says "a tag with a Mac session outstanding ships what only Max can show." + +**Recommend.** Two changes: + +- **Run 8.8 (with PR #37's fix) before 9.2** moves the guard. +- **Add 9.0, a Max spike, before 9.1.** A throwaway second external, built as B1's chosen design, + next to `tap.python~` in one patch. In Max, confirm: + - both objects start, in either creation order; + - `outlet_insert_after` places an outlet before the dumpout, and patch cords survive it; + - `get` reaches an obex-stored dumpout; + - how a class-level `anything` and instance methods and attributes take turns in dispatch (M3); + - what `object_getmethod()` answers for an unknown name on a class with `anything`; + - which thread a `metro`-driven message runs on with Overdrive on, and with Scheduler in Audio + Interrupt on. + + Write the answers into the plan before 9.1. + +## Major + +### M1. min's outlets cannot do what "Ports" describes (verified) + +**The plan says:** value outlets "plus a last `outlet<> m_dumpout{this, "dumpout"}` stored in the +obex", new outlets by `outlet_insert_after`, "min's lists following". It leaves open "whether min's +`outlet<>` named `dumpout` is enough". + +**The evidence** is in min's outlet header and the SDK: + +- **Order.** An `outlet<>` pushes itself onto the object's list as it is constructed + (`c74_min_outlet.h:312`). A member `m_dumpout` is constructed before the constructor body adds + the class's outlets (as `tap.python~`'s `describe_ports()` does). So the dumpout would be the + second outlet, not the last. +- **No names.** The string is a description; min has no outlet names. Only + `object_obex_store(x, _sym_dumpout, …)` makes `get` work, as the SDK's own example shows + (`ext_obex.h`, at `object_obex_store`). That settles the plan's open question. +- **No Max outlet behind a later `outlet<>`.** min makes the Max outlet in a private `create()`, + called only once, by `create_outlets()` at instantiation (`c74_min_outlet.h:438`). An `outlet<>` + added on a reload keeps a null instance, and sending through it calls `outlet_list(nullptr, …)`. + `tap.python~` gets away with this because min never sends through its signal outlets. A control + outlet would crash. + +**Recommend.** Specify the ports outright: + +- Make the value outlets, then the dumpout, in the constructor, in that order. +- Store the dumpout with `object_obex_store()` once the Max outlets exist (min's `setup` stage or + later). +- Send through raw outlet pointers taken from `outlet_nth()` and `outlet_insert_after()`, never + through `outlet<>::send()`. Keep min's list only for assist text, as `tap.python~` does. + +### M2. A reload can change outlets while another thread is outputting (verified gap; Max side unverified) + +**The plan says:** the `outlet_*` calls come after the GIL is released, and "a message that arrives +on the scheduler thread while a reload runs waits for the GIL". + +**The problem.** Once the GIL is released, nothing orders a message's output against a reload's +port changes. The reload runs on the main thread, without the GIL, and makes these changes: + +- `outlet_delete` and `outlet_insert_after` on Max's outlets; +- edits to min's outlet vector; +- `object_deletemethod` and `object_addmethod` for the class's messages. + +Under Overdrive, a scheduler-thread message can compute output for three outlets, release the GIL, +and index an outlet the main thread has just deleted: a use-after-free. The SDK documents nothing +about `outlet_insert_after` or `outlet_delete` (`ext_proto.h:465-469`, declarations only), let +alone their thread safety. + +**Recommend.** Add a per-object lock, held while a message maps its output onto outlets and while +a reload changes ports. Under it, a message checks that its outlet count is still current and +drops the output (once, reported) if not. Never hold it across a call into Python. Add a glue test +that races a widening reload against a second thread's messages. + +### M3. `anything` and the guard rest on Max dispatch the plan has not checked (unverified) + +**The plan says:** a min `message<> m_anything` forwards unknown selectors, and "the guard excludes +it by name". + +**What is known:** + +- min registers `anything` as a class method (`c74_min_object_wrapper.h:580`). +- The Python methods are instance methods (`object_addmethod`), and the fields are instance + attributes. The SDK says of instance methods: "these methods are private -- instance methods are + not actually fully implemented at this time" (`ext_obex.h:2296`). +- Today no class has both, because `tap.python~` reserves `anything`. + +**What is not known:** + +- whether Max tries instance methods and instance attributes (`steps 8`, `getsteps`) before a + class's `anything`; +- whether `object_getmethod()` answers an unknown name with the `anything` method on such a class. + If it does, the guard reserves every name: 1.0.0 again, and excluding `anything` by name does not + help. +- The test kernel from PR #37 answers from a fixed list, so it cannot reveal either. + +**Recommend.** + +- Do not give the class a min `anything`. Register `anything` per instance, and only when the + Python class defines it, so classes without one keep exactly `tap.python~`'s dispatch. +- Make the forwarder proof against dispatch order. Before calling the Python `anything`, it tries + the selector as a message, then as an attribute set, then as `get`. +- Make the guard ignore an answer that is the object's own forwarder. +- Settle the dispatch order in the 9.0 spike. + +### M4. D10 leaves out Scheduler in Audio Interrupt (omission verified; Max behavior unverified) + +**The plan says:** a message runs "on Max's main thread, or the scheduler thread under Overdrive", +and "worker mode (2.5) is the answer when both are in one patch and the computation is heavy". + +**The problem.** With Scheduler in Audio Interrupt on, the scheduler runs on the audio thread. A +`metro`-driven `tap.python` method then runs Python there, and waits for the GIL behind a reload +on the main thread, which holds it for milliseconds. + +- **Worker mode does not help**, because the heavy work itself is on the audio thread. +- **CLAUDE.md's guarantee breaks.** CLAUDE.md promises that in worker mode the audio thread "never + takes the GIL". That stops being true for any patch that also has a `tap.python`. +- **The plan, the production plan and the ReadMe never mention the setting.** The SDK has + `systhread_isaudiothread()` (`ext_systhread.h:180`) to detect it. + +**Recommend.** + +- **State it as an honest limit**, with its consequence: dropouts while Python runs or waits. +- **Decide what the object does about it:** warn once, or defer such messages to the main thread + (an attribute, or the default). +- **Correct D10's advice.** + +### M5. Announce-once hides diagnostics when the two objects apply different rules (verified) + +**The plan says** of announce-once only that the `Loaded` line "describes whichever ran the save". + +**The problem.** Every class diagnostic goes through `announce()`, which only the processor that +ran the file speaks (`m_announcing = executed`, `processor.h:187`). The two kinds disagree on +exactly what those diagnostics report: + +- `mode`, `latency`, `latencysamples` and `anything` are reserved only in `tap.python~`; +- methods with an unsaid-length tuple hint are dropped only in `tap.python`. + +So if a `tap.python` runs the save, a `tap.python~` on the same file silently loses a field named +`mode`, and the reverse happens too. + +**Recommend.** Key announce-once on the file and the object kind, or have each kind announce what +differs. Pin it with two processors whose reserved names and `bind_audio` differ. + +### M6. 9.1 changes `tap.python~`'s contract while saying it does not (verified) + +**The plan says** 9.1 leaves "`tap.python~`'s battery and glue test unchanged", and the CHANGELOG +should say the `list[…]` parameter "only adds". + +**The evidence:** + +- **List parameters work differently today.** `hint_kind(list[float])` is `'list'`, which + `value_type_from_hint` maps to a symbol (`value.h`). So today `foo 1` passes `""` to a parameter + hinted `list[float]`, and an `np.ndarray` parameter receives a `str`. Taking all the remaining + atoms changes what existing classes receive and how many arguments they accept. +- **The element type is missing.** `hint_kind` does not report it, so the support module must + change too. The plan does not say so. +- **The tuple rule has no gate.** Not exposing a method with an unsaid-length tuple return must be + limited to `tap.python`. Today `bind_message` ignores return hints (`processor.h:1390`), so an + ungated change removes messages from `tap.python~` classes. +- **The CHANGELOG's policy** (`CHANGELOG.md:5`): "From 1.0.0, a change to the class contract is a + major version." + +**Recommend.** + +- **Gate the tuple rule on `bind_audio`.** +- **Record the list parameter as a change, with a test pinning today's behavior first** (the + house rule). Either call the release 2.0, or argue in the plan that a hint that never worked is + not contract. Then the CHANGELOG states that, rather than "only adds". +- **Output an unsaid-length tuple as a list** from the first outlet rather than hiding the method, + which keeps D7's "a class written for one object loads in the other". + +## Minor + +- **m1. The output table contradicts itself and numpy (verified).** + - The sequence row covers "any non-str iterable", which includes the `dict` that the last row + reports. It also includes `bytes`, an unordered `set`, and one-shot generators. + - `np.bool_` has no `__index__` in numpy 2.5.3 but does convert with `float()` (checked: `1.0`). + So by the table it is output as a float, against the `bool` row and the ReadMe's rule that + numpy's bool counts as `bool`. + - **Recommend:** list the accepted sequence types, and add `np.bool_` to the bool row. +- **m2. A `str` return as a bare selector (design; Max behavior unverified).** + - `"list"`, `"int"`, `"float"` and `""` become malformed zero-argument messages. + - `"60"` becomes a selector, not a number. + - The example `note_name.py` outputs the pitch class `C` as a message named `C`, where most Max + objects that output a name send `symbol C`. + - **Recommend:** decide between `anything` and `symbol` in the 9.0 spike, against real + downstream objects (`route`, `sel`, `prepend`, a message box's `$1`), and say what happens to + the reserved selectors. +- **m3. `call()` cannot be overloaded on its return type (verified).** The new `call()` would have + the same parameters as the existing `bool call(std::string_view, std::span)` + (`processor.h:403`). It needs another name, such as `call_with_output()`. +- **m4. Outlet counts from string hints count nested commas (verified).** `return_shape` splits a + string hint on every comma (`runtime.h:559`). Checked: + - the hint `'tuple[list[int], dict[str, int]]'` gives 3; + - the same hint as an object gives 2. + + Outlet counts become patch cords, so this needs the depth-aware split that `_split_union` + already has. +- **m5. The planned mock tests cannot see what they claim (verified).** + - The glue test's `outlet_nth` stub returns a made-up pointer (`n + 1`), while the mock records + output by real outlet id. Output through such a pointer would land on another object's outlet, + or nowhere. + - `object_obex_store`, `object_obex_dumpout` and `outlet_insert_after` are not stubbed at all. + - **Recommend:** make the stubs faithful (CLAUDE.md's rule since PR #37). +- **m6. The file watcher's class name would be registered twice (unverified).** A "pure move" + keeps `class_new("tap.python~.filewatch", …)` and its `nobox` registration in both binaries. + Name it per host, or let B1's single core own it. +- **m7. The examples invite the class-body shadowing trap (verified).** Once `def list(...)` or + `def int(...)` is defined, a later annotation in the same class using `list[...]` or `int` + refers to the method. On 3.13: `TypeError: 'function' object is not subscriptable`. + `default.py` already warns about this. The ReadMe section and the new examples must too. +- **m8. D10 contradicts the Threads section, and overstates the GIL (verified).** + - D10 says a message holds the GIL "until it returns, and outputs on that thread". The Threads + section says the outlet calls come after the GIL is released. + - D10's "waits for the GIL in 0.5 ms slices" holds only for Python bytecode. A single long C + call (a numpy operation) holds the GIL throughout, as the production plan's 2.6 says itself + ("what a switch cannot interrupt is a single C call"). +- **m9. An unsourced claim about `js` (verified, plan text).** "Non-finite values pass through, as + `js` passes them" cites nothing, and `tap.python~` zeroes non-finite output. Either cite Max's + documentation for `js`, or choose on the merits: Max's own number boxes and arithmetic objects + can propagate NaN downstream. + +## What the plan should change, in order + +1. **Fix 1.0.0 and run 8.8 first:** PR #37, then the Mac session. +2. **Rewrite D11 around one core** (B1's option a, b or c), and record the decision. +3. **Add 9.0, the Max spike** (B2), and write its answers into the plan: + - two objects in either order; + - outlet insertion before a dumpout; + - `get` through the dumpout; + - `anything` dispatch and `object_getmethod()` on such a class; + - the threads under Overdrive and under Scheduler in Audio Interrupt; + - `symbol` or `anything` for a `str` return. +4. **Respecify Ports** (M1) and add the per-object output lock (M2). +5. **Redesign `anything`** as an instance method registered only when defined, with a forwarder + that does not depend on dispatch order (M3). +6. **Add Scheduler in Audio Interrupt to D10 and the ReadMe's limits** (M4). +7. **Key announce-once per object kind** (M5). +8. **Gate or record the contract changes**, with tests pinning today's behavior first (M6). +9. **Fix the minor items** in the text and the planned tests. From abad3a2388169c7f292c97234cc2bfd5a1baf0fb Mon Sep 17 00:00:00 2001 From: Timothy Place Date: Sat, 3 Oct 2026 01:25:58 +0000 Subject: [PATCH 4/5] Fix the Windows glue test: the fake "found" method folded into method_false The test kernel's fake object_getmethod() answered a registered name with a lambda returning 0 and an unknown name with method_false(), also returning 0. MSVC's Release link folds identical functions (/OPT:ICF, on by default), so on Windows both had one address, found_method() read every name as unanswered, and maxtest_host_answers was exposed as a message: the one failing check. lld --icf=all folds the same pair here (one address in nm); GCC settled the comparison at compile time, which is why Linux and macOS passed. The fake now answers with answered_method(), whose body is its own, and the scenario first requires that the kernel's two answers differ, so a folding linker fails it by name. The fix to the object itself is unchanged. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01WnyasLjC6poziMpwguw4in --- CLAUDE.md | 4 +++- .../tap.python_tilde/tap.python_tilde_test.cpp | 12 +++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 6cf1056..7d3e505 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -138,7 +138,9 @@ attaches all the zips + SHA256s to a release — a pre-release for 0.x, a draft answers `method_false()`, a function, not null, for a name an object does not have — a null test reserved every Python name in 1.0.0 (`found_method()` in the object; its glue test's kernel answers as Max does). The mock kernel is thinner than Max: when a stub decides a behavior, make - the test's stub faithful to the SDK (as `attr_args_offset` and `object_getmethod` are). + the test's stub faithful to the SDK (as `attr_args_offset` and `object_getmethod` are), and give + each fake function a body of its own: MSVC's Release link folds identical functions + (`/OPT:ICF`), which once gave a fake "found" method `method_false()`'s address on Windows only. - **Say what is true of a class once, what is true of an instance per instance.** Many objects can share one class file; only the processor whose `load()` ran the file (`load_script` says so) announces the class — its `Loaded` line and its diagnostics — through `announce()` (plan 6.7). diff --git a/source/projects/tap.python_tilde/tap.python_tilde_test.cpp b/source/projects/tap.python_tilde/tap.python_tilde_test.cpp index cb4bdc5..d515df3 100644 --- a/source/projects/tap.python_tilde/tap.python_tilde_test.cpp +++ b/source/projects/tap.python_tilde/tap.python_tilde_test.cpp @@ -59,12 +59,19 @@ namespace c74 { t_atom_long method_false(void*) { return 0; } + // The method the pretend class answers with. Its body must be its own: a linker that folds + // identical functions (MSVC's /OPT:ICF, on by default in Release; lld's --icf=all) gave a + // `return 0` here method_false's address, so every name read as unanswered on Windows. + void* answered_method(void*) { + static int s_marker; + return &s_marker; + } method object_getmethod(void*, t_symbol* s) { static const char* const k_answered[] = {"dsp64", "assist", "notify", "filechanged", "dspsetup", "maxtest_host_answers"}; for (const auto* name : k_answered) { if (s == gensym(name)) { - return reinterpret_cast(+[](void*) -> void* { return nullptr; }); + return reinterpret_cast(answered_method); } } return reinterpret_cast(method_false); @@ -434,6 +441,9 @@ SCENARIO("A save that changes process()'s inputs or outputs changes the object's SCENARIO("Methods named like messages Max sends with C arguments are not exposed, and the promised ones are " "(plan 8.2)") { ext_main(nullptr); + // the test's kernel must tell its two answers apart, or what follows tests the linker + REQUIRE(c74::max::object_getmethod(nullptr, c74::max::gensym("maxtest_host_answers")) + != reinterpret_cast(c74::max::method_false)); const auto file = tap::python::package_root() / "python" / "maxtest_mock_reserved.py"; write_file(file, "class maxtest_mock_reserved:\n" // a field, and a method, which must be exposed whatever the guard answers From 8407f93d1f28a78bcf89491d7a1bcb69fd3b5ebd Mon Sep 17 00:00:00 2001 From: Timothy Place Date: Sat, 3 Oct 2026 01:25:58 +0000 Subject: [PATCH 5/5] Use the Tap family's PythonTap icon for the package icon.png was min's template icon. It is now PythonTap-ground.svg from TapHouse's brand/ (tap/TapHouse#9, a545539, still open), rendered at 500x500 with cairosvg as brand/make_icons.py renders its PNGs: the ground version is the one the brand guide names for package icons. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01WnyasLjC6poziMpwguw4in --- CHANGELOG.md | 5 +++++ icon.png | Bin 36702 -> 19486 bytes 2 files changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4756ee0..cc15626 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,11 @@ a major version. The guard now recognizes `method_false()`, the unit test's kernel answers as Max does, and the Mac session (8.8) is what would have caught it before the tag. +### Changed + +- **The package has the Tap family's PythonTap icon** in place of min's template icon: the + "ground" version from TapHouse's `brand/`, rendered at 500×500. + ## 1.0.0 — 2026-10-01 The first stable release: the class contract as the ReadMe states it, the production plan's diff --git a/icon.png b/icon.png index c2ab314a32fda92e90d21668d2111d5328cc25e4..f59bcc2207c888836265c75d50efaba2f02e9559 100644 GIT binary patch literal 19486 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 X^VTQTb>Bm%@Q=E(j#8Gw*&F{4jgH@l