Skip to content

9.1 The core for tap.python: what a method returns, as output; outlets from the hints; the audio option - #46

Merged
tap merged 6 commits into
mainfrom
v2/core-output
Oct 9, 2026
Merged

tap merged 6 commits into
mainfrom
v2/core-output

Conversation

@tap

@tap tap commented Oct 9, 2026

Copy link
Copy Markdown
Owner

Plan item 9.1 (docs/TAP-PYTHON-PLAN.md, "The core"). Everything that tap.python (a Python class as a Max object without audio) needs from the host-independent core lands here, with tests. There is no Max code change: the Max object is 9.3. tap.python~'s battery and glue test pass unchanged, with one deliberate exception: the list[…] parameter change below.

What's in it

What a method returns, as output (value.h, processor.h)

  • output_item is one message for an outlet: a selector and its atoms. No selector means one number; the selector list means a list; any other selector is the message it names. output is what each outlet emits from one call.
  • processor::call_with_output() sits beside call() and converts the result by the plan's output table:
    • None outputs nothing.
    • A bool, number or str is one value. A str is symbol <s>, as the 9.0 spike decided.
    • A list, unhinted tuple, range or 1-D array is a list; when its first element is a str, it's the message that names instead.
    • A 0-d array is its value.
    • A return hinted tuple[…] gives one value per outlet.
  • Anything an outlet can't carry is reported and nothing is output: an integer past 64 bits, a list holding None or another list, a dict, a 2-D array.
  • numpy's types are recognized by name without importing numpy. I checked the conversion code for CPython data symbols (the delay-loaded Windows external can't use them); there are none in a Release build.

Outlets

  • message_info gains return_count and spreads, and outlet_count() gives the widest return hint.
  • The support module now splits a return hint written as a string only at its top level, so 'tuple[list[int], dict[str, int]]' counts 2 outlets, not 3 (audit m4).

The audio option

  • The processor's constructor takes bind_audio. With it off, process() and prepare() are ordinary messages, has_process() stays false, and prepare() does nothing.
  • With it off, a tuple hint that doesn't say how many values is output as a list from one outlet, and the console says so once.

A last parameter hinted list[…] or np.ndarray takes all the atoms left after the other parameters, as a list or as a float64 array. This applies to tap.python~ too.

  • It is a contract change: before, such a parameter took one atom converted to a symbol, so a number arrived as the empty string, and extra atoms were refused.
  • The first commit pins that old behaviour with a test; the change then updates the test.
  • It's recorded in a new CHANGELOG 2.0.0 — unreleased section and in the ReadMe's Messages paragraph.

Announcements per kind of object (audit M5)

  • Some diagnostics depend on how a kind of object binds the class: a name its host reserves, process()'s binding, a keyword-only parameter, the tuple rules. These are now said once per (save, kind), so the audio and control kinds each say theirs even when the other kind's object ran the file.
  • What's true of the class whatever binds it (a class that won't load or instantiate, a hint that won't resolve, an attribute whose type changed) is still said once per save.
  • The Loaded line says what that object bound, e.g. Loaded outputs.py: 5 messages, 2 outlets.

Examples (python/): euclid.py (a Euclidean rhythm: bang in, list out), scale.py (a list scaled with numpy: list in, list out) and note_name.py (a MIDI note's name from two outlets, tuple[str, int]).

Bench

  • tap_python_bench now also times a message with a list of 64 numbers, converted in and back out. In Release on this Mac it costs 5.6 µs for the bridge alone and 15 µs for scale.py.
  • update-perf-docs.py writes that as a sentence beside the process() table. I did not regenerate the ReadMe here. With macOS's indexing running, every row came out 20–35% slower and noisier than the 30 September table. main and this branch measured alike back to back, so it's the machine, not the change. Today's figures are in the plan; the table gets regenerated at the next idle measurement (9.6 at the latest).

Tests

test_output.cpp, 10 scenarios and 396 assertions:

  • the output table as one data-driven test over its edges: 64-bit limits, NaN and infinity, numpy scalars of each family, 0-d and 2-D arrays, nested lists;
  • tuple hints of each length against results of the wrong length;
  • outlet counts from object and string hints;
  • bind_audio off;
  • two kinds of object sharing one file, each order;
  • list and array parameters;
  • a raising method;
  • a message called while the class reloads on another thread.

test_examples.cpp runs the three new examples on known inputs.

On this Mac:

  • the core battery passes 111/111, and 111/111 under ThreadSanitizer;
  • tap.python~'s glue test passes;
  • clang-format and clang-tidy-18 (as in style.yml) are clean.

CI will run Linux (ASan/UBSan, TSan) and Windows.

Docs

  • The plan: 9.1 ticked in both plan files, and "The core" updated as built.
  • CLAUDE.md: notes on call_with_output() and the new examples.

🤖 Generated with Claude Code

https://claude.ai/code/session_019rVV9SkjkWmiBXFMT4whkz

tap and others added 6 commits October 9, 2026 14:52
… a symbol (9.1)

Before plan 9.1 changes it — a last parameter hinted list[...] or np.ndarray is to take the
remaining atoms — the test states today's behavior: one numeric atom arrives as the empty symbol,
and more atoms than parameters are refused.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019rVV9SkjkWmiBXFMT4whkz
…om the hints; the audio option (9.1)

What docs/TAP-PYTHON-PLAN.md's "The core" asks of 9.1, host-independent and tested here:

- value.h: output_item (a selector and its atoms: a number, a list, or the message a selector
  names) and output, what each outlet outputs from one call.
- processor::call_with_output() beside call(): the method's result converted by the output table
  — None nothing; a bool, number or str one value (a str as `symbol <s>`, plan 9.0); a list,
  unhinted tuple, range or 1-D array a list, or the message its first str names; a 0-d array its
  value; a return hinted tuple[...] one value per outlet. What an outlet cannot carry — an
  integer past 64 bits, a list holding None or a list, a dict, a 2-D array — is reported and
  nothing is output. numpy's types are known by name, without importing numpy, and no CPython
  data symbol is used (the delay-loaded Windows external).
- message_info::return_count and spreads, and outlet_count(): the widest return hint. The support
  module's return_shape() splits a hint written as a string at its top level only (audit m4).
- The constructor's bind_audio: off (tap.python), process() and prepare() are messages like any
  other; a tuple hint that does not say how many values is output as a list, and said once.
- A last parameter hinted list[...] or np.ndarray takes the atoms left after the others — for
  tap.python~ too: a change to the class contract, pinned by the test first and recorded in
  CHANGELOG.md (2.0.0) and the ReadMe.
- Announce-once per kind (audit M5): what depends on how an object binds the class — a name its
  host reserves, process() and tuple returns — is said once per (save, kind), even when an object
  of the other kind ran the file; the Loaded line stays once per save and says what that object
  bound ("5 messages, 2 outlets").

test_output.cpp: the output table as one data-driven test over its edges (64-bit limits, NaN and
infinity, numpy scalars, 0-d and 2-D arrays, nested lists), tuple hints of each length against
results of the wrong length, outlet counts from object and string hints, both kinds on one file,
the list and array parameters, a raising method, and a call racing a reload. tap.python~'s battery
is unchanged and green (108/108).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019rVV9SkjkWmiBXFMT4whkz
…nown inputs (9.1)

Three things Max users write by hand, as classes for the object without audio: a Euclidean rhythm
from two attributes (a bang, a list out), a list scaled with numpy (a list in as one array, a list
out), and a MIDI note's name from two outlets (tuple[str, int]). Each carries default.py's comment
on why a method named int or list comes last. The core battery runs them on known inputs.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019rVV9SkjkWmiBXFMT4whkz
…entence in the ReadMe's table (9.1)

tap_python_bench also times processor::call_with_output() on an object without audio: a list of 64
numbers converted in and the returned list converted back out, for a class that returns it as it
came (the bridge alone) and for scale.py (one numpy array), fastest of the rounds, in the JSON as
"calls". scripts/update-perf-docs.py writes the sentence beside the process() table, so the
ReadMe's performance claim for tap.python is measured.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019rVV9SkjkWmiBXFMT4whkz
…put() and the new examples

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019rVV9SkjkWmiBXFMT4whkz
…e is next regenerated

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019rVV9SkjkWmiBXFMT4whkz
@tap
tap merged commit fe35e93 into main Oct 9, 2026
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant