@portll/cobolwork 0.2.76 → 0.2.140

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 (61) hide show
  1. package/README.md +66 -14
  2. package/bin/cobolwork.mjs +126 -9
  3. package/lib/arith.mjs +235 -0
  4. package/lib/baseline.mjs +5 -0
  5. package/lib/build.mjs +112 -30
  6. package/lib/capabilities.mjs +12 -7
  7. package/lib/card.mjs +18 -0
  8. package/lib/cics-commands.mjs +136 -125
  9. package/lib/consequence.mjs +29 -0
  10. package/lib/control.mjs +1169 -91
  11. package/lib/csd.mjs +17 -2
  12. package/lib/dataflow.mjs +276 -126
  13. package/lib/embedded-sql.mjs +110 -0
  14. package/lib/enterprise-options.mjs +255 -0
  15. package/lib/equivalence.mjs +124 -0
  16. package/lib/evidence/cli.mjs +65 -0
  17. package/lib/evidence/journal.mjs +94 -0
  18. package/lib/evidence/record.mjs +110 -0
  19. package/lib/evidence/run.mjs +111 -0
  20. package/lib/evidence/seal.mjs +177 -0
  21. package/lib/evidence/slsa.mjs +64 -0
  22. package/lib/evidence/sshsig.mjs +172 -0
  23. package/lib/evidence/store.mjs +170 -0
  24. package/lib/evidence/verify.mjs +167 -0
  25. package/lib/explain.mjs +9 -5
  26. package/lib/exploitability.mjs +263 -0
  27. package/lib/ftp.mjs +136 -0
  28. package/lib/ironwork.mjs +109 -0
  29. package/lib/jcl.mjs +27 -7
  30. package/lib/kernel/findings.mjs +11 -0
  31. package/lib/kernel/registry.mjs +4 -0
  32. package/lib/layout.mjs +202 -0
  33. package/lib/option-diff.mjs +49 -0
  34. package/lib/options.mjs +109 -46
  35. package/lib/parser.mjs +80 -209
  36. package/lib/policy.mjs +8 -3
  37. package/lib/precompile.mjs +5 -84
  38. package/lib/reach.mjs +29 -12
  39. package/lib/revision.json +1 -1
  40. package/lib/sarif.mjs +1 -0
  41. package/lib/sbom.mjs +235 -0
  42. package/lib/scan.mjs +93 -27
  43. package/lib/sets/cics.mjs +119 -2
  44. package/lib/sets/copybook.mjs +2 -1
  45. package/lib/sets/flow.mjs +82 -2
  46. package/lib/sets/hidden.mjs +4 -8
  47. package/lib/sets/jcl.mjs +70 -106
  48. package/lib/sets/recon.mjs +2 -1
  49. package/lib/sets/semantics.mjs +377 -0
  50. package/lib/sets/web.mjs +34 -10
  51. package/lib/sets/zowe.mjs +246 -0
  52. package/lib/sources.mjs +10 -0
  53. package/lib/tui/app.mjs +31 -13
  54. package/lib/tui/model.mjs +17 -2
  55. package/lib/utilities.mjs +229 -15
  56. package/lib/verify.mjs +128 -0
  57. package/package.json +1 -1
  58. package/rules/compliance-dora.json +507 -0
  59. package/rules/compliance-ffiec.json +507 -0
  60. package/rules/compliance-nist80053.json +507 -0
  61. package/schema/cobolwork.policy.schema.json +94 -16
package/lib/control.mjs CHANGED
@@ -20,6 +20,7 @@
20
20
  // entered holding no facts at all, which can only withhold credit. ALTER makes a GO TO reach every
21
21
  // paragraph, for the same reason.
22
22
  import { segmentEnd } from './parser.mjs';
23
+ import { parser, attempt, candidates, leaves, storedRange } from './arith.mjs';
23
24
 
24
25
  const WORD = (t, u) => t && t.t === 'word' && t.u === u;
25
26
  const PHRASED = new Set(['READ', 'WRITE', 'REWRITE', 'DELETE', 'START', 'RETURN', 'CALL', 'COMPUTE', 'ADD', 'SUBTRACT',
@@ -28,6 +29,8 @@ const PHRASE_WORDS = new Set(['END', 'INVALID', 'EXCEPTION', 'OVERFLOW', 'END-OF
28
29
  // One copy of every node's facts. The analysis keeps a few, so this is a quarter of what it may use.
29
30
  const FACT_BUDGET_BYTES = 256 * 1024 * 1024;
30
31
  const SETTLE_BUDGET = 20 * 1000 * 1000;
32
+ // Statements times fact words past which arithmetic intervals are not iterated.
33
+ const ARITH_WORK = 40 * 1000 * 1000;
31
34
  // Routines whose only effect is to end the run with an abend.
32
35
  const ABEND_ROUTINE = /^(CEE3ABD|CEE3AB2|ILBOABN0|ILBOABN)$/i;
33
36
 
@@ -50,6 +53,8 @@ function meet(a, b) {
50
53
  if (a.set && b.set) out.set = new Set([...a.set].filter((v) => b.set.has(v)));
51
54
  else if (a.set || b.set) out.set = new Set(a.set || b.set);
52
55
  if (a.numeric || b.numeric) out.numeric = true;
56
+ if (!a.dep !== !b.dep) out.dep = a.dep || b.dep;
57
+ if (a.viaExpr && b.viaExpr) out.viaExpr = true;
53
58
  if (a.alpha || b.alpha) out.alpha = true;
54
59
  const lo = pick(a, b, 'lo', Math.max);
55
60
  const hi = pick(a, b, 'hi', Math.min);
@@ -67,9 +72,10 @@ function join(a, b) {
67
72
  if (a.set && b.set) out.set = new Set([...a.set, ...b.set]);
68
73
  if (a.numeric && b.numeric) out.numeric = true;
69
74
  if (a.alpha && b.alpha) out.alpha = true;
75
+ if (a.viaExpr || b.viaExpr) out.viaExpr = true;
70
76
  if (a.lo != null && b.lo != null) { out.lo = Math.min(a.lo, b.lo); out.loInc = a.lo === out.lo ? a.loInc : b.loInc; }
71
77
  if (a.hi != null && b.hi != null) { out.hi = Math.max(a.hi, b.hi); out.hiInc = a.hi === out.hi ? a.hiInc : b.hiInc; }
72
- return Object.keys(out).length ? out : null;
78
+ return Object.keys(out).some((k) => k !== 'viaExpr') ? out : null;
73
79
  }
74
80
  // Whether a known value meets what a check's outcome says of its field.
75
81
  function satisfies(cons, v) {
@@ -99,6 +105,9 @@ function pick(a, b, k, better) {
99
105
  return v === av.v ? av : bv;
100
106
  }
101
107
 
108
+ const INDEX_SINKS = new Set(['subscript', 'reference-modification', 'occurs-depending-count', 'loop-bound']);
109
+ const atLeastOne = (cons) => cons.lo != null && (cons.lo >= 1 || (cons.lo >= 0 && !cons.loInc));
110
+
102
111
  // Whether a constraint makes a value safe for what the sink does with it. A value that can only be
103
112
  // one of a set of literals is safe everywhere. Digits alone cannot carry a command, a statement, a
104
113
  // script or a job, and are exactly what arithmetic needs. Letters and spaces cannot carry markup or a
@@ -109,12 +118,16 @@ export function stops(cons, sinkKind) {
109
118
  const allDigits = set && [...cons.set].every((v) => /^[+-]?\d+(\.\d+)?$/.test(v));
110
119
  switch (sinkKind) {
111
120
  case 'arithmetic': return cons.numeric === true || allDigits;
112
- case 'subscript': case 'reference-modification': case 'occurs-depending-count': case 'loop-bound':
121
+ // A subscript and a reference-modification start count from 1, so 0 is out of range too; a
122
+ // count or a loop bound of 0 is not.
123
+ case 'subscript': case 'reference-modification':
124
+ return (cons.hi != null && atLeastOne(cons)) || (allDigits && [...cons.set].every((v) => Number(v) >= 1));
125
+ case 'occurs-depending-count': case 'loop-bound': case 'storage-length':
113
126
  return cons.hi != null || allDigits;
114
127
  case 'dynamic-sql': case 'internal-reader': return set || cons.numeric === true;
115
128
  case 'web-response': case 'http-header': case 'log': return set || cons.numeric === true || cons.alpha === true;
116
129
  case 'os-command': case 'dynamic-program-load': case 'cics-dynamic-transfer': case 'dynamic-file-path':
117
- case 'queue-name': case 'outbound-host': return set;
130
+ case 'queue-name': case 'outbound-host': case 'connection-target': case 'cics-sysid': case 'cics-system-resource': return set;
118
131
  default: return false;
119
132
  }
120
133
  }
@@ -127,6 +140,8 @@ const SIGN = new Set(['POSITIVE', 'NEGATIVE', 'ZERO', 'ZEROS', 'ZEROES']);
127
140
  const FIGURATIVE = { SPACE: ' ', SPACES: ' ', ZERO: '0', ZEROS: '0', ZEROES: '0', 'LOW-VALUE': '\u0000', 'LOW-VALUES': '\u0000', 'HIGH-VALUE': 'ÿ', 'HIGH-VALUES': 'ÿ', QUOTE: '"', QUOTES: '"' };
128
141
  const NEGATE = { '=': '<>', '<>': '=', '>': '<=', '<=': '>', '<': '>=', '>=': '<' };
129
142
  const FLIP = { '=': '=', '<>': '<>', '>': '<', '<': '>', '>=': '<=', '<=': '>=' };
143
+ // Past this an expression's intermediate value may not be held exactly, and no bound is drawn from it.
144
+ const EXPR_LIMIT = 1e15;
130
145
  const RESTRICTING_OPS = new Set(['>', '<', '>=', '<=']);
131
146
 
132
147
  // A condition as a tree of AND, OR and NOT over atoms. It reads what COBOL writes - class and sign
@@ -233,6 +248,7 @@ export function parseCondition(toks, resolve) {
233
248
  export function buildControl(prog, resolve, { extra = null } = {}) {
234
249
  if (!prog.proc) return null;
235
250
  const { tokens, from, to } = prog.proc;
251
+ const loopBody = new Map();
236
252
  const stmtAt = new Map();
237
253
  for (const st of prog.statements) if (st.at != null && st.verb !== 'WHEN') stmtAt.set(st.at, st);
238
254
  const labelAt = new Map();
@@ -357,6 +373,10 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
357
373
  // A name that is neither a paragraph nor data nor a loop word is a paragraph the parse missed.
358
374
  if (first && first.t === 'word' && !stmtAt.has(st.at + 1) && !LOOP_START.has(first.u) && !/^\d+$/.test(first.v) && !resolve(first)) unknownLabel = true;
359
375
  const body = parseList(new Set([...stops, 'END-PERFORM']));
376
+ // Where an inline body lies in the tokens, and whether it can run code outside them.
377
+ let leaves = false;
378
+ for (let j = st.end; j < k; j++) if (isWord(j, 'GO') || (isWord(j, 'PERFORM') && tokens[j + 1] && tokens[j + 1].t === 'word' && labelNames.has(tokens[j + 1].u))) leaves = true;
379
+ loopBody.set(st, { from: st.at, to: k, leaves });
360
380
  if (isWord(k, 'END-PERFORM')) k++;
361
381
  return { k: 'inline', st, body, loop, testAfter };
362
382
  }
@@ -469,6 +489,7 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
469
489
  const sectionEnd = new Map();
470
490
  for (const p of paras) if (p.section) sectionEnd.set(p.section, p.end);
471
491
  const tests = [];
492
+ const loopHeads = [];
472
493
 
473
494
  function lowerList(list, next, ctx) {
474
495
  let cont = next;
@@ -553,6 +574,7 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
553
574
  const body = n.k === 'perform' ? add('call', { st: n.st, range: rangeOf(n.target, n.thru), succ: [step] }) : lowerList(n.body, step, loopCtx);
554
575
  nodes[t].succ = [outcome(t, true, next), outcome(t, false, body)];
555
576
  nodes[head].succ = [n.testAfter ? body : t];
577
+ if (n.st.loops) loopHeads.push({ head, step, loops: n.st.loops, test: t, testAfter: n.testAfter, body: n.k === 'inline' ? n.body : null });
556
578
  return head;
557
579
  }
558
580
  case 'goto': {
@@ -601,6 +623,64 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
601
623
  return top;
602
624
  };
603
625
  const fieldKey = (f) => (f.index ? `IX:${f.index}` : f);
626
+ const overlaps = (w, f) => {
627
+ if (w === f) return true;
628
+ if (w.index || f.index) return w.index && f.index && w.index === f.index;
629
+ if (w.level === 66 || f.level === 66) return true;
630
+ if (itemsInRecord(w) !== itemsInRecord(f)) return false;
631
+ if (w.offset == null || f.offset == null || w.size == null || f.size == null) return true;
632
+ const wEnd = w.offset + (w.contributes || w.size);
633
+ const fEnd = f.offset + (f.contributes || f.size);
634
+ return w.offset < fEnd && f.offset < wEnd;
635
+ };
636
+ const writtenBy = (n) => {
637
+ const out = [];
638
+ const add = (tok) => { const f = tok ? resolve(tok) : null; if (f) out.push(f.level === 88 && f.parent ? f.parent : f); };
639
+ if (n.st) {
640
+ for (const t of n.st.targets || []) add(t);
641
+ if (n.st.verb === 'CALL' || n.st.verb === 'READ' || n.st.verb === 'RETURN') {
642
+ // A callee may write what it is passed by reference, and a READ fills the file's records.
643
+ for (const t of n.st.sources || []) {
644
+ const f = resolve(t);
645
+ if (!f) continue;
646
+ if (f.records) for (const r of f.records) out.push(r);
647
+ else if (n.st.verb === 'CALL') out.push(f);
648
+ }
649
+ }
650
+ }
651
+ if (n.e && n.e.kind === 'SQL') {
652
+ // A statement fills the host variables of its INTO list and reads the rest.
653
+ const toks = n.e.toks;
654
+ const into = toks.findIndex((t) => WORD(t, 'INTO'));
655
+ if (into >= 0) {
656
+ for (let i = into + 1; i < toks.length && !WORD(toks[i], 'FROM'); i++) {
657
+ if (toks[i].t === 'op' && toks[i].v === ':' && toks[i + 1]) add(toks[i + 1]);
658
+ }
659
+ }
660
+ } else if (n.e) {
661
+ // A command may fill what any option is given directly, except what it sends FROM. A name
662
+ // inside that argument's own parentheses - a subscript, a reference modification - is read.
663
+ let depth = 0;
664
+ let option = null;
665
+ for (let i = 0; i < n.e.toks.length; i++) {
666
+ const t = n.e.toks[i];
667
+ if (t.t === 'sep') { depth += t.v === '(' ? 1 : -1; continue; }
668
+ if (depth === 0) { option = t.t === 'word' ? t.u : option; continue; }
669
+ if (depth === 1 && t.t === 'word' && option !== 'FROM') add(t);
670
+ }
671
+ }
672
+ return out;
673
+ };
674
+ // A VALUE holds for the whole run only if no statement writes storage that overlaps the item.
675
+ let writtenItems = null;
676
+ const neverWritten = (it) => {
677
+ if (it.external) return false;
678
+ if (!writtenItems) {
679
+ writtenItems = [];
680
+ for (const n of nodes) if (n.kind === 'stmt' || n.kind === 'exec' || n.kind === 'step' || n.kind === 'call') writtenItems.push(...writtenBy(n));
681
+ }
682
+ return !writtenItems.some((w) => overlaps(w, it));
683
+ };
604
684
  const constValue = (toks) => {
605
685
  if (toks.length === 3 && WORD(toks[0], 'LENGTH') && WORD(toks[1], 'OF')) {
606
686
  const it = resolve(toks[2]);
@@ -613,7 +693,7 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
613
693
  if (t.t === 'word' && FIGURATIVE[t.u] != null) return FIGURATIVE[t.u];
614
694
  const it = resolve(t);
615
695
  // A level-78 constant, or a field with a VALUE that no statement writes, stands for its value.
616
- if (it && !it.index && (it.level === 78 || it.constant || (!it.receiving && it.values && it.values.length === 1))) {
696
+ if (it && !it.index && (it.level === 78 || it.constant || (!it.receiving && it.values && it.values.length === 1 && neverWritten(it)))) {
617
697
  const v = it.values[0];
618
698
  if (v && v.t === 'lit') return String(v.v).trimEnd();
619
699
  if (v && (v.t === 'num' || /^[+-]?\d+(\.\d+)?$/.test(v.v))) return String(Number(v.v));
@@ -660,6 +740,157 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
660
740
  if (range) return join(range, { set: new Set(vals) }) || null;
661
741
  return vals.length ? { set: new Set(vals) } : null;
662
742
  };
743
+ const pictureOf = (field) => String(field.picture || '').toUpperCase().replace(/(\w)\((\d+)\)/g, (_, ch, n) => ch.repeat(Number(n)));
744
+ const categories = new Map();
745
+ const categoryOf = (field) => {
746
+ if (categories.has(field)) return categories.get(field);
747
+ const usage = String(field.effectiveUsage || 'DISPLAY').replace('COMPUTATIONAL', 'COMP');
748
+ const pic = pictureOf(field);
749
+ const cat = (field.children || []).some((c) => c.level !== 88) ? { k: 'A' }
750
+ : /^[XA]+$/.test(pic) && usage === 'DISPLAY' ? { k: 'A' }
751
+ : /^S?9+(V9*)?$/.test(pic) && /^(DISPLAY|COMP|COMP-3|COMP-4|COMP-5|BINARY|PACKED-DECIMAL)$/.test(usage) ? { k: 'N', digits: pic.split('V')[0].replace(/[^9]/g, '').length }
752
+ : null;
753
+ categories.set(field, cat);
754
+ return cat;
755
+ };
756
+ // An unsigned whole number, for which "not zero" is "at least one".
757
+ const unsignedInteger = (field) => !!field && !field.index && categoryOf(field)?.k === 'N' && /^9+$/.test(pictureOf(field));
758
+ // What a numeric item's picture and storage allow: an unsigned item is not negative, a signed one
759
+ // has no known lower end, and a binary item may fill its bytes. Null for anything else.
760
+ const picRange = (f) => {
761
+ if (!f || f.index || f.level === 88 || f.level === 66 || categoryOf(f)?.k !== 'N') return null;
762
+ const pic = pictureOf(f);
763
+ let hi = 10 ** pic.split('V')[0].replace(/[^9]/g, '').length - (pic.includes('V') ? 0 : 1);
764
+ if (/^(COMP|COMP-4|COMP-5|BINARY)$/.test(String(f.effectiveUsage || '').replace('COMPUTATIONAL', 'COMP')) && f.size > 0) hi = Math.max(hi, 2 ** (8 * f.size) - 1);
765
+ return { lo: pic.startsWith('S') ? -Infinity : 0, hi };
766
+ };
767
+ const variableLength = (it) => !!it.dependingOn || (it.children || []).some(variableLength);
768
+ const linAdd = (a, b, sign) => {
769
+ if (!a || !b) return null;
770
+ const terms = new Map(a.terms);
771
+ for (const [f, c] of b.terms) { const n = (terms.get(f) || 0) + sign * c; if (n) terms.set(f, n); else terms.delete(f); }
772
+ const r = { terms, lo: a.lo + (sign > 0 ? b.lo : -b.hi), hi: a.hi + (sign > 0 ? b.hi : -b.lo) };
773
+ return Math.abs(r.lo) > EXPR_LIMIT || Math.abs(r.hi) > EXPR_LIMIT ? null : r;
774
+ };
775
+ // An arithmetic expression as `terms` (a coefficient per numeric field) plus a `lo`..`hi` interval
776
+ // for its constants, or null for what it cannot read exactly: division, a power, a product of two
777
+ // unknowns, a subscript, a non-numeric name, or a number past EXPR_LIMIT where the compiler's
778
+ // intermediate precision could give way.
779
+ const linearOf = (toks) => {
780
+ let i = 0;
781
+ const isOp = (v) => toks[i] && toks[i].t === 'op' && toks[i].v === v;
782
+ const isSep = (v) => toks[i] && toks[i].t === 'sep' && toks[i].v === v;
783
+ const constant = (n) => (Math.abs(n) > EXPR_LIMIT ? null : { terms: new Map(), lo: n, hi: n });
784
+ // LENGTH OF is the item's size, which a table that varies in length only bounds.
785
+ const sized = (tok) => {
786
+ const it = tok && tok.t === 'word' ? resolve(tok) : null;
787
+ if (!it || it.index || it.level === 88 || it.size == null || it.size > EXPR_LIMIT) return null;
788
+ return { terms: new Map(), lo: variableLength(it) ? 0 : it.size, hi: it.size };
789
+ };
790
+ const times = (a, b) => {
791
+ if (!a || !b) return null;
792
+ const scalar = (e) => (!e.terms.size && e.lo === e.hi ? e.lo : null);
793
+ const k = scalar(a) ?? scalar(b);
794
+ const e = scalar(a) != null ? b : a;
795
+ if (k == null && (a.terms.size || b.terms.size)) return null;
796
+ if (k == null) {
797
+ const ends = [a.lo * b.lo, a.lo * b.hi, a.hi * b.lo, a.hi * b.hi];
798
+ const r = { terms: new Map(), lo: Math.min(...ends), hi: Math.max(...ends) };
799
+ return Math.abs(r.lo) > EXPR_LIMIT || Math.abs(r.hi) > EXPR_LIMIT ? null : r;
800
+ }
801
+ const terms = new Map();
802
+ for (const [f, c] of e.terms) if (c * k) terms.set(f, c * k);
803
+ const ends = [e.lo * k, e.hi * k];
804
+ const r = { terms, lo: Math.min(...ends), hi: Math.max(...ends) };
805
+ return Math.abs(r.lo) > EXPR_LIMIT || Math.abs(r.hi) > EXPR_LIMIT ? null : r;
806
+ };
807
+ const primary = () => {
808
+ const t = toks[i];
809
+ if (!t) return null;
810
+ if (isSep('(')) {
811
+ i++;
812
+ const e = sum();
813
+ if (!isSep(')')) return null;
814
+ i++;
815
+ return e;
816
+ }
817
+ if (t.t !== 'word' && t.t !== 'num') return null;
818
+ if (WORD(t, 'LENGTH') && WORD(toks[i + 1], 'OF')) { const e = sized(toks[i + 2]); i += 3; return e; }
819
+ if (WORD(t, 'FUNCTION') && WORD(toks[i + 1], 'LENGTH') && toks[i + 2] && toks[i + 2].v === '(') {
820
+ const e = sized(toks[i + 3]);
821
+ if (!toks[i + 4] || toks[i + 4].v !== ')') return null;
822
+ i += 5;
823
+ return e;
824
+ }
825
+ i++;
826
+ while ((WORD(toks[i], 'OF') || WORD(toks[i], 'IN')) && toks[i + 1] && toks[i + 1].t === 'word') i += 2;
827
+ if (isSep('(')) return null;
828
+ const n = numberOf(constValue([t]));
829
+ if (n != null) return constant(n);
830
+ const f = t.t === 'word' ? resolve(t) : null;
831
+ return picRange(f) ? { terms: new Map([[f, 1]]), lo: 0, hi: 0 } : null;
832
+ };
833
+ const factor = () => {
834
+ if (isOp('-')) { i++; return times(constant(-1), primary()); }
835
+ if (isOp('+')) i++;
836
+ return primary();
837
+ };
838
+ const product = () => {
839
+ let e = factor();
840
+ while (e && isOp('*')) { i++; e = times(e, factor()); }
841
+ return e;
842
+ };
843
+ function sum() {
844
+ let e = product();
845
+ while (e && (isOp('+') || isOp('-'))) { const sign = toks[i].v === '+' ? 1 : -1; i++; e = linAdd(e, product(), sign); }
846
+ return e;
847
+ }
848
+ const e = sum();
849
+ return e && i === toks.length ? e : null;
850
+ };
851
+ // What `left op right` says of each field in it that has coefficient 1 or -1, from the rest of the
852
+ // expression: X + Y <= K gives X <= K - Y, and Y unsigned gives X <= K. The other operand counts by
853
+ // what its picture rules out (a sign) and by constants alone; a bound it has only from checks is
854
+ // left as `dep`, for the second pass to join to those checks. Only what narrows the field is kept.
855
+ const exprCons = (left, op, right) => {
856
+ const l = linearOf(left), r = linearOf(right);
857
+ const e = l && r ? linAdd(l, r, -1) : null;
858
+ const out = new Map();
859
+ if (!e || !['<', '<=', '>', '>='].includes(op)) return out;
860
+ const upper = op === '<' || op === '<=';
861
+ const inc = op === '<=' || op === '>=';
862
+ for (const [x, c] of e.terms) {
863
+ if (Math.abs(c) !== 1) continue;
864
+ const own = picRange(x);
865
+ // c*x + sum(d*g) + R op 0, R in [e.lo, e.hi]: x is on the `side` of the sum of m*g, plus b.
866
+ const side = upper === (c > 0) ? 'hi' : 'lo';
867
+ const b = -c * (upper ? e.lo : e.hi);
868
+ const open = [];
869
+ const soft = [];
870
+ for (const [g, d] of e.terms) {
871
+ if (g === x) continue;
872
+ const m = -c * d;
873
+ const needs = side === 'hi' ? (m > 0 ? 'hi' : 'lo') : (m > 0 ? 'lo' : 'hi');
874
+ if (!(needs === 'lo' && picRange(g).lo === 0)) open.push({ g, m, needs }); else soft.push({ g, m, needs });
875
+ }
876
+ if (open.length > 1) continue;
877
+ const c1 = { viaExpr: true };
878
+ // An operand whose picture gives 0 for its low end may have a higher one from a check: the picture's bound stands, and the check's tightens it.
879
+ if (!open.length) {
880
+ c1[side] = b; c1[`${side}Inc`] = inc;
881
+ if (soft.length === 1) c1.dep = { g: soft[0].g, m: soft[0].m, needs: soft[0].needs, side, b, inc };
882
+ } else c1.dep = { g: open[0].g, m: open[0].m, needs: open[0].needs, side, b, inc };
883
+ const narrows = c1[side] != null && (side === 'hi' ? c1.hi < own.hi || (c1.hi === own.hi && !inc) : c1.lo > own.lo || (c1.lo === own.lo && !inc));
884
+ if (narrows || c1.dep) out.set(fieldKey(x), c1);
885
+ }
886
+ return out;
887
+ };
888
+ // A constraint without its `dep`, or null where nothing else is left.
889
+ const plainCons = (c) => {
890
+ if (!c || !c.dep) return c || null;
891
+ const { dep, ...rest } = c;
892
+ return Object.keys(rest).some((k) => k !== 'viaExpr') ? rest : null;
893
+ };
663
894
  // What a condition says each field holds when it is `outcome`, and which fields it tests.
664
895
  function consOf(tree, outcome) {
665
896
  switch (tree.k) {
@@ -674,7 +905,7 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
674
905
  case 'sign': {
675
906
  const f = fieldOf(tree.subject);
676
907
  if (!f) return new Map();
677
- const c = tree.sign === 'ZERO' ? (outcome ? { set: new Set(['0']) } : null)
908
+ const c = tree.sign === 'ZERO' ? (outcome ? { set: new Set(['0']) } : unsignedInteger(f) ? { lo: 1, loInc: true } : null)
678
909
  : tree.sign === 'POSITIVE' ? (outcome ? { lo: 0, loInc: false } : { hi: 0, hiInc: true })
679
910
  : (outcome ? { hi: 0, hiInc: false } : { lo: 0, loInc: true });
680
911
  return c ? new Map([[fieldKey(f), c]]) : new Map();
@@ -686,9 +917,10 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
686
917
  if (!f || v == null) {
687
918
  const g = fieldOf(tree.right);
688
919
  const w = constValue(tree.left);
689
- if (g && w != null) { f = g; v = w; op = FLIP[op]; } else return new Map();
920
+ if (g && w != null) { f = g; v = w; op = FLIP[op]; } else return exprCons(tree.left, outcome ? tree.op : NEGATE[tree.op], tree.right);
690
921
  }
691
- const c = relCons(outcome ? op : NEGATE[op], v);
922
+ const eff = outcome ? op : NEGATE[op];
923
+ const c = eff === '<>' && numberOf(v) === 0 && unsignedInteger(f) ? { lo: 1, loInc: true } : relCons(eff, v);
692
924
  return c ? new Map([[fieldKey(f), c]]) : new Map();
693
925
  }
694
926
  case 'cond': {
@@ -787,7 +1019,7 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
787
1019
  for (const key of keys) {
788
1020
  const field = restricts.get(key) || (typeof key === 'string' ? { index: key.slice(3) } : key);
789
1021
  noteField(key, field);
790
- const c = { id: checks.length, key, field, file: where.file, line: where.line, tCons: whenTrue.get(key) || null, fCons: whenFalse.get(key) || null, restricts: restricts.has(key) };
1022
+ const c = { id: checks.length, key, field, file: where.file, line: where.line, test: id, tCons: plainCons(whenTrue.get(key)), fCons: plainCons(whenFalse.get(key)), tDep: whenTrue.get(key)?.dep || null, fDep: whenFalse.get(key)?.dep || null, restricts: restricts.has(key) };
791
1023
  checks.push(c);
792
1024
  mine.push(c);
793
1025
  }
@@ -835,6 +1067,25 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
835
1067
  a.c = { id: checks.length, key, field: a.field, file: a.n.st.file, line: a.n.st.line, tCons: { set: new Set([a.value]) }, fCons: null, restricts: false, assigned: true };
836
1068
  checks.push(a.c);
837
1069
  }
1070
+ // One fact per distinct constant v moved into a whole number: "it holds a constant from 1 to v", set by every MOVE of one that small, so it survives a join only where every route moved one.
1071
+ const moved = new Map();
1072
+ for (const a of assigns) {
1073
+ const cat = a.field.level === 88 ? null : categoryOf(a.field);
1074
+ const v = numberOf(a.value);
1075
+ if (!cat || cat.k !== 'N' || v == null || !Number.isInteger(v) || v < 1 || String(v).length > cat.digits) continue;
1076
+ if (!moved.has(a.field)) moved.set(a.field, []);
1077
+ moved.get(a.field).push(a);
1078
+ }
1079
+ for (const [field, list] of moved) {
1080
+ const key = fieldKey(field);
1081
+ noteField(key, field);
1082
+ for (const hi of new Set(list.map((a) => Number(a.value)))) {
1083
+ const bounds = { lo: 1, hi };
1084
+ const c = { id: checks.length, key, field, file: list[0].n.st.file, line: list[0].n.st.line, tCons: { numeric: true, lo: 1, loInc: true, hi, hiInc: true }, fCons: null, restricts: false, assigned: true, bounds };
1085
+ checks.push(c);
1086
+ for (const a of list) if (Number(a.value) <= hi) (a.upTo ||= []).push(c);
1087
+ }
1088
+ }
838
1089
  // Fact numbers: for each check, `x` that it ran (only a restricting one), `t` and `f` that it came
839
1090
  // out true or false with something to say about its field.
840
1091
  let nFacts = 0;
@@ -842,6 +1093,8 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
842
1093
  c.x = c.restricts ? nFacts++ : null;
843
1094
  c.t = c.tCons ? nFacts++ : null;
844
1095
  c.f = c.fCons ? nFacts++ : null;
1096
+ c.rt = c.tDep ? nFacts++ : null;
1097
+ c.rf = c.fDep ? nFacts++ : null;
845
1098
  }
846
1099
  for (const id of tests) {
847
1100
  const n = nodes[id];
@@ -849,6 +1102,8 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
849
1102
  if (c.x != null) { n.gen.true.push(c.x); n.gen.false.push(c.x); }
850
1103
  if (c.t != null) n.gen.true.push(c.t);
851
1104
  if (c.f != null) n.gen.false.push(c.f);
1105
+ if (c.rt != null) n.gen.true.push(c.rt);
1106
+ if (c.rf != null) n.gen.false.push(c.rf);
852
1107
  }
853
1108
  }
854
1109
  // A constant also makes true every outcome it satisfies of a check on the same field, so a length
@@ -865,6 +1120,7 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
865
1120
  for (const a of assigns) {
866
1121
  const gen = (a.n.genStmt ||= []);
867
1122
  gen.push(a.c.t);
1123
+ for (const c of a.upTo || []) gen.push(c.t);
868
1124
  for (const c of checksOf.get(a.c.key) || []) {
869
1125
  if (c.t != null && satisfies(c.tCons, a.value)) gen.push(c.t);
870
1126
  if (c.f != null && satisfies(c.fCons, a.value)) gen.push(c.f);
@@ -896,6 +1152,7 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
896
1152
  return it && !it.index && it.level !== 88 && it.size != null ? it.size : null;
897
1153
  };
898
1154
  const writesRecordOf = (st, item) => (st.targets || []).some((t) => { const it = resolve(t); return it && !it.index && itemsInRecord(it) === itemsInRecord(item); });
1155
+ const tallies = [];
899
1156
  const tallyIn = (seq) => {
900
1157
  for (let j = 0; j < seq.length; j++) {
901
1158
  const el = seq[j];
@@ -921,6 +1178,7 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
921
1178
  const bounds = { lo: start, hi: start + size };
922
1179
  const c = { id: checks.length, key, field: count, file: el.st.file, line: el.st.line, tCons: { numeric: true, lo: bounds.lo, loInc: true, hi: bounds.hi, hiInc: true }, fCons: null, restricts: false, x: null, t: nFacts++, f: null, bounds };
923
1180
  checks.push(c);
1181
+ tallies.push(c);
924
1182
  (n.genStmt ||= []).push(c.t);
925
1183
  }
926
1184
  break;
@@ -950,19 +1208,6 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
950
1208
  // A flag's values are compared only where nothing but the literal decides the comparison: an
951
1209
  // alphanumeric literal against an alphanumeric item, which a value it is given must fill so that
952
1210
  // JUSTIFIED cannot move it, and an integer against a numeric item with room for its digits.
953
- const pictureOf = (field) => String(field.picture || '').toUpperCase().replace(/(\w)\((\d+)\)/g, (_, ch, n) => ch.repeat(Number(n)));
954
- const categories = new Map();
955
- const categoryOf = (field) => {
956
- if (categories.has(field)) return categories.get(field);
957
- const usage = String(field.effectiveUsage || 'DISPLAY').replace('COMPUTATIONAL', 'COMP');
958
- const pic = pictureOf(field);
959
- const cat = (field.children || []).some((c) => c.level !== 88) ? { k: 'A' }
960
- : /^[XA]+$/.test(pic) && usage === 'DISPLAY' ? { k: 'A' }
961
- : /^S?9+(V9*)?$/.test(pic) && /^(DISPLAY|COMP|COMP-3|COMP-4|COMP-5|BINARY|PACKED-DECIMAL)$/.test(usage) ? { k: 'N', digits: pic.split('V')[0].replace(/[^9]/g, '').length }
962
- : null;
963
- categories.set(field, cat);
964
- return cat;
965
- };
966
1211
  const plainLiteral = (tok) => tok && tok.t === 'lit' && !tok.prefix;
967
1212
  const integerOf = (tok) => (tok && (tok.t === 'num' || tok.t === 'word') && /^\d+$/.test(tok.v) ? tok.v : null);
968
1213
  const heldAs = (field, tok) => {
@@ -980,7 +1225,15 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
980
1225
  return digits != null ? `N:${Number(digits)}` : null;
981
1226
  };
982
1227
  const heldBy = new Map();
983
- const held = (a) => { if (!heldBy.has(a)) heldBy.set(a, heldAs(a.field, a.tok)); return heldBy.get(a); };
1228
+ // A flag moved from a constant item holds that item's value, when the item fills the flag exactly.
1229
+ const heldTok = (a) => {
1230
+ const t = a.tok;
1231
+ if (!t || t.t !== 'word' || /^\d+$/.test(t.v)) return t;
1232
+ const src = resolve(t);
1233
+ if (!src || src.index || (src.level !== 78 && src.size !== a.field.size)) return t;
1234
+ return { t: 'lit', v: a.value };
1235
+ };
1236
+ const held = (a) => { if (!heldBy.has(a)) heldBy.set(a, heldAs(a.field, heldTok(a))); return heldBy.get(a); };
984
1237
  const flagFields = (tree, out = new Set()) => {
985
1238
  if (tree.k === 'not') flagFields(tree.a, out);
986
1239
  else if (tree.k === 'and' || tree.k === 'or') { flagFields(tree.a, out); flagFields(tree.b, out); }
@@ -1074,6 +1327,733 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
1074
1327
  }
1075
1328
  }
1076
1329
  }
1330
+ // A lower bound survives an increment. A field set to a constant of 1 or more, or varied from one,
1331
+ // stays at 1 or more through ADD, SET UP BY, COMPUTE X = X + n and a VARYING step, where n is
1332
+ // never negative. The upper bound is lost, and the lower bound survives only where the sum cannot
1333
+ // wrap past the field's capacity to a small number: an upper bound live before the increment that
1334
+ // leaves room for n, or ON SIZE ERROR, which leaves the field as it was. Any other write kills it.
1335
+ const intCap = (f) => {
1336
+ if (!f || f.index || f.level === 88 || inTable(f)) return null;
1337
+ const cat = categoryOf(f);
1338
+ return cat && cat.k === 'N' && /^S?9+$/.test(pictureOf(f)) ? 10 ** cat.digits - 1 : null;
1339
+ };
1340
+ // The most a token can add: a literal or constant that is not negative, or an unsigned number.
1341
+ const addend = (tok) => {
1342
+ if (!tok || (tok.t !== 'word' && tok.t !== 'num')) return null;
1343
+ const v = numberOf(constValue([tok]));
1344
+ if (v != null) return v >= 0 ? Math.ceil(v) : null;
1345
+ const f = tok.t === 'word' ? resolve(tok) : null;
1346
+ const cat = f && !f.index && f.level !== 88 && !inTable(f) ? categoryOf(f) : null;
1347
+ return cat && cat.k === 'N' && /^9+(V9*)?$/.test(pictureOf(f)) ? 10 ** cat.digits : null;
1348
+ };
1349
+ // The least a token adds: its value where it is a constant, else nothing.
1350
+ const least = (tok) => { const v = numberOf(constValue([tok])); return v != null && v > 0 ? Math.floor(v) : 0; };
1351
+ const PHRASE_AT = (t) => WORD(t, 'ON') || WORD(t, 'NOT') || WORD(t, 'SIZE') || (t.t === 'word' && t.u.startsWith('END-'));
1352
+ const sizeErrorGuarded = (seg) => seg.some((t, i) => WORD(t, 'SIZE') && WORD(seg[i + 1], 'ERROR') && !WORD(seg[i - 1], 'NOT'));
1353
+ // The fields a statement only ever raises, and by how much at most.
1354
+ const increments = (st) => {
1355
+ const seg = segOf(st);
1356
+ let max = 0;
1357
+ let min = 0;
1358
+ let fields = [];
1359
+ if (st.verb === 'ADD') {
1360
+ const to = seg.findIndex((t) => WORD(t, 'TO'));
1361
+ if (to < 1 || seg.some((t) => WORD(t, 'GIVING') || WORD(t, 'CORRESPONDING') || WORD(t, 'CORR'))) return null;
1362
+ for (const t of seg.slice(0, to)) { const m = addend(t); if (m == null) return null; max += m; min += least(t); }
1363
+ const end = seg.findIndex((t, i) => i > to && PHRASE_AT(t));
1364
+ if (seg.slice(to + 1, end < 0 ? seg.length : end).some((t) => t.t === 'sep')) return null;
1365
+ fields = (st.targets || []).map((t) => resolve(t));
1366
+ } else if (st.verb === 'SET') {
1367
+ const up = seg.findIndex((t) => WORD(t, 'UP'));
1368
+ if (up < 1 || !WORD(seg[up + 1], 'BY') || seg.length !== up + 3 || seg.slice(0, up).some((t) => t.t !== 'word')) return null;
1369
+ max = addend(seg[up + 2]);
1370
+ if (max == null) return null;
1371
+ min = least(seg[up + 2]);
1372
+ fields = (st.targets || []).map((t) => resolve(t));
1373
+ } else if (st.verb === 'COMPUTE') {
1374
+ const eq = seg.findIndex((t) => t.t === 'op' && t.v === '=');
1375
+ const lhs = seg.slice(0, eq).filter((t) => !WORD(t, 'ROUNDED'));
1376
+ if (eq < 1 || lhs.length !== 1 || lhs[0].t !== 'word') return null;
1377
+ const target = resolve(lhs[0]);
1378
+ const end = seg.findIndex((t, i) => i > eq && PHRASE_AT(t));
1379
+ const parts = [[]];
1380
+ for (const t of seg.slice(eq + 1, end < 0 ? seg.length : end)) { if (t.t === 'op' && t.v === '+') parts.push([]); else parts[parts.length - 1].push(t); }
1381
+ if (parts.length < 2 || parts.some((p) => p.length !== 1)) return null;
1382
+ const self = parts.filter((p) => p[0].t === 'word' && resolve(p[0]) === target);
1383
+ if (self.length !== 1) return null;
1384
+ for (const p of parts) { if (p === self[0]) continue; const m = addend(p[0]); if (m == null) return null; max += m; min += least(p[0]); }
1385
+ fields = [target];
1386
+ } else return null;
1387
+ fields = [...new Set(fields.filter((f) => intCap(f) != null))];
1388
+ return fields.length ? { fields, max, min, sizeError: sizeErrorGuarded(seg) } : null;
1389
+ };
1390
+ // Facts of the form "at least k", one per field and k so that routes which reach it by different
1391
+ // statements agree. They are kept apart because an increment does not kill them.
1392
+ const loOf = new Map();
1393
+ const newLo = (field, lo, at) => {
1394
+ const key = fieldKey(field);
1395
+ const had = (loOf.get(field) || []).find((c) => c.tCons.lo === lo);
1396
+ if (had) return had;
1397
+ noteField(key, field);
1398
+ const c = { id: checks.length, key, field, file: at.file, line: at.line, tCons: { lo, loInc: true }, fCons: null, restricts: false, x: null, t: nFacts++, f: null, mono: true };
1399
+ checks.push(c);
1400
+ if (!loOf.has(field)) loOf.set(field, []);
1401
+ loOf.get(field).push(c);
1402
+ return c;
1403
+ };
1404
+ // A field an increment by at least 1 raises can start from 0: the bound it reaches is 1 or more.
1405
+ const raisers = new Map();
1406
+ const raised = new Set();
1407
+ nodes.forEach((n, id) => {
1408
+ if (n.kind !== 'stmt' || !n.st || n.st.at == null) return;
1409
+ const inc = increments(n.st);
1410
+ if (!inc || inc.min < 1 || inc.sizeError) return;
1411
+ raisers.set(id, inc);
1412
+ for (const f of inc.fields) raised.add(f);
1413
+ });
1414
+ for (const a of assigns) {
1415
+ const v = numberOf(a.value);
1416
+ const cap = intCap(a.field);
1417
+ if (cap == null || v == null || !Number.isInteger(v) || v < (raised.has(a.field) ? 0 : 1) || v > cap) continue;
1418
+ if ((a.n.st.targets || []).some((t) => { const o = resolve(t); return o && o !== a.field && overlaps(o, a.field); })) continue;
1419
+ (a.n.genStmt ||= []).push(newLo(a.field, v, a.n.st).t);
1420
+ }
1421
+ // An increment by at least m turns "at least k" into "at least k + m", where the sum fits the field.
1422
+ const lifted = new Map();
1423
+ for (const [id, inc] of raisers) {
1424
+ const list = [];
1425
+ for (const field of inc.fields) {
1426
+ const pairs = [];
1427
+ for (const c of [...(loOf.get(field) || []), ...tallies.filter((t) => t.field === field)]) {
1428
+ const lo = c.tCons.lo + inc.min;
1429
+ if (lo < 1) continue;
1430
+ if (lo <= intCap(field)) pairs.push([c.t, newLo(field, lo, nodes[id].st).t]);
1431
+ }
1432
+ if (pairs.length) list.push({ field, max: inc.max, pairs });
1433
+ }
1434
+ if (list.length) lifted.set(id, list);
1435
+ }
1436
+ // A VARYING counter is at least what it starts at inside the loop: a constant FROM sets it at the
1437
+ // head, and a FROM naming a field that is at least k sets it to k, where that field held it.
1438
+ const stepped = new Map();
1439
+ const derived = new Map();
1440
+ for (let more = true; more;) {
1441
+ more = false;
1442
+ for (const { head, step, loops } of loopHeads) {
1443
+ loops.forEach((loop, idx) => {
1444
+ const field = loop.counter ? resolve(loop.counter) : null;
1445
+ const cap = intCap(field);
1446
+ const by = loop.by ? (loop.by.length === 1 ? addend(loop.by[0]) : null) : 1;
1447
+ if (cap == null || by == null) return;
1448
+ const at = { file: loop.counter.file, line: loop.counter.line };
1449
+ const from = numberOf(constValue(loop.from || []));
1450
+ if (from != null) {
1451
+ const key = `${head}|${idx}`;
1452
+ if (derived.has(key) || !Number.isInteger(from) || from < 1 || from > cap) return;
1453
+ const c = newLo(field, from, at);
1454
+ derived.set(key, c);
1455
+ nodes[head].genStmt = [...(nodes[head].genStmt || []), c.t];
1456
+ if (idx === 0) stepped.set(step, { field, max: by });
1457
+ else if (!loop.until.some((t) => { const v = numberOf(constValue([t])); return v != null && v >= cap; })) nodes[step].genStmt = [...(nodes[step].genStmt || []), c.t];
1458
+ more = true;
1459
+ return;
1460
+ }
1461
+ const src = idx === 0 && loop.from && loop.from.length === 1 && loop.from[0].t === 'word' ? resolve(loop.from[0]) : null;
1462
+ const srcCap = intCap(src);
1463
+ if (srcCap == null || srcCap > cap) return;
1464
+ for (const c of loOf.get(src) || []) {
1465
+ const key = `${head}|${idx}|${c.tCons.lo}`;
1466
+ let d = derived.get(key);
1467
+ if (!d) {
1468
+ d = newLo(field, c.tCons.lo, at);
1469
+ derived.set(key, d);
1470
+ stepped.set(step, { field, max: by });
1471
+ more = true;
1472
+ }
1473
+ const pair = `${head}|${c.t}>${d.t}`;
1474
+ if (derived.has(pair)) continue;
1475
+ derived.set(pair, true);
1476
+ (nodes[head].implies ||= []).push([c.t, d.t, true]);
1477
+ more = true;
1478
+ }
1479
+ });
1480
+ }
1481
+ }
1482
+
1483
+ // A bound an expression gives a field through another field, X <= G + 1, is a bound only where a
1484
+ // check has bounded G: "X <= G + 1 held" and "G <= 100 held" together give X <= 101. The first
1485
+ // fact dies with a write to X or G, the second with a write to G, and the result with a write to X,
1486
+ // so the pair is joined at whichever of the two checks comes last.
1487
+ const outcomeAt = new Map();
1488
+ for (const node of nodes) if (node.kind === 'outcome') outcomeAt.set(`${node.test}|${node.value}`, node);
1489
+ // A backward scan for the last byte past the spaces, VARYING I FROM the field's length BY -1 UNTIL
1490
+ // I < 1 OR F(I:1) > SPACE, leaves I from 1 to that length where F > SPACES held as it began: a
1491
+ // field that compares above spaces has a byte above a space, under any collating sequence, and the
1492
+ // scan meets it before 0. The witness dies with a write to F or to anything its subscript reads.
1493
+ const keptOnStep = new Map();
1494
+ const isSpaces = (toks) => toks.length === 1 && (WORD(toks[0], 'SPACE') || WORD(toks[0], 'SPACES') || (toks[0].t === 'lit' && !toks[0].prefix && /^ +$/.test(String(toks[0].v))));
1495
+ const textOf = (toks) => toks.map((t) => (t.t === 'word' ? t.u : String(t.v))).join(' ');
1496
+ // A whole alphanumeric item or table element, with the fields its subscript reads.
1497
+ const scanned = (toks) => {
1498
+ const base = toks[0] && toks[0].t === 'word' ? resolve(toks[0]) : null;
1499
+ if (!base || base.index || base.level === 88 || base.size == null || categoryOf(base)?.k !== 'A' || variableLength(base)) return null;
1500
+ let i = 1;
1501
+ while (i + 1 < toks.length && (WORD(toks[i], 'OF') || WORD(toks[i], 'IN')) && toks[i + 1].t === 'word') i += 2;
1502
+ const reads = [];
1503
+ if (i < toks.length) {
1504
+ if (toks[i].v !== '(' || toks[toks.length - 1].v !== ')') return null;
1505
+ let depth = 0;
1506
+ for (let j = i; j < toks.length; j++) {
1507
+ const t = toks[j];
1508
+ if (t.v === ':') return null;
1509
+ if (t.v === '(' || t.v === ')') { depth += t.v === '(' ? 1 : -1; if (!depth && j < toks.length - 1) return null; continue; }
1510
+ const f = t.t === 'word' ? resolve(t) : null;
1511
+ if (f) reads.push(f.level === 88 && f.parent ? f.parent : f);
1512
+ }
1513
+ }
1514
+ return { base, reads };
1515
+ };
1516
+ // What an outcome says is above spaces (`above`) or only not all spaces.
1517
+ const notBlank = (tree, outcome, out = []) => {
1518
+ if (tree.k === 'not') return notBlank(tree.a, !outcome, out);
1519
+ if ((tree.k === 'and' && outcome) || (tree.k === 'or' && !outcome)) { notBlank(tree.a, outcome, out); notBlank(tree.b, outcome, out); return out; }
1520
+ if (tree.k !== 'rel') return out;
1521
+ const op = outcome ? tree.op : NEGATE[tree.op];
1522
+ if (isSpaces(tree.right) && (op === '>' || op === '<>')) out.push({ toks: tree.left, above: op === '>' });
1523
+ else if (isSpaces(tree.left) && (op === '<' || op === '<>')) out.push({ toks: tree.right, above: op === '<' });
1524
+ return out;
1525
+ };
1526
+ const scans = [];
1527
+ for (const { head, step, test, testAfter, body, loops } of loopHeads) {
1528
+ if (testAfter || !body || loops.length !== 1 || !body.every((el) => el.k === 'stmt' && el.st.verb === 'CONTINUE')) continue;
1529
+ const loop = loops[0];
1530
+ const counter = loop.counter ? resolve(loop.counter) : null;
1531
+ const cap = intCap(counter);
1532
+ const tree = nodes[test].tree;
1533
+ if (cap == null || !tree || tree.k !== 'or' || !loop.by || loop.by.map((t) => t.v).join('') !== '-1') continue;
1534
+ const from = loop.from || [];
1535
+ let start = numberOf(constValue(from));
1536
+ if (start == null && WORD(from[0], 'LENGTH') && WORD(from[1], 'OF')) start = scanned(from.slice(2))?.base.size ?? null;
1537
+ if (start == null || !Number.isInteger(start) || start < 1 || start > cap) continue;
1538
+ const isCounter = (toks) => toks.length === 1 && toks[0].t === 'word' && resolve(toks[0]) === counter;
1539
+ const done = (r) => r.k === 'rel' && isCounter(r.left) && (r.op === '<' ? numberOf(constValue(r.right)) === 1 : (r.op === '<=' || r.op === '=') && numberOf(constValue(r.right)) === 0);
1540
+ const probe = (r) => {
1541
+ if (r.k !== 'rel') return null;
1542
+ const [el, op] = isSpaces(r.right) ? [r.left, r.op] : isSpaces(r.left) ? [r.right, FLIP[r.op]] : [null, null];
1543
+ if (!el || (op !== '>' && op !== '<>') || el.length < 6) return null;
1544
+ const tail = el.slice(-5);
1545
+ if (tail[0].v !== '(' || !isCounter([tail[1]]) || tail[2].v !== ':' || numberOf(constValue([tail[3]])) !== 1 || tail[4].v !== ')') return null;
1546
+ const toks = el.slice(0, -5);
1547
+ const s = scanned(toks);
1548
+ return s && s.base.size === start ? { toks, above: op === '>' } : null;
1549
+ };
1550
+ const p = done(tree.a) ? probe(tree.b) : done(tree.b) ? probe(tree.a) : null;
1551
+ const exit = p && outcomeAt.get(`${test}|true`);
1552
+ if (exit) scans.push({ ...p, counter, start, exit, head, step, at: loop.counter });
1553
+ }
1554
+ // The scan's counter never passes its start: the step runs only once I < 1 has failed, so it
1555
+ // cannot wrap, and the body writes nothing.
1556
+ for (const s of scans) {
1557
+ const key = fieldKey(s.counter);
1558
+ noteField(key, s.counter);
1559
+ const h = { id: checks.length, key, field: s.counter, file: s.at.file, line: s.at.line, tCons: { hi: s.start, hiInc: true }, fCons: null, restricts: false, x: null, t: nFacts++, f: null };
1560
+ checks.push(h);
1561
+ nodes[s.head].genStmt = [...(nodes[s.head].genStmt || []), h.t];
1562
+ keptOnStep.set(s.step, [...(keptOnStep.get(s.step) || []), h.t]);
1563
+ }
1564
+ if (scans.length) {
1565
+ const witness = new Map();
1566
+ const bitFor = (toks, above) => {
1567
+ const k = `${above ? '>' : '<>'}|${textOf(toks)}`;
1568
+ if (witness.has(k)) return witness.get(k);
1569
+ const s = scanned(toks);
1570
+ const bit = nFacts++;
1571
+ for (const f of [s.base, ...s.reads]) {
1572
+ const key = fieldKey(f);
1573
+ noteField(key, f);
1574
+ checks.push({ pseudo: true, key, field: f, x: null, t: bit, f: null });
1575
+ }
1576
+ witness.set(k, bit);
1577
+ return bit;
1578
+ };
1579
+ const wanted = new Set(scans.map((s) => textOf(s.toks)));
1580
+ for (const id of tests) {
1581
+ const n = nodes[id];
1582
+ if (!n.tree) continue;
1583
+ for (const outcome of [true, false]) {
1584
+ for (const nb of notBlank(n.tree, outcome)) {
1585
+ if (!wanted.has(textOf(nb.toks)) || !scanned(nb.toks)) continue;
1586
+ n.gen[String(outcome)].push(bitFor(nb.toks, false));
1587
+ if (nb.above) n.gen[String(outcome)].push(bitFor(nb.toks, true));
1588
+ }
1589
+ }
1590
+ }
1591
+ for (const s of scans) {
1592
+ const bit = witness.get(`${s.above ? '>' : '<>'}|${textOf(s.toks)}`);
1593
+ if (bit == null) continue;
1594
+ const key = fieldKey(s.counter);
1595
+ noteField(key, s.counter);
1596
+ const y = { id: checks.length, key, field: s.counter, file: s.at.file, line: s.at.line, tCons: { numeric: true, lo: 1, loInc: true, hi: s.start, hiInc: true }, fCons: null, restricts: false, x: null, t: nFacts++, f: null, bounds: { lo: 1, hi: s.start } };
1597
+ checks.push(y);
1598
+ (s.exit.implies ||= []).push([bit, y.t]);
1599
+ }
1600
+ }
1601
+ const MAX_BOUNDS_PER_RELATION = 8;
1602
+ for (const c of [...checks]) {
1603
+ if (c.pseudo || c.test == null) continue;
1604
+ for (const outcome of [true, false]) {
1605
+ const bit = outcome ? c.rt : c.rf;
1606
+ const dep = bit != null ? (outcome ? c.tDep : c.fDep) : null;
1607
+ if (!dep) continue;
1608
+ const gKey = fieldKey(dep.g);
1609
+ const bounds = [];
1610
+ for (const u of checks) {
1611
+ if (u.pseudo || u.key !== gKey || u.bounds || u.assigned || u === c) continue;
1612
+ for (const [ubit, ucons, uoutcome] of [[u.t, u.tCons, true], [u.f, u.fCons, false]]) {
1613
+ if (ubit == null || !ucons || ucons.dep) continue;
1614
+ const r = asRange(ucons);
1615
+ const gv = dep.needs === 'hi' ? r.hi : r.lo;
1616
+ if (gv == null) continue;
1617
+ const value = dep.m * gv + dep.b;
1618
+ if (!Number.isFinite(value) || Math.abs(value) > EXPR_LIMIT) continue;
1619
+ bounds.push({ u, ubit, uoutcome, value, inc: dep.inc && r[`${dep.needs}Inc`] !== false });
1620
+ }
1621
+ }
1622
+ if (!bounds.length || bounds.length > MAX_BOUNDS_PER_RELATION) continue;
1623
+ noteField(gKey, dep.g);
1624
+ checks.push({ pseudo: true, key: gKey, field: dep.g, x: null, t: bit, f: null });
1625
+ for (const { u, ubit, uoutcome, value, inc } of bounds) {
1626
+ const y = { id: checks.length, key: c.key, field: c.field, file: c.file, line: c.line, tCons: { [dep.side]: value, [`${dep.side}Inc`]: inc }, fCons: null, restricts: false, x: null, t: nFacts++, f: null };
1627
+ checks.push(y);
1628
+ const here = outcomeAt.get(`${c.test}|${outcome}`);
1629
+ if (here) (here.implies ||= []).push([ubit, y.t]);
1630
+ const there = u.test != null ? outcomeAt.get(`${u.test}|${uoutcome}`) : null;
1631
+ if (there) (there.implies ||= []).push([bit, y.t]);
1632
+ }
1633
+ }
1634
+ }
1635
+
1636
+ // ---- what an arithmetic statement leaves in an integer field ----
1637
+ // COMPUTE, ADD, SUBTRACT, MULTIPLY, DIVIDE and a MOVE of a field or function leave the receiving
1638
+ // field inside an interval worked out from what each operand can hold: its picture, the facts that
1639
+ // hold at the statement, the length of an item. The interval is a fact only where the result fits
1640
+ // the receiving field, so nothing was lost to truncation or overflow, and where no divisor can be
1641
+ // zero. Whatever the statement's operands needed to hold is what the fact needs to hold.
1642
+ const BINARY_USAGE = /^(COMP|COMP-4|COMP-5|BINARY)$/;
1643
+ const rangesOf = new Map();
1644
+ // The whole numbers an integer field can hold when read, and those a result may take to be stored whole.
1645
+ const rangeOfField = (f) => {
1646
+ if (!f || f.index || f.level === 88) return null;
1647
+ if (rangesOf.has(f)) return rangesOf.get(f);
1648
+ let out = null;
1649
+ const cat = categoryOf(f);
1650
+ const pic = pictureOf(f);
1651
+ if (cat && cat.k === 'N' && /^S?9+$/.test(pic)) {
1652
+ const D = 10n ** BigInt(cat.digits) - 1n;
1653
+ const signed = pic[0] === 'S';
1654
+ const digits = { lo: signed ? -D : 0n, hi: D };
1655
+ out = { hold: digits, fit: digits };
1656
+ if (BINARY_USAGE.test(String(f.effectiveUsage || 'DISPLAY').replace('COMPUTATIONAL', 'COMP'))) {
1657
+ if (f.size >= 1 && f.size <= 8) {
1658
+ const bits = BigInt(f.size * 8);
1659
+ const store = signed ? { lo: -(2n ** (bits - 1n)), hi: 2n ** (bits - 1n) - 1n } : { lo: 0n, hi: 2n ** bits - 1n };
1660
+ out = {
1661
+ hold: { lo: store.lo < digits.lo ? store.lo : digits.lo, hi: store.hi > digits.hi ? store.hi : digits.hi },
1662
+ fit: { lo: store.lo > digits.lo ? store.lo : digits.lo, hi: store.hi < digits.hi ? store.hi : digits.hi },
1663
+ };
1664
+ } else out = null;
1665
+ }
1666
+ }
1667
+ rangesOf.set(f, out);
1668
+ return out;
1669
+ };
1670
+ // The most bytes an item named by these tokens can be, as a subscripted element or as written.
1671
+ const sizeBound = (span) => {
1672
+ const f = span[0] && span[0].t === 'word' ? resolve(span[0]) : null;
1673
+ if (!f || f.index || f.level === 88 || f.size == null || !Number.isInteger(f.size)) return null;
1674
+ if (span.length === 1) return f.contributes || f.size;
1675
+ if (!WORD(span[1], 'OF') && span[1].v === '(' && span[span.length - 1].v === ')' && !span.some((t) => t.t === 'op' && t.v === ':')) {
1676
+ return (f.children || []).some((c) => c.level !== 88) ? Math.ceil(f.size / Math.max(1, f.occurs || 1)) : f.size;
1677
+ }
1678
+ return null;
1679
+ };
1680
+ const arithEnv = {
1681
+ field: (tok, subscripted) => {
1682
+ const f = tok.t === 'word' ? resolve(tok) : null;
1683
+ return rangeOfField(f) ? { k: 'field', f, tok, plain: !subscripted && !inTable(f) } : null;
1684
+ },
1685
+ sizeOf: sizeBound,
1686
+ };
1687
+ const MAX_SAFE = BigInt(Number.MAX_SAFE_INTEGER);
1688
+ // A check's outcome as whole-number ends, from a range or a set of numbers; null ends are open.
1689
+ const wholeEnds = (cons) => {
1690
+ const isNum = (v) => /^[+-]?\d+(\.\d+)?$/.test(v);
1691
+ if (cons.set && !(cons.set.size && [...cons.set].every(isNum))) return null;
1692
+ const r = cons.set ? asRange(cons) : cons;
1693
+ const end = (v, up, inc) => (v == null || Math.abs(v) >= Number.MAX_SAFE_INTEGER ? null : BigInt(up ? (inc ? Math.floor(v) : Math.ceil(v) - 1) : (inc ? Math.ceil(v) : Math.floor(v) + 1)));
1694
+ const lo = end(r.lo, false, r.loInc);
1695
+ const hi = end(r.hi, true, r.hiInc);
1696
+ return lo == null && hi == null ? null : { lo, hi };
1697
+ };
1698
+ let factsByKey = new Map();
1699
+ const indexFacts = () => {
1700
+ factsByKey = new Map();
1701
+ for (const c of checks) {
1702
+ if (c.pseudo || c.key == null || (!c.tCons && !c.fCons)) continue;
1703
+ if (!factsByKey.has(c.key)) factsByKey.set(c.key, []);
1704
+ factsByKey.get(c.key).push(c);
1705
+ }
1706
+ };
1707
+ // The facts held where the statement being read runs, from the last analysis; none on the first pass.
1708
+ let heldBits = null;
1709
+ // What a field can hold, and the facts that say so: its picture, or a constant, or the tightest
1710
+ // lower and upper end among the facts that hold here.
1711
+ const leafCands = (leaf) => {
1712
+ const r = rangeOfField(leaf.f);
1713
+ const base = { needs: [], lo: r.hold.lo, hi: r.hold.hi };
1714
+ if (!leaf.plain) return [base];
1715
+ const cv = numberOf(constValue([leaf.tok]));
1716
+ if (cv != null && Number.isInteger(cv)) return [{ needs: [], lo: BigInt(cv), hi: BigInt(cv) }];
1717
+ if (!heldBits) return [base];
1718
+ let lo = null;
1719
+ let hi = null;
1720
+ for (const c of factsByKey.get(fieldKey(leaf.f)) || []) {
1721
+ for (const [bit, cons] of [[c.t, c.tCons], [c.f, c.fCons]]) {
1722
+ if (bit == null || !cons || !hasFact(heldBits, bit)) continue;
1723
+ const e = wholeEnds(cons);
1724
+ if (!e) continue;
1725
+ if (e.lo != null && e.lo > (lo ? lo.v : base.lo)) lo = { bit, v: e.lo };
1726
+ if (e.hi != null && e.hi < (hi ? hi.v : base.hi)) hi = { bit, v: e.hi };
1727
+ }
1728
+ }
1729
+ const needs = [...new Set([lo && lo.bit, hi && hi.bit].filter((x) => x != null))].sort((x, y) => x - y);
1730
+ const from = lo ? lo.v : base.lo;
1731
+ const to = hi ? hi.v : base.hi;
1732
+ return needs.length && from <= to ? [base, { needs, lo: from, hi: to }] : [base];
1733
+ };
1734
+ // A fact "the field holds a whole number from lo to hi" (either end may be open), one per field and interval.
1735
+ const derivedOf = new Map();
1736
+ const arithBudget = () => nodes.length * Math.ceil((nFacts + 64) / 32) * 4 <= FACT_BUDGET_BYTES / 2;
1737
+ const derivedFact = (field, lo, hi, at) => {
1738
+ if (lo != null && (lo > MAX_SAFE || lo < -MAX_SAFE)) lo = null;
1739
+ if (hi != null && (hi > MAX_SAFE || hi < -MAX_SAFE)) hi = null;
1740
+ if (lo == null && hi == null) return null;
1741
+ // Each end is a fact of its own, so that routes which reach it by different statements meet at the looser.
1742
+ if (lo != null && hi != null) { derivedFact(field, lo, null, at); derivedFact(field, null, hi, at); }
1743
+ const id = `${lo ?? ''}|${hi ?? ''}`;
1744
+ if (!derivedOf.has(field)) derivedOf.set(field, new Map());
1745
+ const mine = derivedOf.get(field);
1746
+ if (mine.has(id)) return mine.get(id);
1747
+ if (!arithBudget()) return null;
1748
+ const key = fieldKey(field);
1749
+ noteField(key, field);
1750
+ const bounds = { ...(lo != null ? { lo: Number(lo) } : {}), ...(hi != null ? { hi: Number(hi) } : {}) };
1751
+ const tCons = { numeric: true, ...(lo != null ? { lo: Number(lo), loInc: true } : {}), ...(hi != null ? { hi: Number(hi), hiInc: true } : {}) };
1752
+ const c = { id: checks.length, key, field, file: at.file, line: at.line, tCons, fCons: null, restricts: false, x: null, t: nFacts++, f: null, bounds, derived: true };
1753
+ checks.push(c);
1754
+ mine.set(id, c);
1755
+ return c;
1756
+ };
1757
+ const derivedSeen = new Set();
1758
+ const rules = [];
1759
+ // Whatever holds after `n` when the facts in `needs` hold before it; with none needed, whenever it runs.
1760
+ const derive = (n, needs, fact, bucket = null) => {
1761
+ const id = `${n.id}|${bucket}|${needs.join(',')}|${fact.t}`;
1762
+ if (derivedSeen.has(id)) return false;
1763
+ derivedSeen.add(id);
1764
+ rules.push({ n, needs, fact, bucket });
1765
+ return true;
1766
+ };
1767
+ // An interval implies every looser interval the model holds about its field, so a route that reached a
1768
+ // tighter one and a route that reached a looser one still meet at the looser. A check the program made
1769
+ // is not among them: the interval is what the data holds, not a test that ran.
1770
+ const withLooser = (fact) => {
1771
+ const out = [fact.t];
1772
+ const own = fact.bounds;
1773
+ for (const c of factsByKey.get(fact.key) || []) {
1774
+ if (c === fact || !c.derived) continue;
1775
+ for (const [bit, cons] of [[c.t, c.tCons], [c.f, c.fCons]]) {
1776
+ if (bit == null || !cons) continue;
1777
+ const e = wholeEnds(cons);
1778
+ if (!e) continue;
1779
+ if ((e.lo == null || (own.lo != null && BigInt(own.lo) >= e.lo)) && (e.hi == null || (own.hi != null && BigInt(own.hi) <= e.hi))) out.push(bit);
1780
+ }
1781
+ }
1782
+ return out;
1783
+ };
1784
+ const expandRules = () => {
1785
+ indexFacts();
1786
+ for (const n of nodes) { n.derive = undefined; n.genArith = undefined; }
1787
+ for (const r of rules) {
1788
+ const bits = withLooser(r.fact);
1789
+ const at = r.bucket == null ? r.n : nodes[r.n.test];
1790
+ if (!r.needs.length) {
1791
+ if (r.bucket == null) (at.genArith ||= []).push(...bits);
1792
+ else ((at.genArithOn ||= { true: [], false: [] })[r.bucket]).push(...bits);
1793
+ } else (r.n.derive ||= []).push({ needs: r.needs, to: bits });
1794
+ }
1795
+ };
1796
+
1797
+ const ARITH_VERBS = new Set(['COMPUTE', 'ADD', 'SUBTRACT', 'MULTIPLY', 'DIVIDE', 'MOVE']);
1798
+ const between = (seg, from, to) => seg.slice(from, to < 0 ? seg.length : to);
1799
+ const idxOf = (seg, word) => seg.findIndex((t) => WORD(t, word));
1800
+ // A statement as a tree per receiving field; null where its words are not ones the model reads.
1801
+ const readArith = (st) => {
1802
+ let seg = segOf(st);
1803
+ const cut = seg.findIndex((t) => t.t === 'word' && (t.u === 'ON' || t.u === 'NOT' || t.u === 'SIZE' || t.u === `END-${st.verb}`));
1804
+ if (cut >= 0) seg = seg.slice(0, cut);
1805
+ if (seg.some((t) => WORD(t, 'CORRESPONDING') || WORD(t, 'CORR') || WORD(t, 'REMAINDER'))) return null;
1806
+ const operands = (toks, count) => {
1807
+ const p = parser(toks, arithEnv);
1808
+ const out = [];
1809
+ while (!p.done()) out.push(p.primary());
1810
+ if (!out.length || (count && out.length !== count)) throw parser;
1811
+ return out;
1812
+ };
1813
+ const sum = (list) => list.reduce((a, b) => ({ k: 'bin', op: '+', a, b }));
1814
+ // Receiving names with ROUNDED; anything else, including a subscript, still writes but is not followed.
1815
+ const receivers = (toks) => {
1816
+ const out = [];
1817
+ for (let i = 0; i < toks.length; i++) {
1818
+ const t = toks[i];
1819
+ if (WORD(t, 'ROUNDED')) { if (!out.length) throw parser; out[out.length - 1].rounded = true; if (WORD(toks[i + 1], 'MODE')) i += WORD(toks[i + 2], 'IS') ? 3 : 2; continue; }
1820
+ if (t.t !== 'word') throw parser;
1821
+ const f = resolve(t);
1822
+ out.push({ tok: t, f: f && !f.index && f.level !== 88 ? f : null, rounded: false, sub: toks[i + 1] && toks[i + 1].v === '(' });
1823
+ if (toks[i + 1] && toks[i + 1].v === '(') { const close = closeIn(toks, i + 1); if (close < 0) throw parser; i = close; }
1824
+ }
1825
+ if (!out.length) throw parser;
1826
+ return out;
1827
+ };
1828
+ const closeIn = (toks, i) => { let d = 0; for (let j = i; j < toks.length; j++) { if (toks[j].v === '(') d++; else if (toks[j].v === ')' && --d === 0) return j; } return -1; };
1829
+ const own = (r) => arithEnv.field(r.tok, false);
1830
+ const per = (targets, make) => targets.map((r) => (r.sub || !r.f ? null : make(r)));
1831
+ return attempt(() => {
1832
+ let targets;
1833
+ let trees;
1834
+ switch (st.verb) {
1835
+ case 'COMPUTE': {
1836
+ const eq = seg.findIndex((t) => t.t === 'op' && t.v === '=');
1837
+ if (eq < 1) return null;
1838
+ targets = receivers(seg.slice(0, eq));
1839
+ const p = parser(seg.slice(eq + 1), arithEnv);
1840
+ const e = p.expr();
1841
+ if (!p.done()) return null;
1842
+ trees = per(targets, () => e);
1843
+ break;
1844
+ }
1845
+ case 'ADD': {
1846
+ const g = idxOf(seg, 'GIVING');
1847
+ if (g < 0) {
1848
+ const to = idxOf(seg, 'TO');
1849
+ if (to < 1) return null;
1850
+ const ops = operands(seg.slice(0, to));
1851
+ targets = receivers(seg.slice(to + 1));
1852
+ trees = per(targets, (r) => { const l = own(r); return l ? sum([...ops, l]) : null; });
1853
+ } else {
1854
+ const ops = operands(seg.slice(0, g).filter((t) => !WORD(t, 'TO')));
1855
+ targets = receivers(seg.slice(g + 1));
1856
+ trees = per(targets, () => sum(ops));
1857
+ }
1858
+ break;
1859
+ }
1860
+ case 'SUBTRACT': {
1861
+ const from = idxOf(seg, 'FROM');
1862
+ const g = idxOf(seg, 'GIVING');
1863
+ if (from < 1) return null;
1864
+ const subs = sum(operands(seg.slice(0, from)));
1865
+ if (g < 0) {
1866
+ targets = receivers(seg.slice(from + 1));
1867
+ trees = per(targets, (r) => { const l = own(r); return l ? { k: 'bin', op: '-', a: l, b: subs } : null; });
1868
+ } else {
1869
+ const [minuend] = operands(between(seg, from + 1, g), 1);
1870
+ targets = receivers(seg.slice(g + 1));
1871
+ trees = per(targets, () => ({ k: 'bin', op: '-', a: minuend, b: subs }));
1872
+ }
1873
+ break;
1874
+ }
1875
+ case 'MULTIPLY': {
1876
+ const by = idxOf(seg, 'BY');
1877
+ const g = idxOf(seg, 'GIVING');
1878
+ if (by < 1) return null;
1879
+ const [a] = operands(seg.slice(0, by), 1);
1880
+ if (g < 0) {
1881
+ targets = receivers(seg.slice(by + 1));
1882
+ trees = per(targets, (r) => { const l = own(r); return l ? { k: 'bin', op: '*', a: l, b: a } : null; });
1883
+ } else {
1884
+ const [b] = operands(between(seg, by + 1, g), 1);
1885
+ targets = receivers(seg.slice(g + 1));
1886
+ trees = per(targets, () => ({ k: 'bin', op: '*', a, b }));
1887
+ }
1888
+ break;
1889
+ }
1890
+ case 'DIVIDE': {
1891
+ const into = idxOf(seg, 'INTO');
1892
+ const by = idxOf(seg, 'BY');
1893
+ const g = idxOf(seg, 'GIVING');
1894
+ if (into > 0 && by < 0) {
1895
+ const [d] = operands(seg.slice(0, into), 1);
1896
+ if (g < 0) {
1897
+ targets = receivers(seg.slice(into + 1));
1898
+ trees = per(targets, (r) => { const l = own(r); return l ? { k: 'bin', op: '/', a: l, b: d } : null; });
1899
+ } else {
1900
+ const [n] = operands(between(seg, into + 1, g), 1);
1901
+ targets = receivers(seg.slice(g + 1));
1902
+ trees = per(targets, () => ({ k: 'bin', op: '/', a: n, b: d }));
1903
+ }
1904
+ } else if (by > 0 && into < 0 && g > by) {
1905
+ const [n] = operands(seg.slice(0, by), 1);
1906
+ const [d] = operands(between(seg, by + 1, g), 1);
1907
+ targets = receivers(seg.slice(g + 1));
1908
+ trees = per(targets, () => ({ k: 'bin', op: '/', a: n, b: d }));
1909
+ } else return null;
1910
+ break;
1911
+ }
1912
+ case 'MOVE': {
1913
+ const to = idxOf(seg, 'TO');
1914
+ if (to < 1) return null;
1915
+ const p = parser(seg.slice(0, to), arithEnv);
1916
+ const e = p.primary();
1917
+ if (!p.done() || (e.k !== 'field' && e.k !== 'range' && e.k !== 'mod')) return null;
1918
+ targets = receivers(seg.slice(to + 1));
1919
+ trees = per(targets, () => e);
1920
+ break;
1921
+ }
1922
+ default: return null;
1923
+ }
1924
+ // Several receivers taking turns: what one takes is no longer what the next reads.
1925
+ if (targets.length > 1) {
1926
+ const written = targets.map((r) => r.f).filter(Boolean);
1927
+ if (written.some((w, i) => written.some((v, j) => i !== j && overlaps(w, v)))) return null;
1928
+ for (const tree of trees) for (const l of tree ? leaves(tree) : []) if (written.some((w) => overlaps(w, l.f))) return null;
1929
+ }
1930
+ return { targets, trees };
1931
+ });
1932
+ };
1933
+
1934
+ const arith = [];
1935
+ try {
1936
+ for (const n of nodes) {
1937
+ if (n.kind !== 'stmt' || !n.st || n.st.at == null || !ARITH_VERBS.has(n.st.verb)) continue;
1938
+ const read = readArith(n.st);
1939
+ if (read && read.trees.some(Boolean)) arith.push({ n, ...read });
1940
+ }
1941
+ } catch {
1942
+ arith.length = 0;
1943
+ }
1944
+
1945
+ // A condition's requirements that hold whichever way the rest of it went: the relations it makes true.
1946
+ const relationsOf = (tree, outcome, out = []) => {
1947
+ switch (tree.k) {
1948
+ case 'not': return relationsOf(tree.a, !outcome, out);
1949
+ case 'and': if (outcome) { relationsOf(tree.a, true, out); relationsOf(tree.b, true, out); } return out;
1950
+ case 'or': if (!outcome) { relationsOf(tree.a, false, out); relationsOf(tree.b, false, out); } return out;
1951
+ case 'rel': out.push({ rel: tree, pol: outcome }); return out;
1952
+ default: return out;
1953
+ }
1954
+ };
1955
+ const ORDERING = new Set(['<', '<=', '>', '>=']);
1956
+ // Relations between two integer fields: the ordering tests whose sides are both plain fields.
1957
+ const relations = new Map();
1958
+ const collectRelations = (tree, test) => {
1959
+ if (tree.k === 'rel') {
1960
+ if (!ORDERING.has(tree.op) || relations.has(tree)) return;
1961
+ const f = fieldOf(tree.left);
1962
+ const g = fieldOf(tree.right);
1963
+ if (!f || !g || f === g || inTable(f) || inTable(g) || !rangeOfField(f) || !rangeOfField(g)) return;
1964
+ if (constValue(tree.left) != null || constValue(tree.right) != null) return;
1965
+ relations.set(tree, { f, g, op: tree.op, tokF: tree.left[0], tokG: tree.right[0], test, entries: { true: new Map(), false: new Map() } });
1966
+ } else for (const s of [tree.a, tree.b]) if (s) collectRelations(s, test);
1967
+ };
1968
+ try {
1969
+ for (const id of tests) { const t = nodes[id].tree; if (t) collectRelations(t, id); }
1970
+ } catch {
1971
+ relations.clear();
1972
+ }
1973
+
1974
+ // The fields that matter: those an index reads, and those that flow into them through the
1975
+ // statements above and the relations between fields.
1976
+ const relevant = new Set();
1977
+ for (const st of prog.statements) for (const x of st.indexes || []) { const it = x.tok && x.tok.t === 'word' ? resolve(x.tok) : null; if (it && !it.index) relevant.add(it); }
1978
+ for (const it of prog.items) {
1979
+ if (!it.dependingOn) continue;
1980
+ const c = resolve({ t: 'word', u: String(it.dependingOn).toUpperCase(), v: it.dependingOn });
1981
+ if (c && !c.index) relevant.add(c);
1982
+ }
1983
+ for (let more = relevant.size > 0; more;) {
1984
+ more = false;
1985
+ const before = relevant.size;
1986
+ for (const a of arith) if (a.targets.some((r) => r.f && relevant.has(r.f))) for (const tree of a.trees) for (const l of tree ? leaves(tree) : []) relevant.add(l.f);
1987
+ for (const r of relations.values()) if (relevant.has(r.f) || relevant.has(r.g)) { relevant.add(r.f); relevant.add(r.g); }
1988
+ more = relevant.size > before;
1989
+ }
1990
+
1991
+ // One pass over the statements and relations, reading each against the facts `prev` holds at it. True if it added anything.
1992
+ const arithRound = (prev) => {
1993
+ if (!relevant.size || !(arith.length || relations.size)) return false;
1994
+ // Each further pass runs the whole analysis again, so a program too large for that is left with what one pass gives.
1995
+ if (prev && nodes.length * Math.ceil(nFacts / 32) > ARITH_WORK) return false;
1996
+ let grew = false;
1997
+ try {
1998
+ indexFacts();
1999
+ for (const a of arith) {
2000
+ a.trees.forEach((tree, i) => {
2001
+ const r = a.targets[i];
2002
+ if (!tree || !r.f || !rangeOfField(r.f) || inTable(r.f) || !relevant.has(r.f)) return;
2003
+ const fit = rangeOfField(r.f).fit;
2004
+ heldBits = prev ? prev.get(a.n.id) || null : null;
2005
+ for (const c of candidates(tree, leafCands, 64)) {
2006
+ const s = storedRange(c.iv, r.rounded);
2007
+ if (s.lo < fit.lo || s.hi > fit.hi || (s.lo === fit.lo && s.hi === fit.hi)) continue;
2008
+ const fact = derivedFact(r.f, s.lo, s.hi, a.n.st);
2009
+ if (fact && derive(a.n, c.needs, fact)) grew = true;
2010
+ }
2011
+ });
2012
+ }
2013
+ for (const rel of relations.values()) {
2014
+ if (!relevant.has(rel.f) && !relevant.has(rel.g)) continue;
2015
+ const at = nodes[rel.test].at || nodes[rel.test].st;
2016
+ heldBits = prev ? prev.get(rel.test) || null : null;
2017
+ for (const pol of [true, false]) {
2018
+ const eff = pol ? rel.op : NEGATE[rel.op];
2019
+ for (const [tgt, src, tok, op] of [[rel.f, rel.g, rel.tokG, eff], [rel.g, rel.f, rel.tokF, FLIP[eff]]]) {
2020
+ const hold = rangeOfField(tgt).hold;
2021
+ for (const c of leafCands({ f: src, tok, plain: true })) {
2022
+ let lo = null;
2023
+ let hi = null;
2024
+ if (op === '<') hi = c.hi - 1n; else if (op === '<=') hi = c.hi; else if (op === '>') lo = c.lo + 1n; else lo = c.lo;
2025
+ if (lo != null && lo <= hold.lo) lo = null;
2026
+ if (hi != null && hi >= hold.hi) hi = null;
2027
+ if ((lo == null && hi == null) || (hi != null && hi < hold.lo) || (lo != null && lo > hold.hi)) continue;
2028
+ const fact = derivedFact(tgt, lo, hi, at);
2029
+ const id = fact && `${fact.t}|${c.needs}`;
2030
+ if (fact && !rel.entries[pol].has(id)) { rel.entries[pol].set(id, { needs: c.needs, fact }); grew = true; }
2031
+ }
2032
+ }
2033
+ }
2034
+ }
2035
+ // A relation holds after the outcome of its test that makes it true, and the fields it bounds hold
2036
+ // what the other side could hold.
2037
+ for (const id of tests) {
2038
+ const n = nodes[id];
2039
+ if (!n.tree) continue;
2040
+ for (const o of [true, false]) {
2041
+ const at = outcomeAt.get(`${id}|${o}`);
2042
+ if (!at) continue;
2043
+ for (const { rel, pol } of relationsOf(n.tree, o)) {
2044
+ const rec = relations.get(rel);
2045
+ if (rec) for (const e of rec.entries[pol].values()) if (derive(at, e.needs, e.fact, String(o))) grew = true;
2046
+ }
2047
+ }
2048
+ }
2049
+ } catch {
2050
+ // Whatever went wrong, the statements keep the facts they had.
2051
+ grew = false;
2052
+ }
2053
+ heldBits = null;
2054
+ return grew;
2055
+ };
2056
+ arithRound(null);
1077
2057
  // At least one fact, so the analysis runs, and a statement it never reaches has no facts at all.
1078
2058
  nFacts ||= 1;
1079
2059
  const extraFacts = new Map();
@@ -1090,7 +2070,14 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
1090
2070
  }
1091
2071
  }
1092
2072
  if (!nFacts) return { nodeOf, checks: [], words: 0, facts: new Map(), nodes: nodes.length, extraFacts };
1093
- const words = Math.ceil(nFacts / 32);
2073
+ let words = 0;
2074
+ let facts = new Map();
2075
+ let partial = false;
2076
+ // The analysis proper, run again whenever arithmetic has added facts.
2077
+ const analyse = () => {
2078
+ words = Math.ceil(nFacts / 32);
2079
+ for (const n of nodes) { n.kill = undefined; n.keeps = undefined; n.genS = undefined; n.genArithOn = undefined; }
2080
+ expandRules();
1094
2081
  // Every node holds a bitset of every fact, several times over, so the cost is their product. A
1095
2082
  // 426,000-line generated program passed an 8 GB heap here, and running out of heap cannot be
1096
2083
  // caught. Past the budget the program is left unordered, which callers already handle.
@@ -1100,19 +2087,9 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
1100
2087
  const factsOfKey = new Map();
1101
2088
  for (const c of checks) {
1102
2089
  const list = factsOfKey.get(c.key) || [];
1103
- for (const f of [c.x, c.t, c.f]) if (f != null) list.push(f);
2090
+ for (const f of [c.x, c.t, c.f, c.rt, c.rf]) if (f != null) list.push(f);
1104
2091
  factsOfKey.set(c.key, list);
1105
2092
  }
1106
- const overlaps = (w, f) => {
1107
- if (w === f) return true;
1108
- if (w.index || f.index) return w.index && f.index && w.index === f.index;
1109
- if (w.level === 66 || f.level === 66) return true;
1110
- if (itemsInRecord(w) !== itemsInRecord(f)) return false;
1111
- if (w.offset == null || f.offset == null || w.size == null || f.size == null) return true;
1112
- const wEnd = w.offset + (w.contributes || w.size);
1113
- const fEnd = f.offset + (f.contributes || f.size);
1114
- return w.offset < fEnd && f.offset < wEnd;
1115
- };
1116
2093
  const killCache = new Map();
1117
2094
  const killOf = (w) => {
1118
2095
  if (!w) return null;
@@ -1128,44 +2105,6 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
1128
2105
  killCache.set(w, bits);
1129
2106
  return bits;
1130
2107
  };
1131
- const writtenBy = (n) => {
1132
- const out = [];
1133
- const add = (tok) => { const f = tok ? resolve(tok) : null; if (f) out.push(f.level === 88 && f.parent ? f.parent : f); };
1134
- if (n.st) {
1135
- for (const t of n.st.targets || []) add(t);
1136
- if (n.st.verb === 'CALL' || n.st.verb === 'READ' || n.st.verb === 'RETURN') {
1137
- // A callee may write what it is passed by reference, and a READ fills the file's records.
1138
- for (const t of n.st.sources || []) {
1139
- const f = resolve(t);
1140
- if (!f) continue;
1141
- if (f.records) for (const r of f.records) out.push(r);
1142
- else if (n.st.verb === 'CALL') out.push(f);
1143
- }
1144
- }
1145
- }
1146
- if (n.e && n.e.kind === 'SQL') {
1147
- // A statement fills the host variables of its INTO list and reads the rest.
1148
- const toks = n.e.toks;
1149
- const into = toks.findIndex((t) => WORD(t, 'INTO'));
1150
- if (into >= 0) {
1151
- for (let i = into + 1; i < toks.length && !WORD(toks[i], 'FROM'); i++) {
1152
- if (toks[i].t === 'op' && toks[i].v === ':' && toks[i + 1]) add(toks[i + 1]);
1153
- }
1154
- }
1155
- } else if (n.e) {
1156
- // A command may fill what any option is given directly, except what it sends FROM. A name
1157
- // inside that argument's own parentheses - a subscript, a reference modification - is read.
1158
- let depth = 0;
1159
- let option = null;
1160
- for (let i = 0; i < n.e.toks.length; i++) {
1161
- const t = n.e.toks[i];
1162
- if (t.t === 'sep') { depth += t.v === '(' ? 1 : -1; continue; }
1163
- if (depth === 0) { option = t.t === 'word' ? t.u : option; continue; }
1164
- if (depth === 1 && t.t === 'word' && option !== 'FROM') add(t);
1165
- }
1166
- }
1167
- return out;
1168
- };
1169
2108
  for (const n of nodes) {
1170
2109
  if (n.kind !== 'stmt' && n.kind !== 'exec' && n.kind !== 'step' && n.kind !== 'call') continue;
1171
2110
  let bits = null;
@@ -1177,13 +2116,64 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
1177
2116
  }
1178
2117
  if (bits) n.kill = bits;
1179
2118
  }
2119
+ // The facts that say a field is bounded above from a value that leaves room for `max` more.
2120
+ const roomFacts = new Map();
2121
+ const roomFor = (field, max) => {
2122
+ let out = roomFacts.get(field)?.get(max);
2123
+ if (out) return out;
2124
+ out = [];
2125
+ const cap = intCap(field);
2126
+ for (const c of checks) {
2127
+ if (c.key !== field || c.mono || c.pseudo) continue;
2128
+ for (const [bit, cons] of [[c.t, c.tCons], [c.f, c.fCons]]) {
2129
+ const r = bit != null && cons ? asRange(cons) : null;
2130
+ if (!r || r.hi == null) continue;
2131
+ if ((r.hiInc ? Math.floor(r.hi) : Math.ceil(r.hi) - 1) + max <= cap) out.push(bit);
2132
+ }
2133
+ }
2134
+ if (!roomFacts.has(field)) roomFacts.set(field, new Map());
2135
+ roomFacts.get(field).set(max, out);
2136
+ return out;
2137
+ };
2138
+ // What a write does not kill: the lower bounds of a field it only raises, where nothing else it
2139
+ // writes shares the field's bytes.
2140
+ const keepAcross = (n, list) => {
2141
+ const wrote = writtenBy(n);
2142
+ for (const { field, max, sizeError } of list) {
2143
+ const mine = loOf.get(field);
2144
+ if (!mine || wrote.some((w) => w !== field && overlaps(w, field))) continue;
2145
+ const guards = sizeError ? null : roomFor(field, max);
2146
+ if (guards && !guards.length) continue;
2147
+ const bits = new Uint32Array(words);
2148
+ for (const c of mine) bits[c.t >>> 5] |= 1 << (c.t & 31);
2149
+ (n.keeps ||= []).push({ bits, guards });
2150
+ }
2151
+ };
2152
+ nodes.forEach((n, id) => {
2153
+ if (!n.kill) return;
2154
+ if (n.kind === 'stmt' && n.st && n.st.at != null) {
2155
+ const inc = increments(n.st);
2156
+ if (inc) keepAcross(n, inc.fields.map((field) => ({ field, max: inc.max, sizeError: inc.sizeError })));
2157
+ for (const { field, max, pairs } of lifted.get(id) || []) {
2158
+ const guards = roomFor(field, max);
2159
+ if (guards.length && !(n.st.targets || []).some((t) => { const o = resolve(t); return o && o !== field && overlaps(o, field); })) (n.lifts ||= []).push({ pairs, guards });
2160
+ }
2161
+ } else if (n.kind === 'step' && stepped.has(id)) {
2162
+ keepAcross(n, [{ ...stepped.get(id), sizeError: false }]);
2163
+ }
2164
+ if (n.kind === 'step' && keptOnStep.has(id)) {
2165
+ const bits = new Uint32Array(words);
2166
+ for (const b of keptOnStep.get(id)) bits[b >>> 5] |= 1 << (b & 31);
2167
+ (n.keeps ||= []).push({ bits, guards: null });
2168
+ }
2169
+ });
1180
2170
 
1181
2171
  // ---- must-analysis ----
1182
2172
  const ZERO = new Uint32Array(words);
1183
2173
  const ONES = new Uint32Array(words).fill(0xffffffff);
1184
2174
  const genBits = (list) => { const b = new Uint32Array(words); for (const x of list) b[x >>> 5] |= 1 << (x & 31); return b; };
1185
- for (const id of tests) { const n = nodes[id]; n.genT = genBits(n.gen.true); n.genF = genBits(n.gen.false); }
1186
- for (const n of nodes) if (n.genStmt) n.genS = genBits(n.genStmt);
2175
+ for (const id of tests) { const n = nodes[id]; n.genT = genBits([...n.gen.true, ...(n.genArithOn ? n.genArithOn.true : [])]); n.genF = genBits([...n.gen.false, ...(n.genArithOn ? n.genArithOn.false : [])]); }
2176
+ for (const n of nodes) if (n.genStmt || n.genArith) n.genS = genBits([...(n.genStmt || []), ...(n.genArith || [])]);
1187
2177
  const summaries = new Map();
1188
2178
  // Reverse postorder from every way in, so a forward analysis visits a node after what precedes it
1189
2179
  // and a join is revisited only when a loop brings something new.
@@ -1209,6 +2199,9 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
1209
2199
  for (let i = 0; i < post.length; i++) order[post[i]] = post.length - 1 - i;
1210
2200
  }
1211
2201
  const scratch = new Uint32Array(words);
2202
+ const killScratch = new Uint32Array(words);
2203
+ // A summary is joined to whatever enters the range, so it cannot assume a guard the caller lacks.
2204
+ let summarising = false;
1212
2205
  // What holds after a node, in a buffer reused by every call: the caller merges it into the next
1213
2206
  // node's facts before calling again. Null where the route ends here.
1214
2207
  const transfer = (n, input) => {
@@ -1226,17 +2219,46 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
1226
2219
  out = scratch;
1227
2220
  }
1228
2221
  if (n.kill) {
1229
- if (out === input) { for (let i = 0; i < words; i++) scratch[i] = input[i] & ~n.kill[i]; out = scratch; }
1230
- else for (let i = 0; i < words; i++) out[i] &= ~n.kill[i];
2222
+ let kill = n.kill;
2223
+ if (n.keeps) {
2224
+ kill = killScratch;
2225
+ kill.set(n.kill);
2226
+ for (const k of n.keeps) {
2227
+ if (k.guards && (summarising || !k.guards.some((g) => input[g >>> 5] & (1 << (g & 31))))) continue;
2228
+ for (let i = 0; i < words; i++) kill[i] &= ~k.bits[i];
2229
+ }
2230
+ }
2231
+ if (out === input) { for (let i = 0; i < words; i++) scratch[i] = input[i] & ~kill[i]; out = scratch; }
2232
+ else for (let i = 0; i < words; i++) out[i] &= ~kill[i];
1231
2233
  }
1232
2234
  // What the statement assigns holds after it, over whatever the write took away.
1233
2235
  if (n.genS) {
1234
2236
  if (out === input) { for (let i = 0; i < words; i++) scratch[i] = input[i] | n.genS[i]; out = scratch; }
1235
2237
  else for (let i = 0; i < words; i++) out[i] |= n.genS[i];
1236
2238
  }
2239
+ // An increment that cannot wrap raises the lower bounds that held before it.
2240
+ if (n.lifts) {
2241
+ for (const { pairs, guards } of n.lifts) {
2242
+ if (summarising || !guards.some((g) => input[g >>> 5] & (1 << (g & 31)))) continue;
2243
+ for (const [from, to] of pairs) {
2244
+ if (!(input[from >>> 5] & (1 << (from & 31)))) continue;
2245
+ if (out === input) { scratch.set(input); out = scratch; }
2246
+ out[to >>> 5] |= 1 << (to & 31);
2247
+ }
2248
+ }
2249
+ }
2250
+ // What a statement or a test leaves in a field, where the facts its operands needed held before it.
2251
+ if (n.derive && !summarising) {
2252
+ for (const d of n.derive) {
2253
+ if (!d.needs.every((x) => input[x >>> 5] & (1 << (x & 31)))) continue;
2254
+ if (out === input) { scratch.set(input); out = scratch; }
2255
+ for (const t of d.to) out[t >>> 5] |= 1 << (t & 31);
2256
+ }
2257
+ }
1237
2258
  // A flag test's outcome turns what held before it into the bound the flag stood for.
1238
2259
  if (n.implies) {
1239
- for (const [from, to] of n.implies) {
2260
+ for (const [from, to, soft] of n.implies) {
2261
+ if (soft && summarising) continue;
1240
2262
  if (!(input[from >>> 5] & (1 << (from & 31)))) continue;
1241
2263
  if (out === input) { scratch.set(input); out = scratch; }
1242
2264
  out[to >>> 5] |= 1 << (to & 31);
@@ -1310,6 +2332,7 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
1310
2332
  // a recursive PERFORM does anyway.
1311
2333
  const performed = [...ranges.values()].filter((r) => r.entry != null && r.end != null);
1312
2334
  const callers = new Map();
2335
+ summarising = true;
1313
2336
  const pending = [...performed].sort((a, b) => order[b.entry] - order[a.entry]);
1314
2337
  const queued = new Set(pending.map((r) => r.key));
1315
2338
  for (let steps = 0; pending.length && steps < performed.length * 20; steps++) {
@@ -1327,7 +2350,8 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
1327
2350
  for (const c of callers.get(r.key) || []) if (!queued.has(c.key)) { queued.add(c.key); pending.push(c); }
1328
2351
  }
1329
2352
  // Stopped at a step limit, a summary may say a range never returns when it does.
1330
- let partial = pending.length > 0;
2353
+ partial = pending.length > 0;
2354
+ summarising = false;
1331
2355
 
1332
2356
  // Contexts: the program's own entry and every extra one enter with nothing; a range enters with
1333
2357
  // what holds at every PERFORM of it that some context reaches. A context runs again only when what
@@ -1359,13 +2383,16 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
1359
2383
  }
1360
2384
  }
1361
2385
  if (work.length || unknownLabel) partial = true;
1362
- const facts = new Map();
2386
+ facts = new Map();
1363
2387
  for (const IN of results.values()) {
1364
2388
  for (const [id, bits] of IN) {
1365
2389
  const had = facts.get(id);
1366
2390
  if (had) meetInto(had, bits); else facts.set(id, bits.slice());
1367
2391
  }
1368
2392
  }
2393
+ };
2394
+ analyse();
2395
+ for (let round = 1; round < 12 && arithRound(facts); round++) { analyse(); }
1369
2396
 
1370
2397
  // What a run can reach, from the graph alone and with the contexts the facts use: code entered from
1371
2398
  // an entry runs on past the end of a paragraph, and a performed range stops at its end. A PERFORM
@@ -1423,6 +2450,7 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
1423
2450
  const guards = reads.get(x.tok);
1424
2451
  let said = null;
1425
2452
  let ran = null;
2453
+ const chains = guards || [];
1426
2454
  for (const g of guards || []) {
1427
2455
  let cons = new Map();
1428
2456
  const r = new Map();
@@ -1434,14 +2462,14 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
1434
2462
  const mine = [];
1435
2463
  for (const key of new Set([...said.keys(), ...ran.keys()])) {
1436
2464
  const c = { id: checks.length, key, field: ran.get(key) || (typeof key === 'string' ? { index: key.slice(3) } : key), file: (n.at || n.st).file, line: (n.at || n.st).line,
1437
- tCons: said.get(key) || null, fCons: null, restricts: ran.has(key), f: null };
2465
+ tCons: plainCons(said.get(key)), fCons: null, restricts: ran.has(key), f: null };
1438
2466
  c.x = c.restricts ? placeFacts++ : null;
1439
2467
  c.t = c.tCons ? placeFacts++ : null;
1440
2468
  checks.push(c);
1441
2469
  mine.push(c);
1442
2470
  }
1443
2471
  posOf.set(x.tok, nodes.length + places.length);
1444
- places.push({ test: id, checks: mine });
2472
+ places.push({ test: id, checks: mine, chains });
1445
2473
  }
1446
2474
  }
1447
2475
  const reachedAt = new Uint8Array(nodes.length + places.length);
@@ -1453,6 +2481,19 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
1453
2481
  const bits = new Uint32Array(Math.max(words, Math.ceil(placeFacts / 32)));
1454
2482
  bits.set(before);
1455
2483
  for (const c of p.checks) for (const x of [c.x, c.t]) if (x != null) bits[x >>> 5] |= 1 << (x & 31);
2484
+ // What a relation before the name says of its fields, if every reading of the name is behind one.
2485
+ let related = null;
2486
+ for (const chain of p.chains) {
2487
+ const got = new Set();
2488
+ for (const [sub, outcome] of chain) {
2489
+ for (const { rel, pol } of relationsOf(sub, outcome)) {
2490
+ const rec = relations.get(rel);
2491
+ if (rec) for (const e of rec.entries[pol].values()) if (e.needs.every((x) => before[x >>> 5] & (1 << (x & 31)))) for (const x of withLooser(e.fact)) got.add(x);
2492
+ }
2493
+ }
2494
+ related = related ? new Set([...related].filter((x) => got.has(x))) : got;
2495
+ }
2496
+ for (const x of related || []) bits[x >>> 5] |= 1 << (x & 31);
1456
2497
  facts.set(nodes.length + i, bits);
1457
2498
  });
1458
2499
 
@@ -1460,7 +2501,7 @@ export function buildControl(prog, resolve, { extra = null } = {}) {
1460
2501
  // the caller has let the parse tree go. A statement no context reaches has no facts, and credits
1461
2502
  // nothing. One `reached` does not mark does not run, unless `partial` says the analysis stopped
1462
2503
  // short or met a paragraph the parse did not find.
1463
- return { nodeOf, posOf, checks: checks.filter((c) => !c.pseudo), words, nodes: nodes.length, facts, reached: reachedAt, extraFacts, partial };
2504
+ return { loopBody, nodeOf, posOf, checks: checks.filter((c) => !c.pseudo), words, nodes: nodes.length, facts, reached: reachedAt, extraFacts, partial };
1464
2505
  }
1465
2506
 
1466
2507
  // For each token a condition reads, what each reading of it waits on: the operands to its left at
@@ -1489,27 +2530,64 @@ export const hasFact = (bits, x) => x != null && bits != null && (bits[x >>> 5]
1489
2530
  // named is the one that decided it: one whose outcome alone makes the value safe, if there is one.
1490
2531
  // A bound the program's data sets rather than a check it made counts only where it keeps the index
1491
2532
  // between 1 and the sink's `limit`.
1492
- export function creditOf(checks, bits, sinkKind, limit = null) {
2533
+ // `offset` is the constant the sink adds to the index it names, T(I - 1): the outcomes are judged
2534
+ // shifted by it, and a shifted top is held to the limit.
2535
+ export function creditOf(checks, bits, sinkKind, limit = null, offset = 0, nonNeg = false) {
1493
2536
  let tested = null;
1494
2537
  let cons = null;
1495
2538
  let consBy = null;
1496
2539
  let alone = null;
2540
+ // An index a check tested is credited only where one outcome of it holds: where both reach the
2541
+ // use, the route on which the check failed reaches it too. An outcome that says something only
2542
+ // through arithmetic on other fields is not one that decided the index.
2543
+ const decided = (c) => !INDEX_SINKS.has(sinkKind) || (hasFact(bits, c.t) && !c.tCons.viaExpr) || (hasFact(bits, c.f) && !c.fCons.viaExpr);
1497
2544
  for (const c of checks) {
1498
- if (c.bounds && !(limit != null && c.bounds.lo >= 1 && c.bounds.hi <= limit)) continue;
1499
- if (hasFact(bits, c.x) && !tested) tested = c;
2545
+ if (c.bounds && !(limit != null && (c.bounds.lo == null ? c.bounds.hi != null : c.bounds.lo + offset >= 1) && (c.bounds.hi == null || c.bounds.hi + offset <= limit))) continue;
2546
+ if (hasFact(bits, c.x) && !tested && decided(c)) tested = c;
1500
2547
  for (const [bit, said] of [[c.t, c.tCons], [c.f, c.fCons]]) {
1501
2548
  if (!hasFact(bits, bit)) continue;
1502
2549
  cons = meet(cons, said);
1503
- consBy ||= c;
2550
+ // A lower bound the program's data sets is shared by every place that sets it, so a test names the guard.
2551
+ if (!consBy || (consBy.mono && !c.mono)) consBy = c;
1504
2552
  if (!alone && stops(said, sinkKind)) alone = c;
1505
2553
  }
1506
2554
  }
1507
2555
  // A check whose outcome puts the index past either end is the use it should have prevented.
1508
- if (limit != null && outside(cons, limit)) return { level: 0, check: null };
1509
- if (cons && stops(cons, sinkKind)) return { level: 2, check: alone || consBy };
2556
+ const shifted = shift(cons, offset);
2557
+ // An unsigned index moved up by a constant starts at that constant, whatever the checks said of its bottom.
2558
+ if (shifted && offset > 0 && nonNeg && shifted.lo == null && !shifted.set) { shifted.lo = offset; shifted.loInc = true; }
2559
+ if (limit != null && outside(shifted, limit)) return { level: 0, check: null };
2560
+ // A bound above the table's or the field's size lets the index past its end, however the check was written.
2561
+ const pastEnd = !!shifted && limit != null && (sinkKind === 'subscript' || sinkKind === 'reference-modification') && overTop(shifted, limit);
2562
+ if (shifted && stops(shifted, sinkKind) && !pastEnd) return { level: 2, check: alone || consBy, cons: shifted };
1510
2563
  return tested ? { level: 1, check: tested } : { level: 0, check: null };
1511
2564
  }
1512
2565
 
1513
- const outside = (cons, limit) => !!cons && (
1514
- (cons.lo != null && (cons.lo > limit || (cons.lo === limit && !cons.loInc)))
1515
- || (cons.hi != null && (cons.hi < 1 || (cons.hi === 1 && !cons.hiInc))));
2566
+ // Whether a constraint keeps an integer at or below `limit`, and at 1 or more where `fromOne`.
2567
+ export function within(said, limit, fromOne = true) {
2568
+ if (!said) return false;
2569
+ const cons = asRange(said);
2570
+ return (!fromOne || atLeastOne(cons)) && cons.hi != null && (cons.hiInc ? cons.hi <= limit : cons.hi <= limit + 1);
2571
+ }
2572
+
2573
+
2574
+ // A constraint moved by a constant: the values it allows, each plus `by`.
2575
+ function shift(said, by) {
2576
+ if (!said) return said;
2577
+ const out = { ...said };
2578
+ if (out.set) out.set = new Set([...out.set].map((v) => (/^[+-]?\d+(\.\d+)?$/.test(v) ? String(Number(v) + by) : v)));
2579
+ if (out.lo != null) out.lo += by;
2580
+ if (out.hi != null) out.hi += by;
2581
+ return out;
2582
+ }
2583
+ const overTop = (said, limit) => {
2584
+ const cons = asRange(said);
2585
+ return cons.hi != null && (cons.hi > limit + 1 || (cons.hi === limit + 1 && cons.hiInc));
2586
+ };
2587
+
2588
+ const outside = (said, limit) => {
2589
+ if (!said) return false;
2590
+ const cons = asRange(said);
2591
+ return (cons.lo != null && (cons.lo > limit || (cons.lo === limit && !cons.loInc)))
2592
+ || (cons.hi != null && (cons.hi < 1 || (cons.hi === 1 && !cons.hiInc)));
2593
+ };