@hoox-sh/pynets 0.2.0 → 0.4.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.
@@ -8,6 +8,7 @@ import { parse } from "../../ast/helper.ts";
8
8
  import type {
9
9
  Assign,
10
10
  Attribute,
11
+ AugAssign,
11
12
  BinOp,
12
13
  BoolOp,
13
14
  Call,
@@ -23,6 +24,7 @@ import type {
23
24
  If,
24
25
  Import,
25
26
  Name,
27
+ Once,
26
28
  ReAssign,
27
29
  Script,
28
30
  stmt,
@@ -33,7 +35,7 @@ import type {
33
35
  UnaryOp,
34
36
  While,
35
37
  } from "../../ast/nodes.ts";
36
- import { emitCall } from "./emit_call.ts";
38
+ import { emitCall, TA_BARE_ATTRS } from "./emit_call.ts";
37
39
  import { emitFunctionDef } from "./emit_udf.ts";
38
40
  import type { EmitCtx, VisitFn } from "./types.ts";
39
41
 
@@ -47,6 +49,7 @@ const CHART_LOCALS = new Set([
47
49
  "close",
48
50
  "volume",
49
51
  "time",
52
+ "time_close",
50
53
  "bar_index",
51
54
  "last_bar_index",
52
55
  "na",
@@ -138,6 +141,7 @@ export function newEmitCtx(): EmitCtx {
138
141
  usesStrategy: false,
139
142
  nextSite: 0,
140
143
  errors: [],
144
+ onceFlags: [],
141
145
  currentFunc: null,
142
146
  currentParamNames: new Set(),
143
147
  currentSeriesParams: new Set(),
@@ -210,6 +214,9 @@ function assemble(ctx: EmitCtx, body: string[]): string {
210
214
  lines.push(` const ${arr} = new Array(n_bars).fill(null);`);
211
215
  }
212
216
  lines.push(" const __var_inited = Object.create(null);");
217
+ for (const flag of ctx.onceFlags) {
218
+ lines.push(` let ${flag} = false;`);
219
+ }
213
220
  for (const fn of ctx.functions) {
214
221
  if (fn) lines.push(indentBlock(fn, 2));
215
222
  }
@@ -223,6 +230,7 @@ function assemble(ctx: EmitCtx, body: string[]): string {
223
230
  lines.push(" const close = close_arr[__bar_idx];");
224
231
  lines.push(" const volume = vol_arr[__bar_idx];");
225
232
  lines.push(" const time = time_arr[__bar_idx];");
233
+ lines.push(" const time_close = __h.timeClose(__bar_idx, time_arr);");
226
234
  lines.push(" const bar_index = __bar_idx;");
227
235
  lines.push(" const last_bar_index = n_bars - 1;");
228
236
  lines.push(" const na = null;");
@@ -267,6 +275,7 @@ function emitNode(state: State, node: expr | stmt | null | undefined): string {
267
275
  switch (node.kind) {
268
276
  case "Assign":
269
277
  case "ReAssign":
278
+ case "AugAssign":
270
279
  case "FunctionDef":
271
280
  case "TypeDef":
272
281
  case "EnumDef":
@@ -274,6 +283,7 @@ function emitNode(state: State, node: expr | stmt | null | undefined): string {
274
283
  case "Break":
275
284
  case "Continue":
276
285
  case "Expr":
286
+ case "Once":
277
287
  return emitStmt(state, node);
278
288
  default:
279
289
  return emitExpr(state, node);
@@ -288,6 +298,8 @@ function emitStmt(state: State, node: stmt | expr): string {
288
298
  return emitAssign(state, node);
289
299
  case "ReAssign":
290
300
  return emitReAssign(state, node);
301
+ case "AugAssign":
302
+ return emitAugAssign(state, node);
291
303
  case "FunctionDef":
292
304
  emitFunctionDef(state.ctx, node as FunctionDef, (s) => emitStmt(state, s));
293
305
  if (state.ctx.currentFunc != null) state.ctx.currentFunc = null;
@@ -312,6 +324,8 @@ function emitStmt(state: State, node: stmt | expr): string {
312
324
  return emitWhile(state, node);
313
325
  case "Switch":
314
326
  return emitSwitch(state, node, false);
327
+ case "Once":
328
+ return emitOnce(state, node);
315
329
  default:
316
330
  return emitExprStmt(state, node);
317
331
  }
@@ -330,6 +344,8 @@ function emitExprStmt(state: State, value: expr | null | undefined): string {
330
344
  return emitWhile(state, value);
331
345
  case "Switch":
332
346
  return emitSwitch(state, value, false);
347
+ case "Once":
348
+ return emitOnce(state, value);
333
349
  default: {
334
350
  const e = emitExpr(state, value);
335
351
  if (!e || e === "null") return "";
@@ -373,6 +389,12 @@ function emitExpr(state: State, node: expr | null | undefined): string {
373
389
  return iife(emitForIn(state, node), "null");
374
390
  case "While":
375
391
  return iife(emitWhile(state, node), "null");
392
+ case "Qualify":
393
+ return emitExpr(state, node.value);
394
+ case "Specialize":
395
+ return emitExpr(state, node.value);
396
+ case "AugAssign":
397
+ return emitAugAssign(state, node);
376
398
  default:
377
399
  return "null";
378
400
  }
@@ -396,6 +418,8 @@ function emitName(state: State, node: Name): string {
396
418
  if (CHART_LOCALS.has(id)) return id;
397
419
  if (state.ctx.userFuncs.has(id)) return id;
398
420
  if (state.ctx.enumTypes.has(id)) return JSON.stringify(id);
421
+ // Dataset last-bar time on every bar (interpret ctx.timenow / lastBarTime).
422
+ if (id === "timenow") return "__h.timenow(time_arr)";
399
423
  return safeIdent(id);
400
424
  }
401
425
 
@@ -423,11 +447,25 @@ function emitBinOp(state: State, node: BinOp): string {
423
447
  return `__h.div(${left}, ${right})`;
424
448
  case "Mod":
425
449
  return `__h.mod(${left}, ${right})`;
450
+ case "BitAnd":
451
+ return emitBitOp(left, right, "&");
452
+ case "BitOr":
453
+ return emitBitOp(left, right, "|");
454
+ case "BitXor":
455
+ return emitBitOp(left, right, "^");
456
+ case "LShift":
457
+ return emitBitOp(left, right, "<<");
458
+ case "RShift":
459
+ return emitBitOp(left, right, ">>");
426
460
  default:
427
461
  return `__h.add(${left}, ${right})`;
428
462
  }
429
463
  }
430
464
 
465
+ function emitBitOp(left: string, right: string, op: "&" | "|" | "^" | "<<" | ">>"): string {
466
+ return `((__l, __r) => { if (__h.isNa(__l) || __h.isNa(__r)) return null; return (__h.nz(__l) | 0) ${op} (__h.nz(__r) | 0); })(${left}, ${right})`;
467
+ }
468
+
431
469
  function emitUnaryOp(state: State, node: UnaryOp): string {
432
470
  const operand = emitExpr(state, node.operand);
433
471
  switch (opKind(node.op)) {
@@ -500,9 +538,62 @@ const STRATEGY_QUERY_ATTRS = new Set([
500
538
  "openprofit",
501
539
  "opentrades",
502
540
  "closedtrades",
541
+ "leverage",
542
+ "margin_liquidation_price",
543
+ "position_avg_price",
544
+ "initial_capital",
545
+ "cash",
546
+ "position_entry_name",
547
+ "account_currency",
548
+ "default_entry_qty",
549
+ "max_contracts_held_all",
550
+ "max_contracts_held_long",
551
+ "max_contracts_held_short",
552
+ "wintrades",
553
+ "losstrades",
554
+ "eventrades",
555
+ "grossprofit",
556
+ "grossloss",
557
+ "avg_trade",
558
+ "avg_winning_trade",
559
+ "avg_losing_trade",
560
+ "netprofit_percent",
561
+ "openprofit_percent",
562
+ "grossprofit_percent",
563
+ "grossloss_percent",
564
+ "avg_trade_percent",
565
+ "avg_winning_trade_percent",
566
+ "avg_losing_trade_percent",
567
+ "max_drawdown",
568
+ "max_drawdown_percent",
569
+ "max_runup",
570
+ "max_runup_percent",
571
+ "percent_profitable",
572
+ "winrate",
573
+ "profitfactor",
574
+ "profit_factor",
503
575
  ]);
504
576
 
505
- /** Namespaces used only as call receivers — not UDT field access. */
577
+ /** Nested `strategy.*` constants (interpret `strategySeriesValue`). */
578
+ const STRATEGY_NESTED_CONSTS: Record<string, string> = {
579
+ "strategy.direction.long": "long",
580
+ "strategy.direction.short": "short",
581
+ "strategy.direction.all": "all",
582
+ "strategy.oca.none": "none",
583
+ "strategy.oca.cancel": "cancel",
584
+ "strategy.oca.reduce": "reduce",
585
+ "strategy.commission.percent": "percent",
586
+ "strategy.commission.cash_per_order": "cash_per_order",
587
+ "strategy.commission.cash_per_contract": "cash_per_contract",
588
+ };
589
+
590
+ /** Nested `strategy.*` series that are not a single identifier. */
591
+ const STRATEGY_NESTED_CALLS: Record<string, string> = {
592
+ "strategy.closedtrades.first_index": "closedtrades_first_index",
593
+ "strategy.opentrades.capital_held": "capital_held",
594
+ };
595
+
596
+ /** Namespaces used as call receivers or dotted constants — not UDT field access. */
506
597
  const ATTR_NAMESPACES = new Set([
507
598
  "ta",
508
599
  "math",
@@ -521,8 +612,155 @@ const ATTR_NAMESPACES = new Set([
521
612
  "session",
522
613
  "chart",
523
614
  "log",
615
+ "order",
616
+ "format",
617
+ "text",
618
+ "size",
619
+ "barmerge",
620
+ "shape",
621
+ "location",
622
+ "xloc",
623
+ "yloc",
624
+ "extend",
625
+ "display",
626
+ "position",
627
+ "hline",
628
+ "dayofweek",
629
+ "month",
630
+ ]);
631
+
632
+ /** Dotted keys from Python `_MATH_CONSTANTS` (base.py). Unknown member → na. */
633
+ const ATTR_CONSTANTS: Record<string, string | number | boolean> = {
634
+ // Math constants — Python SoT `_MATH_CONSTANTS` (evaluator/base.py).
635
+ "math.pi": Math.PI,
636
+ "math.e": Math.E,
637
+ "math.phi": (1 + Math.sqrt(5)) / 2,
638
+ "math.rphi": 2 / (1 + Math.sqrt(5)),
639
+ "format.mintick": "mintick",
640
+ "format.percent": "percent",
641
+ "format.volume": "volume",
642
+ "format.price": "price",
643
+ "text.formatting.none": "",
644
+ "text.formatting.bold": "bold",
645
+ "text.formatting.italic": "italic",
646
+ "text.formatting.bold_italic": "bold italic",
647
+ "size.auto": "auto",
648
+ "size.tiny": 8,
649
+ "size.small": 10,
650
+ "size.normal": 12,
651
+ "size.large": 16,
652
+ "size.huge": 20,
653
+ "order.ascending": 1,
654
+ "order.descending": -1,
655
+ "barmerge.gaps_on": true,
656
+ "barmerge.gaps_off": false,
657
+ "barmerge.lookahead_on": true,
658
+ "barmerge.lookahead_off": false,
659
+ "shape.arrowup": "arrowup",
660
+ "shape.arrowdown": "arrowdown",
661
+ "shape.circle": "circle",
662
+ "shape.cross": "cross",
663
+ "shape.diamond": "diamond",
664
+ "shape.flag": "flag",
665
+ "shape.labelup": "labelup",
666
+ "shape.labeldown": "labeldown",
667
+ "shape.square": "square",
668
+ "shape.triangledown": "triangledown",
669
+ "shape.triangleup": "triangleup",
670
+ "shape.xcross": "xcross",
671
+ "location.abovebar": "abovebar",
672
+ "location.belowbar": "belowbar",
673
+ "location.top": "top",
674
+ "location.bottom": "bottom",
675
+ "location.absolute": "absolute",
676
+ "xloc.bar_index": "bar_index",
677
+ "xloc.bar_time": "bar_time",
678
+ "yloc.price": "price",
679
+ "yloc.abovebar": "abovebar",
680
+ "yloc.belowbar": "belowbar",
681
+ "extend.none": "none",
682
+ "extend.left": "left",
683
+ "extend.right": "right",
684
+ "extend.both": "both",
685
+ // Pine display.* is a bitfield: none=0 pane=1 data_window=2 price_scale=4
686
+ // status_line=8 all=15. Integers so `display.pane + display.data_window` works
687
+ // (Python SoT base.py / compiler.py — matches interpret backend).
688
+ "display.none": 0,
689
+ "display.pane": 1,
690
+ "display.all": 15,
691
+ "display.data_window": 2,
692
+ "display.price_scale": 4,
693
+ "display.status_line": 8,
694
+ "position.top_left": "top_left",
695
+ "position.top_center": "top_center",
696
+ "position.top_right": "top_right",
697
+ "position.middle_left": "middle_left",
698
+ "position.middle_center": "middle_center",
699
+ "position.middle_right": "middle_right",
700
+ "position.bottom_left": "bottom_left",
701
+ "position.bottom_center": "bottom_center",
702
+ "position.bottom_right": "bottom_right",
703
+ "hline.style_solid": "solid",
704
+ "hline.style_dashed": "dashed",
705
+ "hline.style_dotted": "dotted",
706
+ // Python plotting.py PlotStyle — longer than hline's "solid".
707
+ "plot.linestyle_solid": "linestyle_solid",
708
+ "plot.linestyle_dashed": "linestyle_dashed",
709
+ "plot.linestyle_dotted": "linestyle_dotted",
710
+ "dayofweek.sunday": 1,
711
+ "dayofweek.monday": 2,
712
+ "dayofweek.tuesday": 3,
713
+ "dayofweek.wednesday": 4,
714
+ "dayofweek.thursday": 5,
715
+ "dayofweek.friday": 6,
716
+ "dayofweek.saturday": 7,
717
+ "month.january": 1,
718
+ "month.february": 2,
719
+ "month.march": 3,
720
+ "month.april": 4,
721
+ "month.may": 5,
722
+ "month.june": 6,
723
+ "month.july": 7,
724
+ "month.august": 8,
725
+ "month.september": 9,
726
+ "month.october": 10,
727
+ "month.november": 11,
728
+ "month.december": 12,
729
+ };
730
+
731
+ const ATTR_CONST_NS = new Set([
732
+ "math",
733
+ "order",
734
+ "format",
735
+ "text",
736
+ "size",
737
+ "barmerge",
738
+ "shape",
739
+ "location",
740
+ "xloc",
741
+ "yloc",
742
+ "extend",
743
+ "display",
744
+ "position",
745
+ "hline",
746
+ "plot",
747
+ "dayofweek",
748
+ "month",
524
749
  ]);
525
750
 
751
+ function attrQualifiedName(node: Attribute): string | null {
752
+ const parts: string[] = [node.attr];
753
+ let cur: expr = node.value;
754
+ while (cur.kind === "Attribute") {
755
+ parts.push(cur.attr);
756
+ cur = cur.value;
757
+ }
758
+ if (cur.kind !== "Name") return null;
759
+ parts.push(cur.id);
760
+ parts.reverse();
761
+ return parts.join(".");
762
+ }
763
+
526
764
  function emitBarstate(attr: string): string {
527
765
  switch (attr) {
528
766
  case "isfirst":
@@ -584,19 +822,34 @@ function emitChart(attr: string): string {
584
822
  }
585
823
  }
586
824
 
825
+ // Static defaults mirror the Python SoT flat table in
826
+ // pynescript/ast/evaluator/base.py ("Chart timeframe defaults (daily)"),
827
+ // cross-checked with runtime/host.py Timeframe (same values). The compile host
828
+ // has no timeframe context (`__h.timeframe` is undefined), so these are the
829
+ // effective values; interpret derives them dynamically from env.timeframe and
830
+ // resolves to the same daily table when no timeframe is configured — every
831
+ // timeframe.* member is backend-parity (commit 6977a42; pinned in
832
+ // test/compile_timeframe_defaults.test.ts).
587
833
  function emitTimeframe(attr: string): string {
588
834
  switch (attr) {
589
835
  case "period":
590
- return `(__h.timeframe || "1")`;
836
+ case "main_period":
837
+ return `(__h.timeframe || "D")`;
591
838
  case "multiplier":
592
839
  return "1";
593
840
  case "isintraday":
594
- return "true";
595
- case "isdaily":
596
841
  case "isweekly":
597
842
  case "ismonthly":
843
+ case "isseconds":
844
+ case "isinseconds":
845
+ case "isminutes":
846
+ case "ishours":
598
847
  return "false";
848
+ case "isdaily":
849
+ case "isdwm":
850
+ return "true";
599
851
  default:
852
+ // Unknown member → na (documented behavior).
600
853
  return "null";
601
854
  }
602
855
  }
@@ -607,6 +860,8 @@ function emitSyminfo(attr: string): string {
607
860
  case "tickerid":
608
861
  case "root":
609
862
  return `"SYMBOL"`;
863
+ case "prefix":
864
+ return `__h.syminfoPrefix()`;
610
865
  default:
611
866
  return "null";
612
867
  }
@@ -635,14 +890,42 @@ function emitAttribute(state: State, node: Attribute): string {
635
890
  if (node.value.kind === "Name" && node.value.id === "color") {
636
891
  return `__h.colorByName(${JSON.stringify(node.attr)})`;
637
892
  }
893
+ if (node.value.kind === "Name" && node.value.id === "ta") {
894
+ // Bare `ta.<attr>` auto-calls only for the whitelist interpret's
895
+ // evalTaAttr supports; everything else is na on both backends.
896
+ if (!TA_BARE_ATTRS.has(node.attr)) return "null";
897
+ return emitCall(state.ctx, { kind: "Call", func: node, args: [] }, visitOf(state));
898
+ }
638
899
  if (node.value.kind === "Name" && node.value.id === "strategy") {
639
900
  const attr = node.attr;
640
901
  if (attr === "long") return `"long"`;
641
902
  if (attr === "short") return `"short"`;
903
+ if (attr === "avg_price_stock") return `"stock"`;
904
+ if (attr === "avg_price_futures") return `"futures"`;
905
+ if (attr === "avg_price_inverse") return `"inverse"`;
906
+ if (attr === "fixed") return `"fixed"`;
907
+ if (attr === "percent_of_equity") return `"percent_of_equity"`;
908
+ // `strategy.commission` the series is 0; `strategy.commission.percent` is nested.
909
+ if (attr === "commission") return "0";
642
910
  if (STRATEGY_QUERY_ATTRS.has(attr)) {
643
911
  state.ctx.usesStrategy = true;
644
912
  return `__h.strategy.${attr}()`;
645
913
  }
914
+ return "null";
915
+ }
916
+ if (node.attr === "all" && node.value.kind === "Name") {
917
+ const kind = node.value.id;
918
+ if (
919
+ kind === "line" ||
920
+ kind === "label" ||
921
+ kind === "box" ||
922
+ kind === "table" ||
923
+ kind === "polyline" ||
924
+ kind === "linefill"
925
+ ) {
926
+ state.ctx.usesDrawings = true;
927
+ return `__h.draw.all(${JSON.stringify(kind)})`;
928
+ }
646
929
  }
647
930
  if (node.value.kind === "Name") {
648
931
  const members = state.ctx.enumTypes.get(node.value.id);
@@ -650,6 +933,23 @@ function emitAttribute(state: State, node: Attribute): string {
650
933
  return JSON.stringify(node.attr);
651
934
  }
652
935
  }
936
+ const qn = attrQualifiedName(node);
937
+ if (qn != null) {
938
+ const nested = STRATEGY_NESTED_CONSTS[qn];
939
+ if (nested !== undefined) return JSON.stringify(nested);
940
+ const nestedCall = STRATEGY_NESTED_CALLS[qn];
941
+ if (nestedCall != null) {
942
+ state.ctx.usesStrategy = true;
943
+ return `__h.strategy.${nestedCall}()`;
944
+ }
945
+ if (qn.startsWith("strategy.") && qn.split(".").length > 2) return "null";
946
+ const dot = qn.indexOf(".");
947
+ const root = dot < 0 ? qn : qn.slice(0, dot);
948
+ if (ATTR_CONST_NS.has(root)) {
949
+ const lit = ATTR_CONSTANTS[qn];
950
+ return lit === undefined ? "null" : JSON.stringify(lit);
951
+ }
952
+ }
653
953
  const obj = emitExpr(state, node.value);
654
954
  if (node.value.kind === "Name" && state.ctx.udtTypes.has(node.value.id)) {
655
955
  return `__h.udtGet(${obj}, ${JSON.stringify(node.attr)})`;
@@ -772,6 +1072,21 @@ function emitReAssign(state: State, node: ReAssign): string {
772
1072
  return emitStore(state, node.target, emitExpr(state, node.value), false);
773
1073
  }
774
1074
 
1075
+ function emitAugAssign(state: State, node: AugAssign): string {
1076
+ // Attribute targets store the raw rhs — the old field is discarded
1077
+ // (quirk preserved from Python visit_AugAssign and interpret evalAugAssign).
1078
+ if (node.target.kind === "Attribute") {
1079
+ return emitStore(state, node.target, emitExpr(state, node.value), false);
1080
+ }
1081
+ const rhs = emitBinOp(state, {
1082
+ kind: "BinOp",
1083
+ left: node.target,
1084
+ op: node.op,
1085
+ right: node.value,
1086
+ });
1087
+ return emitStore(state, node.target, rhs, false);
1088
+ }
1089
+
775
1090
  function emitStore(state: State, target: expr, rhs: string, isVar: boolean): string {
776
1091
  if (target.kind === "Name") {
777
1092
  return storeName(state, target.id, rhs, isVar);
@@ -842,6 +1157,15 @@ function emitIfStmt(state: State, node: If, asExpr = false): string {
842
1157
  return `if (${test}) {\n${indentBlock(thenBlock, 2)}\n} else {\n${indentBlock(elseBlock, 2)}\n}`;
843
1158
  }
844
1159
 
1160
+ function emitOnce(state: State, node: Once): string {
1161
+ const flag = `__once_${state.ctx.onceFlags.length}`;
1162
+ state.ctx.onceFlags.push(flag);
1163
+ const test = node.test == null ? "true" : `__h.nz(${emitExpr(state, node.test)})`;
1164
+ const body = emitBranch(state, node.body, false);
1165
+ const inner = body ? `${indentBlock(body, 4)}\n ${flag} = true;` : ` ${flag} = true;`;
1166
+ return `if (!${flag}) {\n if (${test}) {\n${inner}\n }\n}`;
1167
+ }
1168
+
845
1169
  function emitBranch(state: State, stmts: stmt[], asExpr: boolean): string {
846
1170
  const list = stmts ?? [];
847
1171
  if (list.length === 0) return asExpr ? "return null;" : "";
@@ -900,6 +1224,7 @@ function isValueExpr(node: expr): boolean {
900
1224
  case "ForIn":
901
1225
  case "While":
902
1226
  case "Switch":
1227
+ case "Once":
903
1228
  return false;
904
1229
  default:
905
1230
  return true;
@@ -1021,10 +1346,7 @@ function emitSwitch(state: State, node: Switch, asExpr: boolean): string {
1021
1346
  defaultCase = c;
1022
1347
  continue;
1023
1348
  }
1024
- const cond =
1025
- subjN != null
1026
- ? `__h.eq(${subjN}, ${emitExpr(state, c.pattern)})`
1027
- : `__h.nz(${emitExpr(state, c.pattern)})`;
1349
+ const cond = emitSwitchCond(state, c.pattern, subjN);
1028
1350
  const kw = first ? "if" : "else if";
1029
1351
  first = false;
1030
1352
  const body = emitBranch(state, c.body ?? [], asExpr);
@@ -1045,6 +1367,21 @@ function emitSwitch(state: State, node: Switch, asExpr: boolean): string {
1045
1367
  return `{\n${indentBlock(block, 2)}\n}`;
1046
1368
  }
1047
1369
 
1370
+ /** Python `_switch_case_matches`: a comma arm (`1, 2 =>`) matches any element. */
1371
+ function emitSwitchCond(state: State, pattern: expr, subjN: string | null): string {
1372
+ if (pattern.kind === "Tuple") {
1373
+ const parts = (pattern.elts ?? []).map((e) => emitSwitchLeaf(state, e, subjN));
1374
+ return parts.length ? `(${parts.join(" || ")})` : "false";
1375
+ }
1376
+ return emitSwitchLeaf(state, pattern, subjN);
1377
+ }
1378
+
1379
+ function emitSwitchLeaf(state: State, pattern: expr, subjN: string | null): string {
1380
+ return subjN != null
1381
+ ? `__h.eq(${subjN}, ${emitExpr(state, pattern)})`
1382
+ : `__h.nz(${emitExpr(state, pattern)})`;
1383
+ }
1384
+
1048
1385
  function emitBlock(state: State, stmts: stmt[] | null | undefined): string {
1049
1386
  const lines: string[] = [];
1050
1387
  for (const s of stmts ?? []) {