claude-mcp-workflow 0.1.8 → 0.1.9

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.
Files changed (56) hide show
  1. package/.claude-plugin/plugin.json +4 -2
  2. package/README.md +37 -0
  3. package/build/engine.d.ts +13 -1
  4. package/build/engine.d.ts.map +1 -1
  5. package/build/engine.js +41 -2
  6. package/build/engine.js.map +1 -1
  7. package/build/types.d.ts +20 -20
  8. package/build/types.js +1 -1
  9. package/build/types.js.map +1 -1
  10. package/hooks/hooks.json +4 -2
  11. package/hooks/workflow-cleanup.sh +8 -4
  12. package/hooks/workflow-start.sh +60 -32
  13. package/package.json +1 -1
  14. package/templates/bug-fix.yaml +98 -16
  15. package/templates/code-review.yaml +30 -12
  16. package/templates/coding.yaml +49 -4
  17. package/templates/debugging.yaml +39 -14
  18. package/templates/explore.yaml +48 -14
  19. package/templates/file-code.yaml +13 -4
  20. package/templates/file-review.yaml +25 -6
  21. package/templates/github-init.yaml +24 -8
  22. package/templates/investigate.yaml +13 -4
  23. package/templates/master.yaml +16 -13
  24. package/templates/new-feature.yaml +24 -26
  25. package/templates/planning.yaml +34 -7
  26. package/templates/reflection.yaml +11 -4
  27. package/templates/review-push.yaml +26 -7
  28. package/templates/skills/architecture/SKILL.md +131 -1
  29. package/templates/skills/aws-lambda/SKILL.md +9 -9
  30. package/templates/skills/browser-verify/SKILL.md +58 -0
  31. package/templates/skills/build-cmake/SKILL.md +24 -8
  32. package/templates/skills/ci-github-actions/SKILL.md +34 -18
  33. package/templates/skills/claude-code-config/SKILL.md +41 -19
  34. package/templates/skills/cleanup/SKILL.md +57 -0
  35. package/templates/skills/coding-skill-selector/SKILL.md +2 -1
  36. package/templates/skills/debug-bridge-scaffold/SKILL.md +98 -0
  37. package/templates/skills/domain-gamedev/SKILL.md +56 -6
  38. package/templates/skills/domain-pixi/SKILL.md +256 -7
  39. package/templates/skills/domain-reid/SKILL.md +39 -5
  40. package/templates/skills/domain-yolo/SKILL.md +18 -7
  41. package/templates/skills/ide-zed/SKILL.md +23 -0
  42. package/templates/skills/lang-as3/SKILL.md +7 -5
  43. package/templates/skills/lang-haxe/SKILL.md +538 -19
  44. package/templates/skills/lang-python/SKILL.md +6 -2
  45. package/templates/skills/math/SKILL.md +64 -2
  46. package/templates/skills/mcp-setup/SKILL.md +14 -2
  47. package/templates/skills/preferences/SKILL.md +10 -10
  48. package/templates/skills/skill-manager/SKILL.md +264 -0
  49. package/templates/skills/target-openfl-native/SKILL.md +52 -17
  50. package/templates/skills/target-openfl-native/references/build-and-versions.md +7 -4
  51. package/templates/skills/task-delegation/SKILL.md +76 -4
  52. package/templates/skills/web-reading/SKILL.md +33 -25
  53. package/templates/skills/workflow-authoring/SKILL.md +47 -12
  54. package/templates/subagent.yaml +29 -8
  55. package/templates/testing.yaml +64 -10
  56. package/templates/web-research.yaml +23 -13
@@ -20,6 +20,19 @@ var plain = "No $interpolation here"; // ✗ Literal text, no parsing
20
20
  - `${expr}` only for expressions (field access, math, function calls)
21
21
  - `'${_name}'` is wrong → `'$_name'`
22
22
 
23
+ ### Escaping a literal `$`: use `$$`, NEVER `\$`
24
+
25
+ To put a literal `$` in a single-quoted string, double it: `$$`. `\$` is **not a valid escape sequence** — `haxe` rejects it with `Invalid escape sequence \$` (the recognized escapes are `\t \n \r \" \' \\ \xXX \uXXXX`; `\$` is not among them).
26
+
27
+ ```haxe
28
+ var a = 'price is $$5'; // ✓ → "price is $5"
29
+ var b = 'unterminated $${ here'; // ✓ → "unterminated ${ here" (escape the $, the { is literal)
30
+ var c = 'cost \$5'; // ✗ COMPILE ERROR: Invalid escape sequence \$
31
+ var d = "cost $5"; // ✓ double quotes never interpolate → literal "cost $5"
32
+ ```
33
+
34
+ So for a literal `$` (or a literal `${`) the two correct forms are **`$$` inside `'...'`** or **just use `"..."`** (no interpolation at all). Reach for double quotes when the string is mostly literal `$`/`${`; use `$$` when you also want interpolation elsewhere in the same string.
35
+
23
36
  ## String.replace() Requires `using StringTools`
24
37
 
25
38
  `String` in Haxe has NO built-in `.replace()` method. Calling `.replace()` without `using StringTools` fails with `String has no field replace`.
@@ -96,7 +109,7 @@ This is distinct from `(default, set)` where the setter IS accessible from subcl
96
109
  Fix: pass narrowed value through a helper with non-null param, or use `.bind()` at call site for value types.
97
110
 
98
111
  ### Strict (`@:nullSafety(Strict)`)
99
- Fields are NEVER narrowed — compiler assumes another thread/callback could modify them. Local variables narrow normally.
112
+ Fields DO narrow right after a null check, but the narrowing is fragile: ANY intervening statement that could mutate state (a call, a write to any field) resets it the compiler assumes another thread/callback could modify the field. In practice a field is only usable as non-null on the line directly after its check. Local variables narrow normally and stay narrowed.
100
113
 
101
114
  **Pattern: capture field into local immediately after null check:**
102
115
  ```haxe
@@ -135,6 +148,15 @@ logo.alpha = 0.3; // ✓
135
148
 
136
149
  **Key rule:** in Strict mode, ANY intervening statement (even writing to a *different* field) can invalidate narrowing of a nullable field. Always capture to local **immediately** after the null check.
137
150
 
151
+ **Null safety with `?.` on abstract method returns:**
152
+ ```haxe
153
+ // WRONG — && narrowing fails for method calls on nullable
154
+ final scrolling:Bool = (_axis != null && _axis.isScrolling());
155
+
156
+ // RIGHT — safe navigation + explicit comparison
157
+ final scrolling:Bool = (_axis?.isScrolling() == true);
158
+ ```
159
+
138
160
  ## Map Key-Value Iteration
139
161
 
140
162
  Use `for (key => value in map)` — cleaner than iterating keys and looking up values separately. Value is guaranteed non-null.
@@ -195,6 +217,42 @@ if (bmd != null) bmd.image.version++;
195
217
 
196
218
  Same applies to `--`, `+=`, `-=` on the end of a `?.` chain.
197
219
 
220
+ ## `Sys.stdin().readAll()` Throws `Error.Blocked` on hxnodejs When Stdin Is a Pipe
221
+
222
+ `Sys.stdin().readAll()` works on neko / eval, but on hxnodejs (`-lib hxnodejs`, `-js`) it raises `haxe.io.Error.Blocked` the moment stdin is a pipe — `echo … | cmd`, heredoc, `cmd <<< …`, process substitution. The hxnodejs sync stdin binding wraps a non-blocking `process.stdin._handle.read()` call and re-throws `EAGAIN` as `Blocked` instead of waiting for EOF. TTY mode happens to work because input arrives in one synchronous chunk; the moment the producer is anything other than a TTY (`/dev/tty`), the call fails before any bytes are consumed.
223
+
224
+ ```haxe
225
+ // WRONG on hxnodejs — `echo 'class C {}' | node bin/cli.js` throws Blocked
226
+ final src:String = Sys.stdin().readAll().toString();
227
+
228
+ // RIGHT — read via Node's native fs.readFileSync(0), which IS blocking
229
+ // on a pipe and returns all bytes through EOF.
230
+ private static function readStdin():String {
231
+ #if nodejs
232
+ final fs:Dynamic = js.Lib.require('fs');
233
+ final buf:Dynamic = fs.readFileSync(0);
234
+ return (buf : Dynamic).toString('utf8');
235
+ #elseif sys
236
+ return Sys.stdin().readAll().toString();
237
+ #else
238
+ throw 'stdin requires a sys target';
239
+ #end
240
+ }
241
+ ```
242
+
243
+ The two-branch shape is required: neko / eval do not have `js.Lib.require`, and hxnodejs is the only target on which `Sys.stdin().readAll()` raises Blocked. The Node path uses untyped `Dynamic` because `js.Lib.require('fs')` returns the raw Node module surface — `fs.readFileSync(0)` returns a Node `Buffer` whose `.toString('utf8')` decodes the bytes.
244
+
245
+ Symptom in test output:
246
+ ```
247
+ haxe_ValueException [Error]: Blocked
248
+ at haxe_Exception.thrown (...:50993:12)
249
+ at _$Sys_FileInput.readAll (...:285:27)
250
+ at <yourReadStdin> (...:46538:33)
251
+ value: { _hx_name: 'Blocked', _hx_index: 0, __enum__: 'haxe.io.Error', ... }
252
+ ```
253
+
254
+ `Error.Blocked` from a `Sys.stdin().readAll()` frame ≡ this gotcha. Verified on Node 20.18 + Haxe 4.3.7 + hxnodejs 12.x.
255
+
198
256
  ## `String.charCodeAt` Returns `Null<Int>` — Use `StringTools.fastCodeAt` Under Strict Null Safety
199
257
 
200
258
  Under `@:nullSafety(Strict)`, `String.charCodeAt(i)` is typed `Null<Int>`. Assigning it to `final c:Int` fails even inside a bounds-checked loop.
@@ -213,6 +271,42 @@ for (i in 0...s.length) {
213
271
 
214
272
  Use `charCodeAt` only when the caller genuinely may pass an out-of-bounds index and wants `null` back. For loops over `0...s.length`, always use `fastCodeAt`.
215
273
 
274
+ Sister pattern: `Array.pop()` / `Array.shift()` have the same root cause — see below.
275
+
276
+ ## Array.pop() / shift() Return Null<T> — Strict Refuses to Narrow Despite Length Guard
277
+
278
+ Under `@:nullSafety(Strict)`, `Array.pop()` and `Array.shift()` are typed `Null<T>`. The classic length-guarded stack-walker pattern fails to compile with `Cannot assign nullable value here` — the compiler does not narrow based on the runtime invariant proved by `stack.length > 0`.
279
+
280
+ ```haxe
281
+ // WRONG — Null<T> cannot be assigned to T under Strict, even with the length guard
282
+ while (stack.length > 0) {
283
+ final node:T = stack.pop(); // Null safety error
284
+ }
285
+ ```
286
+
287
+ Three valid fixes:
288
+
289
+ **Option 1 — cast at assignment** (guard already proved correctness):
290
+ ```haxe
291
+ final node:T = (cast stack.pop() : T);
292
+ ```
293
+
294
+ **Option 2 — capture as `Null<T>` and break on null**:
295
+ ```haxe
296
+ final node:Null<T> = stack.pop();
297
+ if (node == null) break;
298
+ ```
299
+
300
+ **Option 3 — drop `@:nullSafety(Strict)` from the walker class** (lowest friction when the file's only nullability concern is this stdlib accessor):
301
+ ```haxe
302
+ @:nullSafety // Basic, not Strict
303
+ class MyWalker { ... }
304
+ ```
305
+
306
+ Same root cause as `String.charCodeAt` returning `Null<Int>`: stdlib accessor is typed nullable for the out-of-bounds case; Strict has no mechanism to narrow on a runtime-proven invariant. Option 3 is the right default when Strict doesn't otherwise benefit the class; use option 1 or 2 when Strict genuinely matters for the rest of the file.
307
+
308
+ Verified on Haxe 4.3.7.
309
+
216
310
  ## Enum Abstract for Simple Enums
217
311
 
218
312
  For marker enums without associated data, use `enum abstract(Int)` — zero-cost at runtime, compiles to a primitive. Use `final` instead of `var` for values (immutable by intent).
@@ -299,6 +393,33 @@ super(items, 30, 30, -1, 1, 0, "end"); // ✗ Hardcoding someone else's default
299
393
 
300
394
  **DANGER: Float constant passed to Int parameter silently skips it.** An `inline final X:Float = 180` looks like an integer but is typed Float. When passed to `new Row(children, X, 16, ...)` where `boxWidth:Int`, the Float skips `boxWidth` and lands on the next Float parameter (e.g. `bgAlpha`). Fix: use `Int` type for constants passed to Int parameters, or cast with `Std.int()`.
301
395
 
396
+ ## Function Default Parameter Values Must Be Compile-Time Constants — Enum Ctors With Args Excluded
397
+
398
+ Default values in ordinary function signatures (`param:T = default`) must be compile-time constants. Zero-arity enum constructors (e.g. `Empty`) qualify and DO work. Enum constructors that take arguments do NOT — even when every argument is itself a literal constant. The compiler rejects with `Default argument value should be constant`.
399
+
400
+ ```haxe
401
+ enum Foo { A; B(s:String); }
402
+
403
+ // WRONG — B('hi') is not a compile-time constant despite the literal arg
404
+ function test(x:Foo = B('hi')):Foo return x;
405
+ // ^^^^^^^ Default argument value should be constant
406
+
407
+ // WRONG — same root cause, Line('\n') / Text("x") rejected
408
+ function emit(?items:Array<Doc>, trailBreak:Doc = Line('\n')):Doc { ... }
409
+
410
+ // RIGHT — make the param nullable, unwrap inside the body with `??`
411
+ function emit(?items:Array<Doc>, ?trailBreak:Doc):Doc {
412
+ final trailBreakDoc:Doc = trailBreak ?? Line('\n');
413
+ ...
414
+ }
415
+
416
+ // RIGHT — zero-arity ctor IS allowed as default
417
+ enum Mode { Empty; Filled(s:String); }
418
+ function f(m:Mode = Empty):Void { ... } // ✓
419
+ ```
420
+
421
+ Verified on Haxe 4.3.7.
422
+
302
423
  ## Macro `Context.error` Halts the Macro — No Defensive `continue` Needed
303
424
 
304
425
  `haxe.macro.Context.error(msg, pos)` throws — execution does NOT fall through. The return type is `Dynamic` only because the function never returns normally. Subsequent statements in the same macro invocation are dead code.
@@ -329,7 +450,7 @@ for (field in fields) {
329
450
  }
330
451
  ```
331
452
 
332
- Downside of the minimal form: the first invalid field halts the whole build, so the user sees one error per compile cycle instead of a batch. If you want **batched** errors across multiple fields, use `Context.warning` (doesn't throw) plus a post-loop `Context.error` / `Context.fatalError`, or collect errors into an array and emit a summary at the end.
453
+ Downside of the minimal form: the first invalid field halts the whole build, so the user sees one error per compile cycle instead of a batch. For **batched** errors across multiple fields, the right API is `Context.reportError(msg, pos)` — it records the error and continues (compilation still fails at the end), so every invalid field is reported in one compile cycle. Follow with a post-loop `Context.error` / `Context.fatalError` only if you must abort before further processing.
333
454
 
334
455
  ## Macro `FunctionArg`: `opt: true` Widens Type To `Null<T>` Even With a Default
335
456
 
@@ -488,6 +609,109 @@ switch (value) { ... }
488
609
  switch value { ... }
489
610
  ```
490
611
 
612
+ ## Abstract Types in Catch Clauses: Runtime Type Is the Underlying Type
613
+
614
+ `catch` accepts abstract types. The runtime check is against the abstract's **underlying type**, not its compile-time conversions. `from` declarations do NOT widen the runtime catch behavior.
615
+
616
+ - `abstract Foo(Dynamic) from Dynamic` — catches **everything** (class instances, anonymous structs, plain strings) because the underlying runtime check is on `Dynamic`.
617
+ - `abstract Foo(SomeClass) from SomeClass from Dynamic` — catches **only** runtime instances of `SomeClass`. The compile-time `from Dynamic` does not widen what is caught at runtime — anonymous structs and unrelated values fall through.
618
+
619
+ ```haxe
620
+ abstract MyErr(Dynamic) from Dynamic {
621
+ public var name(get, never):String;
622
+ inline function get_name():String return this.name;
623
+ }
624
+
625
+ class Test {
626
+ static function main():Void {
627
+ try { throw { name: 'IOError' }; }
628
+ catch (e: MyErr) { trace('caught: ' + e.name); } // works — anon struct caught
629
+ }
630
+ }
631
+ ```
632
+
633
+ Practical pattern: `abstract MyError(Dynamic) from Dynamic` as a typed unified catch surface that catches every thrown shape, with typed accessors via getters.
634
+
635
+ Verified on Haxe 4.x — `--interp` and `-js` targets.
636
+
637
+ ## `this` Inside an Abstract Body IS the Underlying Type
638
+
639
+ Inside abstract methods and getters, `this` is already typed as the **underlying type**. No cast is needed.
640
+
641
+ ```haxe
642
+ // WRONG — redundant cast; this is already Dynamic
643
+ abstract MyAbs(Dynamic) from Dynamic {
644
+ public var name(get, never):String;
645
+ inline function get_name():String return (this:Dynamic).name; // noise
646
+ }
647
+
648
+ // RIGHT — direct field access; this IS Dynamic
649
+ abstract MyAbs(Dynamic) from Dynamic {
650
+ public var name(get, never):String;
651
+ inline function get_name():String return this.name;
652
+ }
653
+ ```
654
+
655
+ Same for any underlying type: `abstract Foo(SomeClass)` makes `this` typed as `SomeClass` inside the body — field access goes direct.
656
+
657
+ **Bonus: `to UnderlyingType` is implicit.** `abstract Foo(Bar) to Bar` — the `to Bar` direction is redundant; the compiler provides it automatically. Drop it.
658
+
659
+ ```haxe
660
+ // REDUNDANT
661
+ abstract MyAbs(Dynamic) from Dynamic to Dynamic { ... }
662
+
663
+ // EQUIVALENT — less noise
664
+ abstract MyAbs(Dynamic) from Dynamic { ... }
665
+ ```
666
+
667
+ `from Dynamic` IS load-bearing (allows implicit conversion into the abstract from any value). `to Dynamic` is automatic.
668
+
669
+ ## Abstract over Dynamic Does NOT Auto-Convert to Typedef Without Explicit `to`
670
+
671
+ `abstract Foo(Dynamic) from Dynamic` accepts any value as input (catch-all), but does NOT automatically convert to a `typedef` of an anonymous struct on output. The compiler treats abstract → typedef as a distinct conversion that must be declared with `to TypedefName`.
672
+
673
+ ```haxe
674
+ // typedef in an extern web-API library
675
+ typedef WebAPIError = { code: Float; name: String; message: String; };
676
+
677
+ abstract MyAbs(Dynamic) from Dynamic { ... }
678
+
679
+ function logError(e: WebAPIError): Void { /* uses e.code, e.name, e.message */ }
680
+
681
+ final m: MyAbs = ...;
682
+ logError(m); // ❌ Compile error:
683
+ // pkg.MyAbs should be webapi.WebAPIError
684
+ // For function argument 'e'
685
+ ```
686
+
687
+ The fix is to declare `to WebAPIError` on the abstract:
688
+
689
+ ```haxe
690
+ abstract MyAbs(Dynamic) from Dynamic to WebAPIError { ... } // ✓ now flows
691
+ ```
692
+
693
+ This is asymmetric with the `from Dynamic` direction — Dynamic → typedef works at value level (structural matching), but abstract → typedef requires an explicit declaration. Failing to add `to` triggers a "viral retyping" cascade: every helper that took the typedef seemingly needs to accept the abstract, which is the wrong fix. The right fix is the one-line `to`.
694
+
695
+ ## Don't Propagate the Abstract Into Signatures That Only Need the Underlying Structural Shape
696
+
697
+ When adding an abstract over an existing typedef (e.g. `DeviceError` over `WebAPIError`), the temptation is to retype every existing helper to accept the abstract. Resist.
698
+
699
+ If a function only uses fields like `.code`, `.name`, `.message` — fields the typedef already has — its signature should stay on the typedef. The abstract auto-converts at call sites (with `to` declared per the section above). Propagating the abstract into structural-only helpers couples them to the unified-type concept needlessly.
700
+
701
+ ```haxe
702
+ // WRONG — abstract propagated into a pure formatter that only needs {code, name, message}
703
+ private function deviceError(err: DeviceError): Void {
704
+ error('${err.code} ${err.name} ${err.message}');
705
+ }
706
+
707
+ // RIGHT — typedef captures the structural need; DeviceError -> WebAPIError via `to`
708
+ private function deviceError(err: WebAPIError): Void {
709
+ error('${err.code} ${err.name} ${err.message}');
710
+ }
711
+ ```
712
+
713
+ Rule of thumb: type the parameter at the level of structural need, not at the level of the most-typed source. The abstract's job is auto-conversion at the boundary — it should NOT bleed into every internal helper.
714
+
491
715
  ## Type Checking: `is` Operator and `Std.downcast`
492
716
 
493
717
  `is` is syntactic sugar for `Std.isOfType` — cleaner syntax for type checks:
@@ -599,15 +823,6 @@ else
599
823
 
600
824
  **General rule:** in any context where the branch body starts with `macro if(...)`, prefer `?:` over `if/else` to avoid ambiguity.
601
825
 
602
- **Null safety with `?.` on abstract method returns:**
603
- ```haxe
604
- // WRONG — && narrowing fails for method calls on nullable
605
- final scrolling:Bool = (_axis != null && _axis.isScrolling());
606
-
607
- // RIGHT — safe navigation + explicit comparison
608
- final scrolling:Bool = (_axis?.isScrolling() == true);
609
- ```
610
-
611
826
  ## Private Access: `@:access` Over `@:privateAccess`
612
827
 
613
828
  Prefer `@:access(package.ClassName)` over `@:privateAccess` blocks. `@:access` is scoped to a specific class's internals — precise and doesn't clutter method bodies. `@:privateAccess` unlocks ALL private fields of ALL types — overly permissive.
@@ -739,9 +954,39 @@ Dollar;
739
954
 
740
955
  **Rule:** Always use double-quoted strings for metadata arguments containing `$`. This applies to all `@:` metadata, not just specific tags.
741
956
 
742
- ## Doc-Comments `/** */` Reject Embedded `*/` Even Inside Backticks
957
+ ## Test Fixture Strings With `$identifier` Content Must Use Double Quotes
958
+
959
+ Single-quoted strings in test files are subject to Haxe interpolation — any `$name` or `${...}` in a parser fixture input or an `Assert.equals` expected literal is treated as an identifier reference, not verbatim text. The compiler error is a **compile-time `Unknown identifier : <name>`** pointing at the call argument, which can mislead: the error looks like a scope or import problem, not a quoting mistake.
960
+
961
+ ```haxe
962
+ // WRONG — single quotes: Haxe interpolates $name as an identifier
963
+ final decl = parseSingleVarDecl('class Foo { var x:Int = a.$name; }');
964
+ Assert.equals('$name', (f : String));
965
+ // Error: Unknown identifier : name
966
+ // ... For function argument 'source'
967
+ // Error: Unknown identifier : name
968
+ // ... For function argument 'expected'
969
+
970
+ // RIGHT — double quotes: $ is literal text
971
+ final decl = parseSingleVarDecl("class Foo { var x:Int = a.$name; }");
972
+ Assert.equals("$name", (f : String));
973
+ ```
974
+
975
+ Verbatim compiler error for reference:
976
+ ```
977
+ HxPostfixSliceTest.hx:92: characters 73-77 : Unknown identifier : name
978
+ HxPostfixSliceTest.hx:92: characters 73-77 : ... For function argument 'source'
979
+ HxPostfixSliceTest.hx:96: characters 21-25 : Unknown identifier : name
980
+ HxPostfixSliceTest.hx:96: characters 21-25 : ... For function argument 'expected'
981
+ ```
982
+
983
+ The failure bites BOTH the fixture input string and the expected-value literal in `Assert.equals`. It survives self-review because `$name` in a string LOOKS intentional — it is reliably caught only by the compiler.
984
+
985
+ **Prevention rule:** before building, scan newly-added or edited `.hx` test code for single-quoted string literals whose content contains `$` followed by a letter, underscore, or `{`; each is a bug unless interpolation is actually intended. The scan must cover **every** test file touched in the same change, not only the file currently in focus — a recurrence happened where the rule was applied correctly in one new test file but missed in a sibling file edited in the same change, caught only by file-review.
743
986
 
744
- Inside a `/** ... */` doc comment the lexer treats the first `*/` it sees as the closing delimiter even when wrapped in backticks for markdown code-span formatting. Everything after the premature close is invalid syntax.
987
+ ## Block Comments Reject Embedded `*/` — Even Inside Backticks
988
+
989
+ Inside any `/* ... */` block comment (doc comments `/** */` included) the lexer treats the first `*/` it sees as the closing delimiter — block comments do not nest, and backtick-wrapping for markdown code-span formatting does not escape it. Everything after the premature close is invalid syntax.
745
990
 
746
991
  ```haxe
747
992
  // WRONG — the inner */ closes the doc comment; ` after it is an "Invalid character"
@@ -757,7 +1002,7 @@ class Foo {} // Error: Invalid character '`'
757
1002
  class Foo {}
758
1003
  ```
759
1004
 
760
- The gotcha applies only to `/** */` doc comments. Plain `/* */` block comments use a different lexer path and tolerate backtick-wrapped `*/` sequences. Simple backticks (`` `foo` ``, `` `//` ``) inside doc comments are fine — the problem is specifically the `*/` character sequence appearing by any means inside a `/** */` comment.
1005
+ The gotcha applies to ALL block comments plain `/* */` and `/** */` doc comments alike; the lexer closes the comment at the first `*/` regardless of surrounding backticks. Simple backticks (`` `foo` ``, `` `//` ``) inside comments are fine — the problem is specifically the `*/` character sequence appearing by any means inside a block comment.
761
1006
 
762
1007
  Verified on Haxe 4.3.7.
763
1008
 
@@ -791,21 +1036,85 @@ params.push(macro $i{dep.paramName}); // Unknown identifier: _di_storage
791
1036
  params.push({expr: EConst(CIdent(dep.paramName)), pos: Context.currentPos()});
792
1037
  ```
793
1038
 
794
- ## Macro `$a{}` Splices Arguments, Not Array Literal
1039
+ ## Single Combined `EVars` Splices Into Outer Scope (Workaround for `$b{}` Isolation)
1040
+
1041
+ When a macro helper must declare N vars that the caller's sibling code can read, returning `Array<Expr>` of separate `EVars` and splicing via `$b{}` doesn't help — the block still isolates. The fix: fold all `Var` entries into ONE `EVars(Array<Var>)` node and splice it as a single `$expr`.
1042
+
1043
+ ```haxe
1044
+ // WRONG — block wraps vars in new scope, caller siblings can't see them
1045
+ function initVars():Expr {
1046
+ return macro { var _prev0:Int = 0; var _prev1:Int = 0; };
1047
+ }
1048
+ // caller: ${initVars()}; — _prev0/_prev1 NOT visible to siblings
1049
+
1050
+ // RIGHT — single EVars node, vars declared at caller's EBlock level
1051
+ function initVars(names:Array<String>):Expr {
1052
+ final vars:Array<haxe.macro.Expr.Var> = [
1053
+ for (n in names) {name: n, type: macro:Int, expr: macro 0}
1054
+ ];
1055
+ return vars.length > 0
1056
+ ? {expr: EVars(vars), pos: Context.currentPos()}
1057
+ : (macro {});
1058
+ }
1059
+ // caller:
1060
+ final initPrev:Expr = initVars(['_prev0', '_prev1']);
1061
+ return macro { $initPrev; /* _prev0 / _prev1 VISIBLE here */ };
1062
+ ```
1063
+
1064
+ **Why it works:** a single `EVars` spliced into a parent `EBlock` IS the same form as writing `var a = 0; var b = 0;` as sibling statements — no extra nesting. `EBlock` always creates a new scope; `EVars` is just a multi-declaration statement.
1065
+
1066
+ **Decision rule:** N var declarations that siblings must read → fold to one `EVars(Array<Var>)`; assignments/mutations of already-declared vars → `EBlock` is fine (nested scope reads outer vars normally). Empty list → `macro {}`.
1067
+
1068
+ ## Macro `$a{}` Is Context-Dependent: Splices Call Arguments, Builds an Array Literal Standalone
795
1069
 
796
- In macro function calls, `$a{arr}` splices array elements as separate arguments, NOT as an array literal. So `f($a{[x, y, z]})` becomes `f(x, y, z)` — three separate arguments, not a single array argument.
1070
+ In a call-argument position, `$a{arr}` splices array elements as separate arguments, NOT as an array literal: `f($a{[x, y, z]})` becomes `f(x, y, z)` — three separate arguments. In a standalone expression position, `$a{arr}` builds an array literal: `macro $a{parts}` yields `[x, y, z]` (an `EArrayDecl`).
797
1071
 
798
1072
  ```haxe
799
1073
  // WRONG — splices as separate args: _dc(doc1, doc2, doc3)
800
1074
  final parts:Array<Expr> = [exprA, exprB, exprC];
801
1075
  macro _dc($a{parts})
802
1076
 
803
- // RIGHT — build array expression manually
804
- final arr:Expr = {expr: EArrayDecl(parts), pos: Context.currentPos()};
1077
+ // RIGHT — build the array literal first (standalone $a{} = array literal), then pass it
1078
+ final arr:Expr = macro $a{parts}; // [doc1, doc2, doc3]
805
1079
  macro _dc($arr) // generates: _dc([doc1, doc2, doc3])
1080
+
1081
+ // EQUIVALENT — construct EArrayDecl manually
1082
+ final arr:Expr = {expr: EArrayDecl(parts), pos: Context.currentPos()};
1083
+ macro _dc($arr)
806
1084
  ```
807
1085
 
808
- `$a{}` is designed for splicing arguments into function calls (like spread operator), not for building array literals. When you need to pass a macro-time `Array<Expr>` as a single runtime array argument, construct `EArrayDecl` manually.
1086
+ When you need a macro-time `Array<Expr>` as a single runtime array argument, wrap it into an array-literal `Expr` first — inside a call's argument list, `$a{}` always splices.
1087
+
1088
+ ## Macro `Array<Expr>` Literal — Parenthesise Each `macro …` Element
1089
+
1090
+ Inside an `Array<Expr>` literal (for `EBlock` / `ECall` / `EArrayDecl` construction in build macros), bare `macro …` reifications must be wrapped in parentheses. Without them, the Haxe parser treats `macro` as an identifier for the next array slot and fails with:
1091
+
1092
+ ```
1093
+ Keyword macro cannot be used as variable name
1094
+ ```
1095
+
1096
+ Plain `Expr` variables (already-built values) do NOT need parens — only `macro …` reifications.
1097
+
1098
+ ```haxe
1099
+ // WRONG — bare macro reification as array element
1100
+ final block:Array<Expr> = [
1101
+ macro final _wo = _copyOpt(opt),
1102
+ macro { var _f:Bool = false; $probeBody; },
1103
+ baseRawWriteCall,
1104
+ ];
1105
+ // Error: Keyword macro cannot be used as variable name
1106
+
1107
+ // RIGHT — each macro reification parenthesised
1108
+ final block:Array<Expr> = [
1109
+ (macro final _wo = _copyOpt(opt)),
1110
+ (macro { var _f:Bool = false; $probeBody; }),
1111
+ baseRawWriteCall, // plain Expr variable — no parens needed
1112
+ ];
1113
+ ```
1114
+
1115
+ Ternary positions like `cond ? macro X : macro Y` bind correctly without parens — the issue is specific to comma-separated array literal elements.
1116
+
1117
+ Verified on Haxe 4.3.7.
809
1118
 
810
1119
  ## Enum Constructor Calls in `macro {}` Trigger Type Checking
811
1120
 
@@ -856,6 +1165,39 @@ private static function extractInt(texpr:TypedExpr):Int {
856
1165
 
857
1166
  `FEnum` only works for real `enum` types. For `enum abstract(Int)`, the compiler resolves values at typing time — the typed AST contains the raw Int, not a reference to the abstract's field.
858
1167
 
1168
+ ## Regex Alternation: `^A|B` Is `(^A)|B`, NOT `^(?:A|B)` — Wrap Both Alts in a Non-Capturing Group
1169
+
1170
+ Regex alternation has lower precedence than every other operator including anchors. `^A|B` parses as `(^A)|B` — the `^` anchor binds ONLY to the first alternative. The second alt is unanchored and scans the rest of input for an arbitrary match anywhere — silently consuming mid-buffer bytes that the parser thought it was inspecting at the cursor.
1171
+
1172
+ ```haxe
1173
+ // WRONG — second alt scans mid-buffer
1174
+ var re = new EReg('^[0-9]+\\.[0-9]+|[0-9]+\\.(?![\\w.])', '');
1175
+ re.match('UI.get() ? 1. : 2.;');
1176
+ // re.matched(0) == '1.' (matched at offset 11, NOT at start)
1177
+ // re.matchedPos().pos == 11 (NOT 0)
1178
+ // Consumer that does `ctx.pos += re.matched(0).length` advances by 2
1179
+ // but the matched bytes started 11 positions away — overwriting the
1180
+ // ident at the parser's actual cursor with the mid-buffer slice.
1181
+
1182
+ // RIGHT — both alts inside a non-capturing group, so `^` binds to both
1183
+ var re = new EReg('^(?:[0-9]+\\.[0-9]+|[0-9]+\\.(?![\\w.]))', '');
1184
+ re.match('UI.get() ? 1. : 2.;');
1185
+ // re.match returns false — `U` is not a digit, regex fails to match at start.
1186
+ ```
1187
+
1188
+ **Defensive runtime check** for any code that builds regexes from `|`-alternations dynamically (or accepts user-supplied patterns): after `re.match(rest)`, verify `re.matchedPos().pos == 0`. If not, treat as no-match — the regex matched something but NOT at the cursor position.
1189
+
1190
+ ```haxe
1191
+ if (!re.match(_rest) || re.matchedPos().pos != 0) {
1192
+ // either no match, or mid-buffer match — both are "not here"
1193
+ throw new ParseError(...);
1194
+ }
1195
+ ```
1196
+
1197
+ **Symptom**: parser produces an AST where two nodes have identical source spans (`@from-to` ranges) at positions where one ident and one literal should sit. The mid-buffer match overwrites the cursor position, the cursor advances by the wrong amount, and the next parse step sees corrupted input. Reproducible only when the second alt's pattern HAPPENS to match somewhere later in the input — easy to miss in narrow unit tests.
1198
+
1199
+ Verified on Haxe 4.3.7, JS / EReg path.
1200
+
859
1201
  ## `EReg.escape` Is Broken on `--interp` Target
860
1202
 
861
1203
  `EReg.escape` on Haxe `--interp` does NOT escape closing parens/brackets (likely more chars). Feeding the result into `new EReg(...)` produces a PCRE error `at N: unmatched closing parenthesis` at regex construction — before any match runs. Works correctly on `neko` and `js`.
@@ -1008,6 +1350,32 @@ return { name: _r_name }; // ok
1008
1350
 
1009
1351
  Emit one `final _r_X:T = _f_X` re-bind per required struct field in macro-generated parse functions that build anonymous struct literals from nullable accumulator variables.
1010
1352
 
1353
+ ## Macro Dead-Code `$v{flag}` Branches Still Type-Check — `cast` Required for Reflective Calls
1354
+
1355
+ A `$v{flag}` compile-time bool short-circuits at **runtime**, but the compiler type-checks the whole expression **before** dead-code elimination. A reflective call like `Type.enumParameters(x)` inside a `$v{forceInlineSep}`-gated `else if` fails the build whenever the macro inlines the call across consumers whose element type is a struct, not `EnumValue`:
1356
+
1357
+ ```
1358
+ WriterLowering.hx:8365: characters 30-48 : pkg.grammar.haxe.trivia.HxMemberDeclT should be EnumValue
1359
+ WriterLowering.hx:8365: characters 30-48 : ... For function argument 'e'
1360
+ ```
1361
+
1362
+ ```haxe
1363
+ // WRONG — type-check fails on struct-shaped element types even though
1364
+ // the branch is never executed when forceInlineSep == false
1365
+ } else if (_si > 0 && $v{forceInlineSep}
1366
+ && Type.enumParameters(_arr[_si - 1].node).length == 0
1367
+ && Type.enumParameters(_t.node).length == 0) {
1368
+
1369
+ // RIGHT — cast suppresses compile-time type-check; runtime is gated by $v{flag}
1370
+ } else if (_si > 0 && $v{forceInlineSep}
1371
+ && Type.enumParameters(cast _arr[_si - 1].node).length == 0
1372
+ && Type.enumParameters(cast _t.node).length == 0) {
1373
+ ```
1374
+
1375
+ **Rule of thumb:** in macro-generated code, any reflective call (`Type.enumParameters`, `Type.getClass`, `Reflect.field`, …) inside a `$v{...}`-gated branch needs `cast` on the argument when the surrounding engine inlines the call across consumers with different static types. The flag short-circuit is a runtime gate, not a compile-time one.
1376
+
1377
+ Verified on Haxe 4.3.7.
1378
+
1011
1379
  ## Helper Signatures Cannot Reference `Context.defineModule`-Synth Sub-Module Types
1012
1380
 
1013
1381
  A test/consumer class trying to "force" a `@:build`-generated synth module via the `private static final _force:Class<MarkerClass> = MarkerClass;` pattern works for FQN references in METHOD BODIES, but NOT for FQN references in HELPER METHOD SIGNATURES of the same class. Method-signature typing precedes static-initializer execution, so the synth module isn't registered when the helper's parameter / return types are resolved.
@@ -1041,6 +1409,24 @@ This is a stricter cousin of the existing "macro-synth sub-module imports don't
1041
1409
 
1042
1410
  Verified on Haxe 4.3.7.
1043
1411
 
1412
+ ## Inline Local Functions Cannot Be Passed as Arguments
1413
+
1414
+ `inline function` on a local variable is a direct-call optimization only. The compiler cannot materialize a closure object for an inline-tagged local, so passing it as an argument to another function fails with `Cannot create closure on inline closure`.
1415
+
1416
+ ```haxe
1417
+ // WRONG — Cannot create closure on inline closure
1418
+ inline function evalAt(x:Int):Int { return x * 2; }
1419
+ helper(evalAt); // Error: Cannot create closure on inline closure
1420
+
1421
+ // RIGHT — drop `inline`; direct calls at the same site are still inlined by the optimizer
1422
+ function evalAt(x:Int):Int { return x * 2; }
1423
+ helper(evalAt);
1424
+ ```
1425
+
1426
+ This surfaces when factoring local helpers (e.g. a recursive `buildXTree`-style function that accepts a callback) and the helper was initially tagged `inline` per project style. `inline` is correct for locals called directly; remove it when the local needs to cross a function boundary as a parameter.
1427
+
1428
+ Verified on Haxe 4.3.7.
1429
+
1044
1430
  ## `for (x:Type in array)` — Type Annotations Are NOT Allowed In For Loops
1045
1431
 
1046
1432
  Haxe's `for` loop iteration variable cannot carry an explicit type annotation. Unlike `var x:T = ...` or function parameters, the loop variable's type is always inferred from the iterable. Adding `:Type` produces `Expected )`.
@@ -1062,3 +1448,136 @@ for (member in iface.members) {
1062
1448
 
1063
1449
  Same restriction applies to array-comprehension `[for (x in xs) ...]`. The "always specify types explicitly" rule (when applied as a project preference) does not apply to for-loop iteration variables — there is no syntax for it.
1064
1450
 
1451
+ ## Adding Enum Ctor: Grep Sister Ctor Literal Across Project
1452
+
1453
+ Haxe's exhaustive switch detection fires `Unmatched patterns: <NewCtor>` at every switch missing an arm — but ONLY when that switch is reached during compile. Pre-flight grepping by function name (e.g. "find all walker functions") misses switches with non-obvious names. The reliable audit: grep on a sister-ctor literal that is already exhaustively handled.
1454
+
1455
+ ```haxe
1456
+ // Adding `IfLineExceeds` to Doc enum.
1457
+ // WRONG audit: "find all flat-walking helpers"
1458
+ // → finds 6 sites, missed 3 hardline-checking helpers in same file
1459
+ // → build breaks with "Unmatched patterns: IfLineExceeds"
1460
+
1461
+ // RIGHT audit: grep on existing sister ctor
1462
+ // $ grep -rn 'case IfWidthExceeds' src/
1463
+ // → enumerates ALL 11 exhaustive switch sites uniformly
1464
+ ```
1465
+
1466
+ **Rule**: when adding a new ctor to an enum used in multiple files, grep `case <SisterCtor>` on an existing exhaustively-handled ctor — this catches every switch regardless of function/variable name. Do this BEFORE committing the ctor; otherwise compile errors fire only when callers reach the unmatched switch and may surface late in the test cycle.
1467
+
1468
+ Verified on Haxe 4.3.7.
1469
+
1470
+ ## Enum Constructor Parameters Cannot Have Metadata — Use a Typedef Instead
1471
+
1472
+ Haxe does NOT allow field-level metadata on individual enum constructor parameters. Attempting `Ctor(a:Int, @:lead("{") b:String)` is a parse-time error — `Unexpected @` — before any macro or typing runs.
1473
+
1474
+ ```haxe
1475
+ // WRONG — parse error: Unexpected @ at the @ before the param
1476
+ enum E {
1477
+ Ctor(a:Int, @:lead("{") b:String);
1478
+ }
1479
+
1480
+ // RIGHT — wrap params in a typedef; metadata is legal on typedef/anon-struct var fields
1481
+ typedef CtorBody = {
1482
+ var a:Int;
1483
+ @:lead("{") var b:String;
1484
+ };
1485
+ enum E {
1486
+ Ctor(v:CtorBody);
1487
+ }
1488
+
1489
+ // ALSO RIGHT — metadata IS allowed on the constructor itself (not its params)
1490
+ enum E {
1491
+ @:deprecated Ctor(a:Int, b:String);
1492
+ }
1493
+ ```
1494
+
1495
+ The restriction bites `@:build` / PEG-style DSLs that drive codegen from per-field metadata: the fix is the **ctor-wraps-typedef** pattern (one struct typedef per constructor), which also keeps per-field metadata working for macro inspection.
1496
+
1497
+ Verified on Haxe 4.3.x.
1498
+
1499
+ ## `#if sys` Is FALSE on hxnodejs Builds — Use `#if (sys || nodejs)`
1500
+
1501
+ `-lib hxnodejs` does NOT define the `sys` conditional flag. Its `extraParams.hxml` uses `--macro allowPackage('sys')` (so `sys.io.File` / `sys.FileSystem` imports compile) plus `--macro define('nodejs')` (a separate flag). Code gated on `#if sys` runs ONLY on neko / hxcpp / eval / etc. — the JS/Node build silently takes the `#else` branch.
1502
+
1503
+ ```haxe
1504
+ // WRONG — silently no-op on hxnodejs build despite sys.io.File being importable
1505
+ #if sys
1506
+ private static function stageProbeSource(s:String):Null<String> {
1507
+ sys.io.File.saveContent('/tmp/scratch.hx', s);
1508
+ return s;
1509
+ }
1510
+ #else
1511
+ private static function stageProbeSource(_:String):Null<String> return null;
1512
+ #end
1513
+
1514
+ // RIGHT — both system targets AND hxnodejs reach the implementation
1515
+ #if (sys || nodejs)
1516
+ private static function stageProbeSource(s:String):Null<String> {
1517
+ sys.io.File.saveContent('/tmp/scratch.hx', s);
1518
+ return s;
1519
+ }
1520
+ #else
1521
+ private static function stageProbeSource(_:String):Null<String> return null;
1522
+ #end
1523
+ ```
1524
+
1525
+ **Symptom**: code that uses `sys.io.File` / `sys.FileSystem` compiles cleanly under `-lib hxnodejs` AND links into `bin/output.js`, but every `#if sys`-guarded code path is dead — the runtime takes `#else` every time. End-to-end smoke tests (run the binary, observe the side effect) catch it; unit tests using `Cli.run([...])` + exit-code-only assertions DON'T (exit stays 0 because the `#else` branch is well-behaved).
1526
+
1527
+ **Detection rule**: before writing a new `#if sys` block in a multi-target project, grep the file's existing import block for the conditional pattern. If imports use `#if (sys || nodejs)` (or `#if (sys || js)` etc.), match that pattern in every new block — single-source the target enumeration to the import guard.
1528
+
1529
+ Verified on Haxe 4.3.7 + hxnodejs 12.x.
1530
+
1531
+ ## Throwing `Error`s in Hot Paths Eagerly Captures a V8 Stack Trace — Catastrophic in Exception-Based Control Flow
1532
+
1533
+ A value that extends `haxe.Exception` compiles to a native `js.lib.Error` on the JS target. **V8 captures a stack trace eagerly at `Error` construction** (up to `Error.stackTraceLimit` frames) — even when `.stack` is never read. In exception-based control flow that throws constantly — PEG parser backtracking, recursive-descent ordered-choice, deep retry loops — this per-throw stack capture DOMINATES runtime.
1534
+
1535
+ **Symptom**: code is ~1ms per line/item, orders of magnitude slower than the actual work. `node --prof` + `node --prof-process` reports a huge **"Unaccounted" fraction (~96%)** — the cost is in V8's native stack-collection machinery, attributed to no JS function. A `caught`/exception frame shows up in the tick list.
1536
+
1537
+ **Confirm in one step**: re-run with `node --stack-trace-limit=0`. A dramatic speedup (measured: 14.0s → 3.9s on a 10891-line parse) proves eager stack capture is the cost.
1538
+
1539
+ **Fix** — throw a single **pre-allocated stackless sentinel** instead of `new ParseError(...)` per throw. Allocated once → its stack is captured once (negligible) → reused on every throw with zero per-throw capture or allocation. Correct when the thrown payload is a pure control-flow signal (the real error is reconstructed elsewhere, e.g. from a "farthest failure position" tracker):
1540
+
1541
+ ```haxe
1542
+ class ParseError extends haxe.Exception {
1543
+ // shared backtracking signal: one instance, stack captured once, never mutated
1544
+ public static final backtrack:ParseError = new ParseError(...);
1545
+ }
1546
+ // hot path:
1547
+ throw ParseError.backtrack; // reused — no capture
1548
+ // NOT: throw new ParseError(span, msg) // captures a stack on every throw
1549
+ ```
1550
+
1551
+ Do **not** reach for a global `Error.stackTraceLimit = 0`: it is global mutable state, JS-only, and silently strips stacks from genuine errors. The shared-sentinel approach is local, target-agnostic, and thread-safe.
1552
+
1553
+ **Caveat — keep the sentinel immutable.** If it has a mutable field (e.g. a `source` an error-decorator writes), make sure no path mutates it while it is in flight — otherwise you reintroduce global mutable state / a cross-parse data race. When you argue "this comparison always selects the rebuild branch over the sentinel", verify it at the **init/boundary values**: a `maxFailPos > sentinel.span.from` check fails (`-1 > -1`) if the sentinel's span equals the tracker's `-1` init value. (Fix used a `(-2,-2)` sentinel span, strictly below the `-1` floor, so the check is always true.)
1554
+
1555
+ Verified on Haxe 4.3.7 + Node (a PEG parser): a 10891-line parse threw 603,361 `new ParseError` (~55/line), each capturing a stack → 14.0s; the stackless sentinel cut it to 3.5s (4×), allocations 603,361 → 1, with byte-identical surfaced errors.
1556
+
1557
+ ## `untyped` Is a Reserved Keyword — Cannot Be a Variable Name
1558
+
1559
+ `untyped` is a Haxe keyword (the `untyped expr` escape hatch that disables type-checking), so it cannot be used as an identifier — a `var`/`final` named `untyped` fails at compile with `Keyword untyped cannot be used as variable name`. It reads like an ordinary descriptive word (e.g. a `final untyped:Array<Bool>` flag array), which is exactly why it slips in.
1560
+
1561
+ ```haxe
1562
+ // WRONG — compile error: Keyword untyped cannot be used as variable name
1563
+ final untyped:Array<Bool> = [for (c in clauses) isUntypedCatch(c)];
1564
+
1565
+ // RIGHT — rename
1566
+ final untypedFlags:Array<Bool> = [for (c in clauses) isUntypedCatch(c)];
1567
+ ```
1568
+
1569
+ Other sneaky-looking reserved words in the same trap (read like normal nouns/verbs but are keywords): `cast`, `dynamic`, `inline`, `extern`, `macro`, `operator`, `overload`, `using`, `abstract`. When a local name collides, rename it (`untypedFlags`, `castNode`, …). Verified on Haxe 4.3.7.
1570
+
1571
+ ## Safe Cast `cast(v, T)` Returns `null` for a `null` Value — Only Throws on a Non-Null Mismatch
1572
+
1573
+ The runtime-checked cast `cast(value, Type)` does NOT unconditionally throw when the value isn't a `Type`. The generated check (Haxe `Boot.__cast`) is `value == null || isOfType(value, T) ? value : throw`. So:
1574
+ - `cast(null, SomeClass)` → returns `null` (no exception).
1575
+ - `cast(nonNullValueOfWrongType, SomeClass)` → throws.
1576
+
1577
+ ```haxe
1578
+ var s:Null<String> = null;
1579
+ var x = cast(s, Sys); // returns null — does NOT throw
1580
+ var y = cast("hi", Sys); // throws — "hi" is a non-null String, not a Sys
1581
+ ```
1582
+
1583
+ Implication when reasoning/messaging about an impossible cast (`cast(x, T)` where `x`'s type and `T` are unrelated): "always throws" / "guaranteed exception" is wrong for a value that may be `null`. The accurate framing is "the cast can never yield a usable `T`" (it throws for any non-null value, yields `null` for a null one). Distinct from `(v : T)` (compile-time ascription — a wrong type is a COMPILE error, never a runtime cast) and from the unchecked single-arg `cast v` (no runtime test at all). Verified on Haxe 4.3.7 / js.