Skip to content

ogar-loco: close the shared-core arity gap (50 → 90 covered) - #265

Merged
AdaWorldAPI merged 1 commit into
mainfrom
claude/rust-scratch-abi-soa-gsamge
Aug 11, 2026
Merged

AdaWorldAPI merged 1 commit into
mainfrom
claude/rust-scratch-abi-soa-gsamge

Conversation

@AdaWorldAPI

Copy link
Copy Markdown
Owner

The core named 95 operations and gave stack arities for 50. The other 45 resolved to a legal FnIndex and then could not say what they consumed — so a body containing one could not be validated or segmented, and the failure surfaced only in a consumer, far from the cause.

shared_core::stack_arity's own doc already called these "real palette entries deliberately not yet covered", with the standing invitation that "coverage grows in the core, once, for everyone." This is that sweep: 40 newly covered, 5 deliberately refused.

Found from the consumer side: mapping a second frontend (Scratch) onto the same core hit 21 of them, and 37 Blockly block types land on the same arity-less bytes — so it predates Scratch and was never Scratch-specific.

Where the numbers come from — not judgement

Each arity is corroborated by the frontends' own declarations, harvested from source: Blockly's block definitions (AdaWorldAPI/blockly, blocks/*.ts) and Apache-2.0 scratch-blocks (src/blocks/*.ts, args0 input_value/input_statement counts). Where both frontends have the block they agree; where only one does, that one decides.

Two conventions were read from the existing table, not invented:

  • VAR_GET => 0 — "leaves: they push, they do not consume" — establishes that an identifier rides as an immediate value byte, never a stack operand. So VAR_SET/VAR_CHANGE/APPEND evaluate only the incoming value, which is exactly what both frontends declare (the variable is a field in each).
  • The two-quantity split governs PROC_DEF: it evaluates nothing (arity 0) and references its body (body_refs 1). Scratch's procedures_definition independently declares zero value inputs and one statement input.

The list family — one table, two frontends

Blockly passes a list expression (lists_length takes a LIST input); Scratch names a global list in a field (data_lengthoflist declares zero inputs). On a stack machine these converge: Scratch's field lowers to an explicit push of the list reference — the shape VAR_GET already has — and the op pops it. So the list is counted as an ordinary operand, and no second opcode family is needed for the second frontend.

What stays refused, and why that is not an oversight

function why it has no fixed arity
RETURN a void return evaluates nothing; a value return evaluates one
NUMBER_PROPERTY even/odd/prime take the number alone; divisible by takes a divisor
PROMPT text_prompt carries its message as a field, text_prompt_ext as an operand
LIST_WITH a list literal holds as many items as its mutator says
PROC_CALL a call passes as many arguments as its procedure declares

These want an arity that is a property of the call, not the function — a design question, not a table row. Guessing would be worse than refusing: a wrong arity doesn't produce a slightly-wrong result, it desynchronizes the stack and reattributes every later operand.

The enumeration that was missing

every_named_function_has_an_arity_except_the_documented_variadics walks the whole byte range instead of a hand-listed set, so a newly minted constant with no arity row fails here immediately rather than years later in a consumer — which is exactly how this gap was found. Verified as a real falsifier by deleting one arity row: it fails naming the exact opcode ([(38, Some("TERNARY"))]).

Three existing tests pinned WAIT as deliberately-uncovered and failed as designed. Each is re-pinned onto the still-variadic set and gained a can-fire half, so "uncovered" keeps discriminating instead of becoming a label for whatever happens to be missing.

Verification

cargo test --workspace: 78 test targets, 0 failures. clippy --workspace --all-targets clean. Downstream, blockly-rs's pinned gap count drops 21 → 1 (PROC_CALL, still variadic) — its pin fired exactly as designed and was re-pinned deliberately.


Generated by Claude Code

The core NAMED 95 operations and gave stack arities for 50. The other 45
resolved to a legal FnIndex and then could not say what they consumed -- so a
body containing one could not be validated or segmented, and the failure
surfaced only in a consumer, far from the cause.

The doc already called these "real palette entries deliberately not yet
covered" with the standing invitation that "coverage grows in the core, once,
for everyone". This is that sweep: 40 newly covered, 5 deliberately refused.

## Where the numbers come from

Not judgement. Each arity is corroborated by the frontends' own declarations,
harvested from source: Blockly's block definitions (AdaWorldAPI/blockly
`blocks/*.ts`) and Apache-2.0 scratch-blocks (`src/blocks/*.ts`, the
`args0` input_value / input_statement counts). Where both frontends have the
block they agree; where only one does, that one decides. `control_wait`
declares one input, `control_stop` none, `logic_ternary` and `math_constrain`
three, `math_atan2` and `operator_random` two, `math_random_float` none.

Two conventions were READ from the existing table rather than invented:

- `VAR_GET => 0` ("leaves: they push, they do not consume") establishes that
  an identifier rides as an immediate value byte, never a stack operand. So
  VAR_SET / VAR_CHANGE / APPEND evaluate only the incoming value -- exactly
  what both frontends declare, the variable being a field in each.
- The two-quantity split governs PROC_DEF: it evaluates nothing (arity 0) and
  REFERENCES its body (body_refs 1), which is why it also lands in the
  body_refs table. Scratch's `procedures_definition` independently declares
  zero value inputs and one statement input.

## The list family, and why one table serves two frontends

Blockly passes a list EXPRESSION (`lists_length` takes a LIST input); Scratch
names a global list in a FIELD (`data_lengthoflist` declares zero inputs). On
a stack machine these converge: Scratch's field lowers to an explicit push of
the list reference -- the shape VAR_GET already has -- and the op pops it. So
the list is counted as an ordinary operand, and no second opcode family is
needed for the second frontend.

## What stays refused, and why that is not an oversight

Five entries: RETURN, NUMBER_PROPERTY, PROMPT, LIST_WITH, PROC_CALL. Every
one is genuinely VARIADIC -- a void return evaluates nothing and a value
return evaluates one; `divisible by` takes a divisor the other number
properties do not; `text_prompt` carries its message as a field where
`text_prompt_ext` makes it an operand; a list literal and a procedure call
hold as many items/arguments as their mutator declares. These want an arity
that is a property of the CALL rather than the function, which is a design
question and not a table row. Guessing would be worse than refusing: a wrong
arity desynchronizes the stack and reattributes every later operand.

## The enumeration that was missing

`every_named_function_has_an_arity_except_the_documented_variadics` walks the
whole byte range instead of a hand-listed set, so a newly minted constant with
no arity row fails HERE immediately rather than years later in a consumer --
which is exactly how this gap was found. Verified as a real falsifier by
deleting one arity row: it fails naming the exact opcode.

Three existing tests pinned WAIT as deliberately-uncovered and failed as
designed; each is re-pinned onto the still-variadic set and gained a can-fire
half, so "uncovered" keeps discriminating instead of labelling whatever
happens to be missing.

cargo test --workspace: 78 test targets, 0 failures. clippy clean.
@cursor

cursor Bot commented Aug 11, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_f1670101-9862-4528-b307-353f6000365e)

@AdaWorldAPI
AdaWorldAPI marked this pull request as ready for review August 11, 2026 21:02
@AdaWorldAPI
AdaWorldAPI merged commit c05d818 into main Aug 11, 2026
2 checks passed

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a6eccf126b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +143 to +144
FnIndex::WAIT | FnIndex::WAIT_UNTIL => 1,
FnIndex::STOP => 0,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Mark newly covered commands as non-pushing

Once these opcodes receive arities, shared_core::pushes_result also starts covering them but falls through to Some(true) because its non-pushing match was not extended. Consequently statement commands such as WAIT, WAIT_UNTIL, STOP, PRINT, APPEND, variable/list mutations, and PROC_DEF leave fictitious stack values during statement_bounds; for example, two consecutive STOP calls now produce DanglingOperands { depth: 2 } instead of two statement boundaries. Add every newly covered command that does not return a value to the non-pushing set.

Useful? React with 👍 / 👎.

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.

2 participants