Skip to content

fix: resolve scratch-blocks v2 render crash for arrayMethod/hashMethod (#634)#635

Merged
takaokouji merged 4 commits into
feat/upstream-merge-2026-05from
fix/issue-634-array-hash-method-flyout-crash
May 4, 2026
Merged

fix: resolve scratch-blocks v2 render crash for arrayMethod/hashMethod (#634)#635
takaokouji merged 4 commits into
feat/upstream-merge-2026-05from
fix/issue-634-array-hash-method-flyout-crash

Conversation

@takaokouji
Copy link
Copy Markdown

@takaokouji takaokouji commented May 4, 2026

Summary

Issue #634完全修正。Ruby converter で a = [1, 2, 3]; puts(a.max) のようなコードが正しくブロック描画されなかった問題を 3 つの v2 incompat fix で解消。

Root cause

scratch-blocks v2 (= Blockly v12) で Input.appendField のフィールド初期化フローが変わり、Smalruby 側の defineDynamicBlock.updateBlockDisplayblock.rendered = false を前置していたため FieldDropdown の SVG <text> 要素が作られず、Field.getTextContent() が null で Block.render() が throw → workspace の XML 解析が中断されていた。

(詳細は #634 の resolution comment 参照)

Changes

1. define-dynamic-block.jsupdateBlockDisplay の v2 対応

block.rendered = false の toggle を削除し、scratch-blocks v2 procedures.ts updateDisplay_ と同じパターンに修正。これで appendField が正しく field.init() を delegate し、SVG <text> 要素が初期化される。

2. define-dynamic-block.jsFieldVerticalSeparator v2 namespace fix

scratch-blocks v2 では ScratchBlocks.FieldVerticalSeparator が namespace export されていない (fieldRegistry のみで提供される)。makeVerticalSeparator(ScratchBlocks) ヘルパーで v1/v2 両対応。

3. Ruby converter — 配列リテラル変換時の重複変数解消

_onVasgn (assignments.js) が _lookupOrCreateVariable を eager に呼んで scalar 変数を作り、その後配列リテラル handler が _lookupOrCreateList で list 変数を作るため、同名の _a_1_ が VM target に 2 つ登録され、scratch-blocks v2 が同名変数の workspace XML を解析失敗していた。

  • 配列リテラル handler が list を作る直前に eager scalar を _context.localVariables から削除
  • _lookupOrCreateVariableOrList で、要求 type の store が空でも反対側 store にエントリがあれば再利用 (top-level scope では _getCurrentScope() === null で create path に落ちるためのフォールバック)

Test plan

  • puts(a.max)Array (a) . max ブロックとして正しく描画される
  • _a_1_ 変数が VM target に 1 つだけ存在 (type: list)
  • Field.getTextContent() が null を返さない
  • toolbox/flyout が壊れない
  • lint pass

PR #630 (parent) で実施した #634 以外の Ruby/array 系テストへの regression は要確認 (CI 待ち)。

Outstanding

  • CI green (lint + unit + integration) — push 後 CI 結果待ち
  • 手動でも他のケース (a.sort, h.keys, a.join(",") 等) が動作することを再確認

関連

🤖 Generated with Claude Code

…` literals

Issue #634 (under feat/upstream-merge-2026-05).

Three independent v2 incompatibilities surfaced together when converting
`a = [1, 2, 3]; puts(a.max)` and similar Ruby code:

1. **`ScratchBlocks.FieldVerticalSeparator` removed in v2.** scratch-blocks
   v2 no longer exports the class on the namespace; it is registered via
   `fieldRegistry`. The old `new ScratchBlocks.FieldVerticalSeparator()`
   call in `defineDynamicBlock.createAllInputs` threw mid-flight in
   `domToMutation`, which made the surrounding XML parser drop the rest of
   the block's `<next>` chain — every Ruby-converted script that touched
   an extension block with an icon ended up as detached top-level blocks.
   Add a v2-compatible `makeVerticalSeparator` helper that falls back to
   `fieldRegistry.fromJson({type: 'field_vertical_separator'})`.

2. **Eager scalar created by `_onVasgn`.** `assignments.js _onVasgn` calls
   `_lookupOrCreateVariable(varName)` *before* visiting the RHS, which
   registers `a` as a SCALAR in `_context.localVariables`. The
   array-literal converter then calls `_lookupOrCreateList(a)` to get the
   list, but the eager scalar is left behind — target-applier creates
   *both* `_a_1_` (list) and `_a_1_` (scalar) on the VM target, and
   scratch-blocks v2 fails to render any block referencing the list. The
   array-literal handler now drops the eager scalar before creating the
   list.

3. **Cross-store re-creation at top level.** At the top level the converter
   has no scope tracking (`_getCurrentScope() === null`), so a subsequent
   `_lookupOrCreateVariable('a')` call (e.g. from
   `visitLocalVariableReadNode` for `a.max`) takes the falls-through
   creation path and registers a fresh scalar with the same transformed
   name. Add a check in `_lookupOrCreateVariableOrList` to reuse an
   existing entry from the *other* store (lists ↔ localVariables) when
   the requested store is empty for that transformed name.

Together these keep exactly one canonical variable per local name across
the converter's data flow, which is required for scratch-blocks v2 to
parse the workspace XML the VM emits.

Note: rendering of `smalrubyRuby_arrayMethod` / `smalrubyRuby_hashMethod`
blocks themselves is still pending — `Field.getTextContent()` returns null
during render even with a clean variable graph. That is a separate v2
incompat tracked in #634 and will be addressed in follow-up commits.
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 4, 2026

…h on v2

Issue #634 — final fix.

Smalruby's `defineDynamicBlock.updateBlockDisplay` followed a v1-style
pattern that toggled `block.rendered = false` around `appendField` and
re-issued `block.initSvg()` + `block.render()` at the end:

    const wasRendered = block.rendered;
    block.rendered = false;
    ...
    createAllInputs(block, ...);   // → appendField(new FieldDropdown(...))
    ...
    block.rendered = wasRendered;
    if (wasRendered) { block.initSvg(); block.render(); }

In Blockly v12 (the runtime under scratch-blocks v2.1.19) this dance
silently drops field initialisation:

  Input.appendField(field, name) {
    field.setSourceBlock(this.sourceBlock);
    this.sourceBlock.initialized && this.initField(field);
    this.sourceBlock.rendered && this.sourceBlock.queueRender();
  }
  Input.initField(field) {
    this.sourceBlock.rendered ? field.init() : field.initModel();
  }

For a block constructed via `domToMutation`:

* `block.initialized` is `false` until `initSvg()` runs the one-shot guard.
* `block.rendered` is `true` (BlockSvg constructor default).

Smalruby was forcing `rendered = false` during `appendField`, so:
* `appendField` saw `initialized=false` → skipped `initField` entirely
  → field's SVG `<text>` element never created.
* The follow-up `block.initSvg()` is gated by `initialized`, but because
  `initialized` was still `false`, that path is fine — *except* my code
  flipped `rendered` back to `true` before it. With `rendered=true`,
  `initSvg`'s field-init would run `field.init()` (which creates the SVG
  text). Either path *should* work.
* In practice, neither path triggers consistently across the
  XML-parser-driven domToMutation flow, leaving the field's
  `textContent_` null. The next `block.render()` invokes the block
  renderer, which calls `Field.getSize` → `Field.render_` →
  `Field.getTextContent`, and the missing text element throws
  "The text content is null." Stack:

    at Field.getTextContent
    at Field.render_
    at Field.getSize
    at new BlockRenderRow
    at BlockRenderer.createRows_
    at BlockRenderer.measure
    at Block.render

The error aborts the parser mid-tree, drops the rest of the block's
`<next>` chain, and leaves Smalruby's flyout/workspace unable to render
any script that references `smalrubyRuby_arrayMethod`,
`smalrubyRuby_hashMethod`, or `smalrubyRuby_stringMethod`.

Fix: stop toggling `block.rendered`. Let `appendField` see the genuine
state (`initialized=false`, `rendered=true`) so it correctly delegates
field initialisation to the subsequent `block.initSvg()` call we make at
the end. `block.initSvg()` walks `inputList` and calls `field.init()`
for every field, which creates each field's SVG `<text>` element. We
also keep `block.render()` to re-measure the block with its new inputs.

This mirrors what scratch-blocks v2's own `procedures.ts updateDisplay_`
does — that's the working blueprint for the same pattern.

After the fix:

* `puts(a.max)` round-trips correctly (`a = [1, 2, 3]` block chain
  followed by `Array (a) . max` connected to `戻り値 と 1 秒言う`).
* `Field.getTextContent` no longer throws.
* Toolbox refresh no longer fails.

Verified manually with Playwright on `?tab=ruby&ruby_version=2`.
@takaokouji takaokouji changed the title fix: de-duplicate scalar/list variable + FieldVerticalSeparator v2 fix (#634, WIP) fix: resolve scratch-blocks v2 render crash for arrayMethod/hashMethod (#634) May 4, 2026
@takaokouji takaokouji marked this pull request as ready for review May 4, 2026 14:00
takaokouji added 2 commits May 4, 2026 23:08
… on v2

Three coordinated fixes for the array/hash method flyout crash on
scratch-blocks v2:

1. define-dynamic-block.js: scratch-blocks v2 only registers
   FieldVerticalSeparator via fieldRegistry; the v1-style
   `new ScratchBlocks.FieldVerticalSeparator()` constructor call now
   throws "is not a constructor" at runtime. Add a `makeVerticalSeparator`
   helper that prefers the v1 constructor when present and falls back to
   `fieldRegistry.fromJson({type: 'field_vertical_separator'})` for v2.

2. target-applier.js: the converter eagerly creates a SCALAR entry in
   `_context.localVariables` for `t = [...]` before visiting the RHS.
   The array-literal handler then creates a LIST entry in `_context.lists`
   under the same transformed name. Pushing both to the VM target makes
   scratch-blocks v2 fail to parse the workspace XML for any block that
   references the list. Iterate `lists` first and dedupe by
   `(scope, name)` so the LIST wins and the eager SCALAR is dropped.

3. smalruby-ruby.js (Ruby generator): the bang-method generator
   (`sort!`, `reverse!`) reads RECEIVER as a name and looks it up via
   `variableNameByName(name)` (defaults to SCALAR_TYPE). With the SCALAR
   dropped at target-applier boundary, the lookup returns null and the
   round-trip emits `nil.sort!`. Fall back to `listNameByName` so array
   bang methods resolve through the surviving LIST entry.

Closes #634
@takaokouji takaokouji merged commit 2702a35 into feat/upstream-merge-2026-05 May 4, 2026
8 of 9 checks passed
@takaokouji takaokouji deleted the fix/issue-634-array-hash-method-flyout-crash branch May 4, 2026 22:43
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