@hyperfixi/testing-framework 4.2.0 → 4.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,68 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [4.3.0] - 2026-10-07
11
+
12
+ More of what upstream reads now translates, and what does not is refused instead of lost. On the
13
+ command-shape gate, round trips that came back silently different fell from 174 in 4.2.0 to 31,
14
+ none of them in English; 24,386 of 27,240 now read as their source does. English-to-English refuses
15
+ 61 of the 1,135 scripts, down from 82, and each of the 61 now belongs to a named family with the
16
+ reason it is not carried yet.
17
+
18
+ ### Added
19
+
20
+ - **`@lokascript/semantic` reads `on every <event>` and `init immediately`.** Both lost their head
21
+ word, so the handler or the block was refused. `EventHandlerSemanticNode.eventModifiers.every` and
22
+ `FeatureSemanticNode.immediately` carry them. English writes them as upstream does; every other
23
+ language writes the English word first (`every al clic …`), as it already did for `on first`.
24
+ - **`@hyperfixi/testing-framework`: two new value-matrix axes.** A `branch` position is an `if` with
25
+ no `then` before a command that names no target (`if 2 is in arr add .a end`). The matrix's `if`
26
+ cells always wrote `then` and a `put` with its target, so an SOV reader always saw where the
27
+ condition ended, and none of them could fail the way Turkish did (see Fixed). The 610 pairs that
28
+ `branch` refuses are listed in the baseline. A twelfth operand kind is a CSS length (`100px`,
29
+ `50%`). The matrix now has 5,396 cells.
30
+
31
+ ### Changed
32
+
33
+ - **`@lokascript/semantic` refuses core's swap strategies that upstream has no spelling for.**
34
+ `translate()` gains a loss kind, `'core-only'` (`TranslationLossKind`). Upstream and the engine
35
+ read `swap morph of #t with it` as an exchange with a property named `morph`, so core's morph
36
+ strategies, `none`, and a strategy with no content are now refused in every language. Before,
37
+ English wrote them as they were read. Core's `swap into #t with x` and `swap over #t with x` are its
38
+ innerHTML and outerHTML, and they are now written as `put x into #t` and `put x into #t's
39
+ outerHTML`. Before, English wrote `swap into of #t with x`, which runs as an exchange.
40
+ - **`@lokascript/semantic` writes a word in the target language only where that language's reader
41
+ brings it back.** A property name is localized only through the reader's property table or as a
42
+ keyword. So `add .foo to my children` keeps `children` in Spanish (`mi children`), where it wrote
43
+ `mi hijos` and read back a property named `hijos`. A call's arguments are written as written
44
+ (`call sprayInto(me)` wrote `sprayInto(yo)`). So is every bracket group inside an expression:
45
+ `2 is in [true, n]` wrote `[verdadero, n]` in Spanish, a variable no reader de-localizes. And so is a
46
+ type name after `am a`. These spots now show English words in foreign renders where they used to
47
+ show words that did not come back.
48
+ - **`@lokascript/semantic`: `innerHTML` and `outerHTML` are no longer English keywords.** A swap's
49
+ strategy slot still reads them, as words.
50
+
51
+ ### Fixed
52
+
53
+ - **`@lokascript/semantic`: Turkish keeps an `if` condition whole before a command with no target.**
54
+ `if 2 is in arr add .a end` read back as `if 2 add .a to is in arr end`, with any operand, and so
55
+ did `is a Number` and `<` after a possessive. Turkish case markers may be dropped, and the marker
56
+ test counted a marker word anywhere in the value: `in` (also the operator), `a` (also the article),
57
+ `nin` (also the possessive). The marker must now be the last token of its group.
58
+ - **`@lokascript/semantic`: a number with a CSS unit is one value.** Upstream reads `100px`,
59
+ `1.5rem` and `50%` as one string. Every tokenizer split them, so `set my *width to 100px` and
60
+ `transition height to 100px` were refused in all 24 languages. A `%` that an operand touches
61
+ (`5%2`) or that is spaced still reads as the operator.
62
+ - **`@lokascript/semantic`: more English forms that were refused in every language now read.**
63
+ `set innerHTML of #d1 to …` and `increment innerHTML of #d1`; a single-quoted string that holds a
64
+ `"` (`set #t to '<div _="…">'`), which was written back in double quotes and so closed early;
65
+ `fetch … don't throw`; `put … at the start of` / `at the end of`, where the `end` closed the
66
+ handler; `remove 3 from :arr` and `remove {color} from me`; `otherwise` as `else`. In a translation
67
+ of an empty then-branch's `otherwise`, 13 languages wrote their own word for it inside the
68
+ condition and read it back there, silently.
69
+ - **`@lokascript/semantic`: a handler's `catch` and `finally` bodies are written in upstream's
70
+ spelling.** `catch e prepend …` kept core's `prepend`, which the engine rejects.
71
+
10
72
  ## [4.2.0] - 2026-10-07
11
73
 
12
74
  A translation that would lose part of a script is now refused instead of returned short.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperfixi/testing-framework",
3
- "version": "4.2.0",
3
+ "version": "4.3.0",
4
4
  "description": "Cross-platform behavior testing suite for LokaScript applications",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -57,10 +57,10 @@
57
57
  "author": "LokaScript Contributors",
58
58
  "license": "MIT",
59
59
  "dependencies": {
60
- "@hyperfixi/patterns-reference": "^4.2.0",
61
- "@lokascript/compilation-service": "^4.2.0",
62
- "@lokascript/i18n": "^4.2.0",
63
- "@lokascript/semantic": "^4.2.0",
60
+ "@hyperfixi/patterns-reference": "^4.3.0",
61
+ "@lokascript/compilation-service": "^4.3.0",
62
+ "@lokascript/i18n": "^4.3.0",
63
+ "@lokascript/semantic": "^4.3.0",
64
64
  "diff": "^8.0.3",
65
65
  "esbuild": "^0.25.12",
66
66
  "happy-dom": "^20.14.5",
@@ -72,8 +72,8 @@
72
72
  "vite": "^8.3.1"
73
73
  },
74
74
  "devDependencies": {
75
- "@hyperfixi/engine": "^4.2.0",
76
- "@lokascript/hyperscript-adapter": "^4.2.0",
75
+ "@hyperfixi/engine": "^4.3.0",
76
+ "@lokascript/hyperscript-adapter": "^4.3.0",
77
77
  "@types/diff": "^5.0.0",
78
78
  "@types/node": "^26.6.3",
79
79
  "@vitest/coverage-v8": "^5.0.0",
@@ -589,6 +589,115 @@ export const LOUD: readonly LoudFamily[] = [
589
589
  "a possessive with nothing after it (`halt the event's then …`), which the engine " +
590
590
  'reads as the bare reference; one upstream test source',
591
591
  matches: source => /'s(?=\s+(then|end)\b|\s*$)/.test(source),
592
+ }, // M1 step 3 (2026-10-07): the English refusals left after the value and
593
+ // command-form fixes, each family with why it is not carried.
594
+ {
595
+ name: 'scroll-by',
596
+ reason:
597
+ "scroll's relative form (`scroll #box down by 200px`) names a direction, which no " +
598
+ "language's dictionary has: adding them is an owner vocabulary decision (the M2 sheet)",
599
+ matches: (_source, _types, plain) =>
600
+ someNode(plain, node => node.type === 'scrollCommand' && 'by' in node),
601
+ },
602
+ {
603
+ name: 'beep',
604
+ reason:
605
+ "`beep!`, upstream's debugging command, has no schema: its form in each language is an " +
606
+ 'owner vocabulary decision (the M2 sheet)',
607
+ matches: (_source, types) => types.has('beepCommand'),
608
+ },
609
+ {
610
+ name: 'halt-modes',
611
+ reason:
612
+ "halt's `default` (also a command word) and `the event's bubbling|default`: the slot " +
613
+ 'that keeps `call` out of `halt call f()` cannot tell `default` from a command without ' +
614
+ 'a halt-only reading (OPEN_ITEMS P56)',
615
+ matches: source => /\bhalt\s+(default\b|the event's\s+(bubbling|default)\b)/.test(source),
616
+ },
617
+ {
618
+ name: 'collection-expressions',
619
+ reason:
620
+ "upstream 0.9.93's collection expressions (`… where it matches .a`, `mapped to`) are an " +
621
+ "expression form semantic's reader does not have: a parser-track item (OPEN_ITEMS P57)",
622
+ // Not `if no <q/> in … where …`, which English reads (its foreign lanes are
623
+ // the value-words family).
624
+ matches: (source, types) => types.has('collectionExpression') && !/\bif\s+no\s+</.test(source),
625
+ },
626
+ {
627
+ name: 'pseudo-class-query',
628
+ reason:
629
+ 'a query of a pseudo-class (`<:checked/>`): the tokenizers read `:checked` as a local ' +
630
+ 'variable; a tokenizer item, rare in pages',
631
+ matches: source => /<:[\w-]+\/>/.test(source),
632
+ },
633
+ {
634
+ name: 'closest-forms',
635
+ reason:
636
+ "`closest @attr` (an ancestor's attribute) and `closest parent <q/>`: the forms of " +
637
+ '`closest` besides a query, rare in pages',
638
+ matches: (_source, _types, plain) =>
639
+ someNode(
640
+ plain,
641
+ node =>
642
+ node.type === 'closestExpr' &&
643
+ // Not `closest @foo to #d1`, which English reads (value-words).
644
+ (node.parentSearch === true ||
645
+ (String(node.css ?? '').startsWith('[') &&
646
+ (node.to as { type?: unknown } | undefined)?.type === 'implicitMeTarget'))
647
+ ),
648
+ },
649
+ {
650
+ name: 'possessive-after-query',
651
+ reason:
652
+ "a possessive after a positional query (`the next <div/>'s textContent`), rare in pages; " +
653
+ 'the `of` form (`the textContent of the next <div/>`) is carried',
654
+ matches: (source, types) => types.has('relativePositionalExpression') && /\/>'s\b/.test(source),
655
+ },
656
+ {
657
+ name: 'colon-event-arguments',
658
+ reason: 'a colon-qualified event name with arguments (`send foo:bar(x:42)`), rare in pages',
659
+ matches: (source, types) => types.has('namedArgumentList') && /\w:\w+\(/.test(source),
660
+ },
661
+ {
662
+ name: 'call-chain-command',
663
+ reason: 'a call chain as a command (`bar().foo()`), rare in pages',
664
+ matches: (source, types) => types.has('pseudoCommand') && /\)\.\w+\(/.test(source),
665
+ },
666
+ {
667
+ name: 'rare-heads',
668
+ reason:
669
+ 'handler heads rare in pages: a quoted event name (`on "a-b"`), a filter after ' +
670
+ 'parameters (`on foo(bar)[bar]`), `and on`, a source on each `or` leg, a ' +
671
+ 'parenthesized feature (`(on click …)`)',
672
+ matches: source =>
673
+ /^\s*on\s+"|^\s*on\s+[\w:.-]+\([^)]*\)\[|\bon\s+\w+\s+\d+\s+and\s+on\b|\bfrom\s+\S+\s+or\s+\w+\s+from\b|^\s*\(on\s/.test(
674
+ source
675
+ ),
676
+ },
677
+ {
678
+ name: 'rare-values',
679
+ reason:
680
+ 'value spellings rare in pages: an escaped class name (`.group-\\[…\\]`), a comma ' +
681
+ 'list in a query (`<#d1, #d2/>`), an attribute after a bare or bracketed reference ' +
682
+ '(`it @a`, `my [@a]`), a dotted path before `of`, a unit after parentheses ' +
683
+ '(`(w)px`), `set {…} on <obj>`',
684
+ matches: source =>
685
+ /\\\[|<[^<>]*,[^<>]*\/>|\b(my|its|your)\s+\[@|\bset\s+(it|me|you)\s+@|\b[a-z]\w*\.[a-z]\w*\s+of\b|\)(px|em|rem|%)|\bset\s+\{[^}]*\}\s+on\b/i.test(
686
+ source
687
+ ),
688
+ },
689
+ {
690
+ name: 'rare-commands',
691
+ reason:
692
+ 'commands rare in pages: `measure` of a property list, `pick characters 0 to 3 from …`, ' +
693
+ 'an `if` whose body is empty (`if false end`), a behavior holding a bare command, two ' +
694
+ 'behaviors with no `end` between',
695
+ matches: (source, types, plain) =>
696
+ (types.has('measureCommand') && /\bmeasure\s+\w+\s*,/.test(source)) ||
697
+ someNode(plain, node => node.type === 'pickCommand' && node.variant === 'range') ||
698
+ /\bif\s+\S+\s+end\b/.test(source) ||
699
+ (types.has('behaviorFeature') && types.has('setFeature')) ||
700
+ (source.match(/\bbehavior\b/g)?.length ?? 0) > 1,
592
701
  },
593
702
  ];
594
703
 
@@ -606,28 +715,6 @@ export interface SilentFamily {
606
715
  }
607
716
 
608
717
  export const SILENT_FAMILIES: readonly SilentFamily[] = [
609
- {
610
- name: 'localized-call-arguments',
611
- reason:
612
- "a call's arguments are written in the target language (`sprayInto(أنا)`, " +
613
- '`writeText(قيمة لـ #input)`) and read back raw, so English holds the foreign words',
614
- ids: ['2d6fa6cf27', '2218bab0ca'],
615
- },
616
- {
617
- name: 'localized-dom-names',
618
- reason:
619
- 'a DOM property or type name is translated as a word (`my children` → de `my kinder`, ' +
620
- '`my style[…]` → `my stil […]`, `a Element` → ar `a عنصر`) and not read back',
621
- ids: [
622
- '76bf595d21',
623
- 'a1ca7a74a0',
624
- 'e172bd73af',
625
- 'ddb498424a',
626
- 'dc9ce23e31',
627
- 'bf3ceac332',
628
- '8ecf869842',
629
- ],
630
- },
631
718
  {
632
719
  name: 'value-words',
633
720
  reason:
@@ -645,13 +732,6 @@ export const SILENT_FAMILIES: readonly SilentFamily[] = [
645
732
  '54a65f4602',
646
733
  ],
647
734
  },
648
- {
649
- name: 'else-word',
650
- reason:
651
- "an empty then-branch's `otherwise` (de `andernfalls`, …) is not read as `else` and " +
652
- 'stays a word in the condition',
653
- ids: ['e60afa928a'],
654
- },
655
735
  {
656
736
  name: 'it-locative',
657
737
  reason: 'it `in me` (a query scope) reads back as `on me`',
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Value matrix gate — the `branch` position's operator-phrase cells (`is equal
3
+ * to`, `includes`, `is an Element`, …). See value-matrix.ts for the matrix
4
+ * and value-matrix-gate.ts for the assertions.
5
+ *
6
+ * @vitest-environment node
7
+ * Required, not a preference: under happy-dom the engines bind happy-dom's
8
+ * DOM constructors, not the jsdom window's (see
9
+ * shipped-examples-execution.test.ts).
10
+ */
11
+ import { describeValueMatrixShard } from './value-matrix-gate';
12
+
13
+ describeValueMatrixShard('branch', 'phrases');
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Value matrix gate — the `branch` cells. See value-matrix.ts for the matrix and
3
+ * value-matrix-gate.ts for the assertions; one position per file so the
4
+ * shards run in parallel.
5
+ *
6
+ * @vitest-environment node
7
+ * Required, not a preference: under happy-dom the engines bind happy-dom's
8
+ * DOM constructors, not the jsdom window's (see
9
+ * shipped-examples-execution.test.ts).
10
+ */
11
+ import { describeValueMatrixShard } from './value-matrix-gate';
12
+
13
+ describeValueMatrixShard('branch');
@@ -9,16 +9,17 @@
9
9
  *
10
10
  * This does. A CELL is one value expression in one position:
11
11
  *
12
- * - OPERANDS: eleven kinds (literal, variable, selector, possessive, `of`,
12
+ * - OPERANDS: twelve kinds (literal, variable, selector, possessive, `of`,
13
13
  * dotted, call, array, parens, a reference's property: `event's type`,
14
- * `the id of target`, and a sigil variable: `$n`), each with instances
15
- * of the value types it produces;
14
+ * `the id of target`, a sigil variable: `$n`, and a CSS length: `100px`),
15
+ * each with instances of the value types it produces;
16
16
  * - OPERATORS: arithmetic, comparison, equality, logic, membership, and the
17
17
  * prefix and postfix forms (`not`, `-`, `no`, `is empty`, `is null`,
18
18
  * `exists`, `as`), and core's operator PHRASES (`is greater than or equal
19
19
  * to`, `does not include`, `is an Element`, …);
20
- * - POSITIONS: a `put` value, a `set` value, an `if` condition, a `repeat
21
- * while` condition, a loop's count (`repeat … times`), an `increment …
20
+ * - POSITIONS: a `put` value, a `set` value, an `if` condition, an `if`
21
+ * condition with no `then` before a command that names no target
22
+ * (`branch`), a `repeat while` condition, a loop's count (`repeat … times`), an `increment …
22
23
  * by` amount, a `set` value in the second command of a chain (`chain`),
23
24
  * what a `get` reads (`get`), and two written targets: what a `set`
24
25
  * writes (`assign`) and what an `increment` counts (`count`).
@@ -112,15 +113,27 @@ export type OperandKind =
112
113
  | 'array'
113
114
  | 'parens'
114
115
  | 'reference'
115
- | 'sigil';
116
+ | 'sigil'
117
+ | 'length';
116
118
 
117
119
  export type Position =
118
- 'put' | 'set' | 'if' | 'while' | 'times' | 'increment' | 'chain' | 'get' | 'assign' | 'count';
120
+ | 'put'
121
+ | 'set'
122
+ | 'if'
123
+ | 'branch'
124
+ | 'while'
125
+ | 'times'
126
+ | 'increment'
127
+ | 'chain'
128
+ | 'get'
129
+ | 'assign'
130
+ | 'count';
119
131
 
120
132
  export const POSITIONS: readonly Position[] = [
121
133
  'put',
122
134
  'set',
123
135
  'if',
136
+ 'branch',
124
137
  'while',
125
138
  'times',
126
139
  'increment',
@@ -164,7 +177,7 @@ export interface MatrixCell {
164
177
  */
165
178
  export const FIXTURE =
166
179
  '<div id="out">∅</div>' +
167
- '<p id="a" class="x" title="t1">6</p>' +
180
+ '<p id="a" class="x" title="t1" aria-pressed="true">6</p>' +
168
181
  '<div id="w"><p class="w">w</p><p class="w">v</p></div>' +
169
182
  '<button id="b">b</button>';
170
183
 
@@ -197,12 +210,16 @@ export const OPERANDS: readonly Operand[] = [
197
210
  { kind: 'possessive', type: 'num', text: "obj's v" },
198
211
  { kind: 'possessive', type: 'num', text: "arr's length" },
199
212
  { kind: 'possessive', type: 'num', text: "#a's textContent's length" },
213
+ // A property name a lexicon translates and no reader brings back: es wrote
214
+ // `hijos`, which read back as a property named `hijos`.
215
+ { kind: 'possessive', type: 'num', text: "#w's children's length" },
200
216
  { kind: 'of', type: 'nstr', text: 'textContent of #a' },
201
217
  { kind: 'of', type: 'nstr', text: 'the textContent of #a' },
202
218
  { kind: 'of', type: 'num', text: 'v of obj' },
203
219
  { kind: 'of', type: 'num', text: 'length of arr' },
204
220
  { kind: 'of', type: 'str', text: '@title of #a' },
205
221
  { kind: 'of', type: 'num', text: 'v of w of obj' },
222
+ { kind: 'of', type: 'num', text: 'the length of the children of #w' },
206
223
  { kind: 'of', type: 'arr', text: 'textContent of .w' },
207
224
  { kind: 'dotted', type: 'nstr', text: '#a.textContent' },
208
225
  { kind: 'dotted', type: 'str', text: 'me.id' },
@@ -213,8 +230,15 @@ export const OPERANDS: readonly Operand[] = [
213
230
  { kind: 'call', type: 'nstr', text: 'String(n)' },
214
231
  { kind: 'call', type: 'str', text: 's.toUpperCase()' },
215
232
  { kind: 'call', type: 'bool', text: 'Array.isArray(arr)' },
233
+ // A word a lexicon translates, inside a call's arguments, which every reader
234
+ // takes as written: es `Math.max(n, verdadero)`.
235
+ { kind: 'call', type: 'num', text: 'Math.max(n, true)' },
216
236
  { kind: 'array', type: 'arr', text: '[1, 2]' },
217
237
  { kind: 'array', type: 'arr', text: '[n, 2]' },
238
+ // A word some language's lexicon translates, inside an array in a larger
239
+ // expression: every reader takes a bracket group whole, so a renderer that
240
+ // localized `true` there (es `2 is in [verdadero, n]`) lost it in all 23.
241
+ { kind: 'array', type: 'arr', text: '[true, n]' },
218
242
  { kind: 'parens', type: 'num', text: '(n + 1)' },
219
243
  { kind: 'parens', type: 'str', text: '(s + "c")' },
220
244
  { kind: 'parens', type: 'bool', text: '(n > 1)' },
@@ -235,6 +259,10 @@ export const OPERANDS: readonly Operand[] = [
235
259
  // a variable, but no cell had one: semantic's English dropped a whole loop
236
260
  // counted by one (`repeat $n times`), and every translation with it.
237
261
  { kind: 'sigil', type: 'num', text: '$n' },
262
+ // A number with a CSS unit is one string upstream (`100px`): every reader
263
+ // split it, so `set my *width to 100px` was refused in all 24 languages.
264
+ { kind: 'length', type: 'str', text: '100px' },
265
+ { kind: 'length', type: 'str', text: '50%' },
238
266
  ];
239
267
 
240
268
  /** An operator slot, and the value types that fill it. */
@@ -330,7 +358,16 @@ export const BINARY_OPERATORS: readonly BinaryOperator[] = [
330
358
  representative: true,
331
359
  signatures: [sig('num', 'arr', '2', '[1, 2, 6]', 'bool')],
332
360
  },
333
- { op: 'matches', representative: true, signatures: [sig('one', 'none', '#a', '.x', 'bool')] },
361
+ {
362
+ op: 'matches',
363
+ representative: true,
364
+ // An attribute selector whose value a lexicon translates: es wrote
365
+ // `<[aria-pressed=verdadero]/>`, another selector, in all 23 languages.
366
+ signatures: [
367
+ sig('one', 'none', '#a', '.x', 'bool'),
368
+ sig('one', 'none', '#a', '<[aria-pressed=true]/>', 'bool'),
369
+ ],
370
+ },
334
371
  // Operator PHRASES (the after-85 handoff, part 2): the words each language
335
372
  // renders for them are read back by the join and the sense table, which no
336
373
  // single-word operator exercises. One of each class is representative, so the
@@ -623,6 +660,7 @@ const TEMPLATES: Record<Position, (expression: string) => string> = {
623
660
  put: e => `on click put ${e} into #out`,
624
661
  set: e => `on click set x to ${e} then put x into #out`,
625
662
  if: e => `on click if ${e} then put "Y" into #out else put "N" into #out end`,
663
+ branch: e => `on click if ${e} add .y else add .n end then put my className into #out`,
626
664
  while: e => `on click set i to 0 then repeat while i < ${e} increment i end then put i into #out`,
627
665
  times: e => `on click set i to 0 then repeat ${e} times increment i end then put i into #out`,
628
666
  increment: e => `on click set i to 1 then increment i by ${e} then put i into #out`,
@@ -649,10 +687,18 @@ const TEMPLATES: Record<Position, (expression: string) => string> = {
649
687
  * `it`, but its role took no literal, so English dropped `get "hello"`, `get
650
688
  * 3` and `get true` whole (and every translation with them) while `get n` and
651
689
  * `get #a's textContent` read.
690
+ *
691
+ * A `branch` condition is every `if` condition but the names (none failed
692
+ * there that `if` did not). The `if` cells write `then` and a `put` with its
693
+ * target, so a reader always saw where the condition ended. Without `then`, an
694
+ * SOV language writes the branch's first role right after the condition, and
695
+ * its reader has to find the seam: tr read `if 2 is in arr add .a end` as
696
+ * `if 2 add .a to is in arr end` (its destination marker is optional, and `in`
697
+ * is also one), with every operand, and the `if` cells all passed.
652
698
  */
653
699
  function positionsFor(type: ValueType, bare: boolean, group: MatrixCell['group']): Position[] {
654
700
  const out: Position[] = ['put', 'set'];
655
- if (type === 'bool' || bare) out.push('if');
701
+ if (type === 'bool' || bare) out.push('if', 'branch');
656
702
  if (type === 'num' || type === 'nstr') out.push('while', 'times', 'increment');
657
703
  if (group !== 'operand-operator') out.push('chain', 'get');
658
704
  return out;