plan-verify 0.0.0-stage → 0.1.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.
Files changed (222) hide show
  1. package/.github/workflows/ci.yml +48 -0
  2. package/README.md +1431 -2
  3. package/assets/icon.png +0 -0
  4. package/assets/logo-mark.png +0 -0
  5. package/assets/logo.png +0 -0
  6. package/examples/checkout.ears.md +74 -0
  7. package/examples/order-always.ears.md +3 -0
  8. package/examples/order-awkward.ears.md +8 -0
  9. package/examples/order-bad.json +1 -0
  10. package/examples/order-chain.ears.md +9 -0
  11. package/examples/order-chosen.ears.md +7 -0
  12. package/examples/order-clear.ears.md +8 -0
  13. package/examples/order-combined.ears.md +10 -0
  14. package/examples/order-conflict.ears.md +8 -0
  15. package/examples/order-covered.ears.md +9 -0
  16. package/examples/order-duplicate.ears.md +8 -0
  17. package/examples/order-fail.json +5 -0
  18. package/examples/order-misuse.ears.md +7 -0
  19. package/examples/order-negation.ears.md +7 -0
  20. package/examples/order-pass.json +5 -0
  21. package/examples/order-quantity.ears.md +8 -0
  22. package/examples/order-redundant.ears.md +9 -0
  23. package/examples/order-repair.ears.md +9 -0
  24. package/examples/order-repeat.ears.md +13 -0
  25. package/examples/order-same.ears.md +3 -0
  26. package/examples/order-status.ears.md +7 -0
  27. package/examples/order-unanswered.ears.md +8 -0
  28. package/examples/order-unhandled.ears.md +8 -0
  29. package/examples/order-unmeasured.ears.md +10 -0
  30. package/examples/order-unused.ears.md +13 -0
  31. package/examples/order-vacuous.ears.md +9 -0
  32. package/examples/order-vague.ears.md +7 -0
  33. package/examples/order-wide.ears.md +5 -0
  34. package/examples/order-wording.ears.md +8 -0
  35. package/examples/order.ears.md +7 -0
  36. package/examples/payment-if.ears.md +7 -0
  37. package/examples/plan-to-problems/README.md +54 -0
  38. package/examples/plan-to-problems/expected-problems.md +25 -0
  39. package/examples/plan-to-problems/generate.mjs +186 -0
  40. package/examples/plan-to-problems/generated.ears.md +27 -0
  41. package/examples/plan-to-problems/lint.json +576 -0
  42. package/examples/plan-to-problems/plan.md +5 -0
  43. package/examples/plan-to-problems/planted.txt +5 -0
  44. package/examples/plan-to-problems/problems.md +112 -0
  45. package/examples/status-placed.ears.md +3 -0
  46. package/package.json +25 -4
  47. package/scripts/install-scriptc.sh +16 -0
  48. package/skills/plan-verify/SKILL.md +1515 -0
  49. package/src/check.ts +250 -0
  50. package/src/decide.ts +1324 -0
  51. package/src/ears.ts +1520 -0
  52. package/src/jsonvalue.ts +228 -0
  53. package/src/lint.ts +1245 -0
  54. package/src/main.ts +273 -0
  55. package/src/policy.ts +148 -0
  56. package/src/repair.ts +567 -0
  57. package/src/report.ts +329 -0
  58. package/src/scenarios.ts +149 -0
  59. package/src/smt.ts +97 -0
  60. package/src/solver.ts +868 -0
  61. package/src/spec.ts +307 -0
  62. package/src/term.ts +331 -0
  63. package/src/text.ts +95 -0
  64. package/test/fixtures/malformed.ears.md +3 -0
  65. package/test/fixtures/unknown-phrase.ears.md +5 -0
  66. package/test/golden/check-fail-json.exit +1 -0
  67. package/test/golden/check-fail-json.stderr +0 -0
  68. package/test/golden/check-fail-json.stdout +13 -0
  69. package/test/golden/check-fail.exit +1 -0
  70. package/test/golden/check-fail.stderr +0 -0
  71. package/test/golden/check-fail.stdout +7 -0
  72. package/test/golden/check-pass-json.exit +1 -0
  73. package/test/golden/check-pass-json.stderr +0 -0
  74. package/test/golden/check-pass-json.stdout +6 -0
  75. package/test/golden/check-pass.exit +1 -0
  76. package/test/golden/check-pass.stderr +0 -0
  77. package/test/golden/check-pass.stdout +3 -0
  78. package/test/golden/emit-smt-json-usage.exit +1 -0
  79. package/test/golden/emit-smt-json-usage.stderr +0 -0
  80. package/test/golden/emit-smt-json-usage.stdout +4 -0
  81. package/test/golden/emit-smt-json.exit +1 -0
  82. package/test/golden/emit-smt-json.stderr +0 -0
  83. package/test/golden/emit-smt-json.stdout +5 -0
  84. package/test/golden/emit-smt.exit +1 -0
  85. package/test/golden/emit-smt.stderr +0 -0
  86. package/test/golden/emit-smt.stdout +10 -0
  87. package/test/golden/help.exit +1 -0
  88. package/test/golden/help.stderr +0 -0
  89. package/test/golden/help.stdout +8 -0
  90. package/test/golden/lint-awkward.exit +1 -0
  91. package/test/golden/lint-awkward.stderr +0 -0
  92. package/test/golden/lint-awkward.stdout +29 -0
  93. package/test/golden/lint-chain.exit +1 -0
  94. package/test/golden/lint-chain.stderr +0 -0
  95. package/test/golden/lint-chain.stdout +56 -0
  96. package/test/golden/lint-checkout.exit +1 -0
  97. package/test/golden/lint-checkout.stderr +0 -0
  98. package/test/golden/lint-checkout.stdout +301 -0
  99. package/test/golden/lint-clean.exit +1 -0
  100. package/test/golden/lint-clean.stderr +0 -0
  101. package/test/golden/lint-clean.stdout +9 -0
  102. package/test/golden/lint-clear.exit +1 -0
  103. package/test/golden/lint-clear.stderr +0 -0
  104. package/test/golden/lint-clear.stdout +9 -0
  105. package/test/golden/lint-conflict-json.exit +1 -0
  106. package/test/golden/lint-conflict-json.stderr +0 -0
  107. package/test/golden/lint-conflict-json.stdout +80 -0
  108. package/test/golden/lint-conflict.exit +1 -0
  109. package/test/golden/lint-conflict.stderr +0 -0
  110. package/test/golden/lint-conflict.stdout +41 -0
  111. package/test/golden/lint-duplicate.exit +1 -0
  112. package/test/golden/lint-duplicate.stderr +0 -0
  113. package/test/golden/lint-duplicate.stdout +14 -0
  114. package/test/golden/lint-gap.exit +1 -0
  115. package/test/golden/lint-gap.stderr +0 -0
  116. package/test/golden/lint-gap.stdout +12 -0
  117. package/test/golden/lint-misuse.exit +1 -0
  118. package/test/golden/lint-misuse.stderr +0 -0
  119. package/test/golden/lint-misuse.stdout +18 -0
  120. package/test/golden/lint-negation.exit +1 -0
  121. package/test/golden/lint-negation.stderr +0 -0
  122. package/test/golden/lint-negation.stdout +13 -0
  123. package/test/golden/lint-quantity.exit +1 -0
  124. package/test/golden/lint-quantity.stderr +0 -0
  125. package/test/golden/lint-quantity.stdout +41 -0
  126. package/test/golden/lint-redundant.exit +1 -0
  127. package/test/golden/lint-redundant.stderr +0 -0
  128. package/test/golden/lint-redundant.stdout +14 -0
  129. package/test/golden/lint-repair-json.exit +1 -0
  130. package/test/golden/lint-repair-json.stderr +0 -0
  131. package/test/golden/lint-repair-json.stdout +127 -0
  132. package/test/golden/lint-repair.exit +1 -0
  133. package/test/golden/lint-repair.stderr +0 -0
  134. package/test/golden/lint-repair.stdout +54 -0
  135. package/test/golden/lint-repeat.exit +1 -0
  136. package/test/golden/lint-repeat.stderr +0 -0
  137. package/test/golden/lint-repeat.stdout +48 -0
  138. package/test/golden/lint-status.exit +1 -0
  139. package/test/golden/lint-status.stderr +0 -0
  140. package/test/golden/lint-status.stdout +44 -0
  141. package/test/golden/lint-unanswered.exit +1 -0
  142. package/test/golden/lint-unanswered.stderr +0 -0
  143. package/test/golden/lint-unanswered.stdout +12 -0
  144. package/test/golden/lint-unhandled.exit +1 -0
  145. package/test/golden/lint-unhandled.stderr +0 -0
  146. package/test/golden/lint-unhandled.stdout +12 -0
  147. package/test/golden/lint-unmeasured.exit +1 -0
  148. package/test/golden/lint-unmeasured.stderr +0 -0
  149. package/test/golden/lint-unmeasured.stdout +13 -0
  150. package/test/golden/lint-unused-json.exit +1 -0
  151. package/test/golden/lint-unused-json.stderr +0 -0
  152. package/test/golden/lint-unused-json.stdout +37 -0
  153. package/test/golden/lint-unused.exit +1 -0
  154. package/test/golden/lint-unused.stderr +0 -0
  155. package/test/golden/lint-unused.stdout +25 -0
  156. package/test/golden/lint-vacuous-json.exit +1 -0
  157. package/test/golden/lint-vacuous-json.stderr +0 -0
  158. package/test/golden/lint-vacuous-json.stdout +27 -0
  159. package/test/golden/lint-vacuous.exit +1 -0
  160. package/test/golden/lint-vacuous.stderr +0 -0
  161. package/test/golden/lint-vacuous.stdout +19 -0
  162. package/test/golden/lint-vague.exit +1 -0
  163. package/test/golden/lint-vague.stderr +0 -0
  164. package/test/golden/lint-vague.stdout +21 -0
  165. package/test/golden/lint-wide.exit +1 -0
  166. package/test/golden/lint-wide.stderr +0 -0
  167. package/test/golden/lint-wide.stdout +9 -0
  168. package/test/golden/lint-wording.exit +1 -0
  169. package/test/golden/lint-wording.stderr +0 -0
  170. package/test/golden/lint-wording.stdout +21 -0
  171. package/test/golden/malformed-scenario.exit +1 -0
  172. package/test/golden/malformed-scenario.stderr +1 -0
  173. package/test/golden/malformed-scenario.stdout +0 -0
  174. package/test/golden/malformed-spec.exit +1 -0
  175. package/test/golden/malformed-spec.stderr +1 -0
  176. package/test/golden/malformed-spec.stdout +0 -0
  177. package/test/golden/policy-counterexample.exit +1 -0
  178. package/test/golden/policy-counterexample.stderr +0 -0
  179. package/test/golden/policy-counterexample.stdout +13 -0
  180. package/test/golden/policy-covered.exit +1 -0
  181. package/test/golden/policy-covered.stderr +0 -0
  182. package/test/golden/policy-covered.stdout +19 -0
  183. package/test/golden/policy-holds.exit +1 -0
  184. package/test/golden/policy-holds.stderr +0 -0
  185. package/test/golden/policy-holds.stdout +3 -0
  186. package/test/golden/policy-json.exit +1 -0
  187. package/test/golden/policy-json.stderr +0 -0
  188. package/test/golden/policy-json.stdout +34 -0
  189. package/test/golden/policy-status.exit +1 -0
  190. package/test/golden/policy-status.stderr +0 -0
  191. package/test/golden/policy-status.stdout +16 -0
  192. package/test/golden/policy-vacuous.exit +1 -0
  193. package/test/golden/policy-vacuous.stderr +0 -0
  194. package/test/golden/policy-vacuous.stdout +3 -0
  195. package/test/golden/policy-wide.exit +1 -0
  196. package/test/golden/policy-wide.stderr +0 -0
  197. package/test/golden/policy-wide.stdout +15 -0
  198. package/test/golden/policy.exit +1 -0
  199. package/test/golden/policy.stderr +0 -0
  200. package/test/golden/policy.stdout +19 -0
  201. package/test/golden/scenarios-json.exit +1 -0
  202. package/test/golden/scenarios-json.stderr +0 -0
  203. package/test/golden/scenarios-json.stdout +75 -0
  204. package/test/golden/scenarios-status.exit +1 -0
  205. package/test/golden/scenarios-status.stderr +0 -0
  206. package/test/golden/scenarios-status.stdout +24 -0
  207. package/test/golden/scenarios.exit +1 -0
  208. package/test/golden/scenarios.stderr +0 -0
  209. package/test/golden/scenarios.stdout +26 -0
  210. package/test/golden/unit.exit +1 -0
  211. package/test/golden/unit.stderr +0 -0
  212. package/test/golden/unit.stdout +2 -0
  213. package/test/golden/unknown-phrase.exit +1 -0
  214. package/test/golden/unknown-phrase.stderr +1 -0
  215. package/test/golden/unknown-phrase.stdout +0 -0
  216. package/test/golden/usage.exit +1 -0
  217. package/test/golden/usage.stderr +9 -0
  218. package/test/golden/usage.stdout +0 -0
  219. package/test/run-all.ts +499 -0
  220. package/test/soundness.ts +375 -0
  221. package/test/unit.ts +1352 -0
  222. package/tsconfig.json +15 -0
package/src/spec.ts ADDED
@@ -0,0 +1,307 @@
1
+ /**
2
+ * Declared vocabulary of an EARS spec.
3
+ * Events and states are booleans. Inputs and outputs are Bool, Int, or enum.
4
+ * Every Int has an inclusive range so the solver's domain is finite.
5
+ */
6
+
7
+ import { formatInt, replaceWord } from "./text.ts";
8
+
9
+ export type Role = "event" | "state" | "input" | "output";
10
+
11
+ export type Sort =
12
+ | { kind: "bool" }
13
+ | { kind: "int"; min: number; max: number }
14
+ | { kind: "enum"; enumName: string };
15
+
16
+ export interface EnumDecl {
17
+ name: string;
18
+ variants: string[];
19
+ line: number;
20
+ }
21
+
22
+ export interface SymbolDecl {
23
+ role: Role;
24
+ name: string;
25
+ sort: Sort;
26
+ description: string;
27
+ /** Plain words that stand for this symbol in a requirement. Empty means the name is used. */
28
+ phrase: string;
29
+ /** How a report states this Bool when it is true. Empty means the phrase is used. */
30
+ outcomeYes: string;
31
+ /** How a report states this Bool when it is false. Empty means it is derived. */
32
+ outcomeNo: string;
33
+ /** Verb words that also name this Bool, such as "start the subscription". */
34
+ action: string;
35
+ /** Report template for an Int or enum. `{value}` stands for the number or variant. */
36
+ says: string;
37
+ /** Requirement pattern for an Int or enum. `{value}` and `{op}` are holes. */
38
+ asText: string;
39
+ unwanted: boolean;
40
+ line: number;
41
+ source: string;
42
+ }
43
+
44
+ export interface Requirement {
45
+ id: string;
46
+ line: number;
47
+ sentence: string;
48
+ system: string;
49
+ antecedent: Term | null;
50
+ consequent: Term;
51
+ hasWhen: boolean;
52
+ hasIf: boolean;
53
+ whenTerm: Term | null;
54
+ }
55
+
56
+ export interface Spec {
57
+ enums: EnumDecl[];
58
+ symbols: SymbolDecl[];
59
+ requirements: Requirement[];
60
+ }
61
+
62
+ /**
63
+ * Predicate terms. Sorts are checked when the spec is parsed, so a term
64
+ * that reaches the solver is boolean, integer, or an enum value of one type.
65
+ */
66
+ export type Term =
67
+ | { kind: "bool"; value: boolean }
68
+ | { kind: "int"; value: number }
69
+ | { kind: "enum"; enumName: string; variant: string; index: number }
70
+ | { kind: "sym"; name: string; index: number; phrase: string }
71
+ | { kind: "not"; term: Term }
72
+ | { kind: "and"; left: Term; right: Term }
73
+ | { kind: "or"; left: Term; right: Term }
74
+ | { kind: "eq"; left: Term; right: Term }
75
+ | { kind: "neq"; left: Term; right: Term }
76
+ | { kind: "lt"; left: Term; right: Term }
77
+ | { kind: "le"; left: Term; right: Term }
78
+ | { kind: "gt"; left: Term; right: Term }
79
+ | { kind: "ge"; left: Term; right: Term }
80
+ | { kind: "add"; left: Term; right: Term }
81
+ | { kind: "sub"; left: Term; right: Term };
82
+
83
+ export function findSymbol(spec: Spec, name: string): number {
84
+ for (let i = 0; i < spec.symbols.length; i++) {
85
+ if (spec.symbols[i].name === name) return i;
86
+ }
87
+ return -1;
88
+ }
89
+
90
+ export function findEnum(spec: Spec, name: string): EnumDecl | null {
91
+ for (let i = 0; i < spec.enums.length; i++) {
92
+ if (spec.enums[i].name === name) return spec.enums[i];
93
+ }
94
+ return null;
95
+ }
96
+
97
+ export function findVariant(spec: Spec, variant: string): { enumName: string; index: number } | null {
98
+ for (let i = 0; i < spec.enums.length; i++) {
99
+ const decl = spec.enums[i];
100
+ for (let v = 0; v < decl.variants.length; v++) {
101
+ if (decl.variants[v] === variant) return { enumName: decl.name, index: v };
102
+ }
103
+ }
104
+ return null;
105
+ }
106
+
107
+ export function isDriver(sym: SymbolDecl): boolean {
108
+ return sym.role !== "output";
109
+ }
110
+
111
+ export function domainSize(spec: Spec, sym: SymbolDecl): number {
112
+ if (sym.sort.kind === "bool") return 2;
113
+ if (sym.sort.kind === "int") return sym.sort.max - sym.sort.min + 1;
114
+ const decl = findEnum(spec, sym.sort.enumName);
115
+ if (decl === null) return 0;
116
+ return decl.variants.length;
117
+ }
118
+
119
+ export function codeAt(sym: SymbolDecl, domainIndex: number): number {
120
+ if (sym.sort.kind === "int") return sym.sort.min + domainIndex;
121
+ return domainIndex;
122
+ }
123
+
124
+ export function displayCode(spec: Spec, sym: SymbolDecl, code: number): string {
125
+ if (sym.sort.kind === "bool") return code === 1 ? "true" : "false";
126
+ if (sym.sort.kind === "int") return String(Math.trunc(code));
127
+ const decl = findEnum(spec, sym.sort.enumName);
128
+ if (decl === null) return "?";
129
+ if (code < 0 || code >= decl.variants.length) return "?";
130
+ return decl.variants[code];
131
+ }
132
+
133
+ /**
134
+ * How a report names one assigned symbol.
135
+ * A Bool uses its outcome when one is declared. Otherwise a phrase that
136
+ * contains "is" is stated with "was" or "was not", and any other phrase
137
+ * stays as written, with "not" in front when the Bool is false.
138
+ * An integer or enum uses `says` when that template is declared.
139
+ * Otherwise a singular integer reads "the quantity was 12", and a plural
140
+ * or an enum reads "were set to" or "was set to".
141
+ */
142
+ export function describeAssignment(sym: SymbolDecl, shown: string, isTrue: boolean): string {
143
+ if (sym.sort.kind === "bool") return reportBool(sym, isTrue);
144
+ return speakBound(sym, shown, "eq", false);
145
+ }
146
+
147
+ /**
148
+ * How a report states an integer or enum comparison.
149
+ * `linked` means the right side is another phrase, so a numeric `says`
150
+ * template is left unused. `rel` is eq, neq, lt, le, gt, or ge.
151
+ */
152
+ export function speakBound(sym: SymbolDecl, shown: string, rel: string, linked: boolean): string {
153
+ if (!linked && sym.says.length > 0) return speakSays(sym, shown, rel);
154
+ const name = sym.phrase.length > 0 ? sym.phrase : sym.name;
155
+ const be = copulaOf(name);
156
+ if (linked) return name + " " + be + " " + linkedTail(rel, shown);
157
+ if (rel === "eq") {
158
+ if (sym.sort.kind === "enum" || pluralNoun(name)) return name + " " + be + " set to " + shown;
159
+ return name + " " + be + " " + shown;
160
+ }
161
+ if (rel === "neq") {
162
+ if (sym.sort.kind === "enum" || pluralNoun(name)) return name + " " + be + " not set to " + shown;
163
+ return name + " " + be + " not " + shown;
164
+ }
165
+ return name + " " + be + " " + relationTail(rel, shown);
166
+ }
167
+
168
+ /** The number or variant on the right of a comparison, or empty when it is not a literal. */
169
+ export function literalShown(term: Term): string {
170
+ if (term.kind === "int") return formatInt(term.value);
171
+ if (term.kind === "enum") return term.variant;
172
+ return "";
173
+ }
174
+
175
+ /** How a report states a Bool. */
176
+ export function reportBool(sym: SymbolDecl, isTrue: boolean): string {
177
+ if (isTrue && sym.outcomeYes.length > 0) return sym.outcomeYes;
178
+ if (!isTrue && sym.outcomeNo.length > 0) return sym.outcomeNo;
179
+ if (!isTrue && sym.outcomeYes.length > 0) return negateReport(sym.outcomeYes);
180
+ if (sym.phrase.length === 0) return sym.name + " is " + (isTrue ? "true" : "false");
181
+ if (hasWord(sym.phrase, "is")) {
182
+ if (isTrue) return replaceWord(sym.phrase, "is", "was");
183
+ return replaceWord(sym.phrase, "is", "was not");
184
+ }
185
+ if (isTrue) return sym.phrase;
186
+ return "not " + sym.phrase;
187
+ }
188
+
189
+ /** Insert "not" after the first "was", "were", or "is", or prefix "not" when none is there. */
190
+ export function negateReport(text: string): string {
191
+ const afterWas = insertNotAfter(text, "was");
192
+ if (afterWas.length > 0) return afterWas;
193
+ const afterWere = insertNotAfter(text, "were");
194
+ if (afterWere.length > 0) return afterWere;
195
+ const afterIs = insertNotAfter(text, "is");
196
+ if (afterIs.length > 0) return afterIs;
197
+ return "not " + text;
198
+ }
199
+
200
+ function speakSays(sym: SymbolDecl, shown: string, rel: string): string {
201
+ const filled = fillTemplate(sym.says, shown, sym.name);
202
+ if (rel === "eq") return filled;
203
+ if (rel === "neq") return negateReport(filled);
204
+ if (rel === "le") return "at most " + filled;
205
+ if (rel === "ge") return "at least " + filled;
206
+ if (rel === "lt") return "less than " + filled;
207
+ if (rel === "gt") return "greater than " + filled;
208
+ return filled;
209
+ }
210
+
211
+ function fillTemplate(template: string, shown: string, symbolName: string): string {
212
+ let text = replaceToken(template, "{value}", shown);
213
+ text = replaceToken(text, "{name}", shown);
214
+ text = replaceToken(text, "{" + symbolName + "}", shown);
215
+ return text;
216
+ }
217
+
218
+ function replaceToken(text: string, token: string, shown: string): string {
219
+ let out = "";
220
+ let i = 0;
221
+ while (i < text.length) {
222
+ if (text.slice(i, i + token.length) === token) {
223
+ out += shown;
224
+ i += token.length;
225
+ continue;
226
+ }
227
+ out += text.slice(i, i + 1);
228
+ i++;
229
+ }
230
+ return out;
231
+ }
232
+
233
+ function linkedTail(rel: string, shown: string): string {
234
+ if (rel === "neq") return "not " + shown;
235
+ return relationTail(rel, shown);
236
+ }
237
+
238
+ function relationTail(rel: string, shown: string): string {
239
+ if (rel === "le") return "at most " + shown;
240
+ if (rel === "ge") return "at least " + shown;
241
+ if (rel === "lt") return "less than " + shown;
242
+ if (rel === "gt") return "greater than " + shown;
243
+ return shown;
244
+ }
245
+
246
+ function copulaOf(phrase: string): string {
247
+ if (pluralNoun(phrase)) return "were";
248
+ return "was";
249
+ }
250
+
251
+ function pluralNoun(phrase: string): boolean {
252
+ const last = lowerLast(phrase);
253
+ if (last.length === 0) return false;
254
+ if (last === "status" || last === "is" || last === "bus") return false;
255
+ if (last.length >= 2 && last.slice(last.length - 2) === "ss") return false;
256
+ if (last.length >= 4 && last.slice(last.length - 4) === "ness") return false;
257
+ return last.slice(last.length - 1) === "s";
258
+ }
259
+
260
+ function lowerLast(phrase: string): string {
261
+ let start = 0;
262
+ for (let i = 0; i < phrase.length; i++) {
263
+ if (phrase.charCodeAt(i) === 32) start = i + 1;
264
+ }
265
+ let out = "";
266
+ for (let i = start; i < phrase.length; i++) {
267
+ const code = phrase.charCodeAt(i);
268
+ if (code >= 65 && code <= 90) out += String.fromCharCode(code + 32);
269
+ else out += phrase.slice(i, i + 1);
270
+ }
271
+ return out;
272
+ }
273
+
274
+ function insertNotAfter(text: string, word: string): string {
275
+ const changed = replaceWord(text, word, word + " not");
276
+ if (changed === text) return "";
277
+ return changed;
278
+ }
279
+
280
+ function hasWord(text: string, word: string): boolean {
281
+ return replaceWord(text, word, word + "\u0001") !== text;
282
+ }
283
+
284
+ export function termMentions(term: Term, index: number): boolean {
285
+ if (term.kind === "sym") return term.index === index;
286
+ if (term.kind === "not") return termMentions(term.term, index);
287
+ if (
288
+ term.kind === "and" ||
289
+ term.kind === "or" ||
290
+ term.kind === "eq" ||
291
+ term.kind === "neq" ||
292
+ term.kind === "lt" ||
293
+ term.kind === "le" ||
294
+ term.kind === "gt" ||
295
+ term.kind === "ge" ||
296
+ term.kind === "add" ||
297
+ term.kind === "sub"
298
+ ) {
299
+ return termMentions(term.left, index) || termMentions(term.right, index);
300
+ }
301
+ return false;
302
+ }
303
+
304
+ export function requirementMentions(req: Requirement, index: number): boolean {
305
+ if (req.antecedent !== null && termMentions(req.antecedent, index)) return true;
306
+ return termMentions(req.consequent, index);
307
+ }
package/src/term.ts ADDED
@@ -0,0 +1,331 @@
1
+ /**
2
+ * Evaluation and printing for predicate terms.
3
+ * Partial assignments use three-valued logic: an assertion is pruned only
4
+ * when it is forced false, which keeps the search sound.
5
+ */
6
+
7
+ import { findEnum, type Spec, type SymbolDecl, type Term } from "./spec.ts";
8
+ import { formatInt, formatSmtInt } from "./text.ts";
9
+
10
+ export const T_FALSE = 0;
11
+ export const T_TRUE = 1;
12
+ export const T_UNKNOWN = 2;
13
+
14
+ export class Binding {
15
+ known: boolean[];
16
+ code: number[];
17
+
18
+ constructor(n: number) {
19
+ this.known = [];
20
+ this.code = [];
21
+ for (let i = 0; i < n; i++) {
22
+ this.known.push(false);
23
+ this.code.push(0);
24
+ }
25
+ }
26
+
27
+ copy(): Binding {
28
+ const next = new Binding(this.code.length);
29
+ for (let i = 0; i < this.code.length; i++) {
30
+ next.known[i] = this.known[i];
31
+ next.code[i] = this.code[i];
32
+ }
33
+ return next;
34
+ }
35
+ }
36
+
37
+ interface Value {
38
+ known: boolean;
39
+ kind: string;
40
+ code: number;
41
+ }
42
+
43
+ function unknownValue(): Value {
44
+ return { known: false, kind: "", code: 0 };
45
+ }
46
+
47
+ function knownValue(kind: string, code: number): Value {
48
+ return { known: true, kind: kind, code: code };
49
+ }
50
+
51
+ export function evalBool(term: Term, spec: Spec, env: Binding): number {
52
+ if (term.kind === "bool") return term.value ? T_TRUE : T_FALSE;
53
+ if (term.kind === "sym") {
54
+ if (!env.known[term.index]) return T_UNKNOWN;
55
+ const sym = spec.symbols[term.index];
56
+ if (sym.sort.kind !== "bool") return T_UNKNOWN;
57
+ return env.code[term.index] === 1 ? T_TRUE : T_FALSE;
58
+ }
59
+ if (term.kind === "not") {
60
+ const inner = evalBool(term.term, spec, env);
61
+ if (inner === T_UNKNOWN) return T_UNKNOWN;
62
+ return inner === T_TRUE ? T_FALSE : T_TRUE;
63
+ }
64
+ if (term.kind === "and") {
65
+ const left = evalBool(term.left, spec, env);
66
+ const right = evalBool(term.right, spec, env);
67
+ if (left === T_FALSE || right === T_FALSE) return T_FALSE;
68
+ if (left === T_TRUE && right === T_TRUE) return T_TRUE;
69
+ return T_UNKNOWN;
70
+ }
71
+ if (term.kind === "or") {
72
+ const left = evalBool(term.left, spec, env);
73
+ const right = evalBool(term.right, spec, env);
74
+ if (left === T_TRUE || right === T_TRUE) return T_TRUE;
75
+ if (left === T_FALSE && right === T_FALSE) return T_FALSE;
76
+ return T_UNKNOWN;
77
+ }
78
+ if (
79
+ term.kind === "eq" ||
80
+ term.kind === "neq" ||
81
+ term.kind === "lt" ||
82
+ term.kind === "le" ||
83
+ term.kind === "gt" ||
84
+ term.kind === "ge"
85
+ ) {
86
+ return evalCmp(term, spec, env);
87
+ }
88
+ return T_UNKNOWN;
89
+ }
90
+
91
+ function evalCmp(term: Term, spec: Spec, env: Binding): number {
92
+ if (
93
+ term.kind !== "eq" &&
94
+ term.kind !== "neq" &&
95
+ term.kind !== "lt" &&
96
+ term.kind !== "le" &&
97
+ term.kind !== "gt" &&
98
+ term.kind !== "ge"
99
+ ) {
100
+ return T_UNKNOWN;
101
+ }
102
+ const left = evalValue(term.left, spec, env);
103
+ const right = evalValue(term.right, spec, env);
104
+ if (!left.known || !right.known) return T_UNKNOWN;
105
+ if (left.kind !== right.kind) return T_UNKNOWN;
106
+ const eq = left.code === right.code;
107
+ if (term.kind === "eq") return eq ? T_TRUE : T_FALSE;
108
+ if (term.kind === "neq") return eq ? T_FALSE : T_TRUE;
109
+ if (left.kind !== "int") return T_UNKNOWN;
110
+ if (term.kind === "lt") return left.code < right.code ? T_TRUE : T_FALSE;
111
+ if (term.kind === "le") return left.code <= right.code ? T_TRUE : T_FALSE;
112
+ if (term.kind === "gt") return left.code > right.code ? T_TRUE : T_FALSE;
113
+ return left.code >= right.code ? T_TRUE : T_FALSE;
114
+ }
115
+
116
+ function evalValue(term: Term, spec: Spec, env: Binding): Value {
117
+ if (term.kind === "bool") return knownValue("bool", term.value ? 1 : 0);
118
+ if (term.kind === "int") return knownValue("int", term.value);
119
+ if (term.kind === "enum") return knownValue("enum:" + term.enumName, term.index);
120
+ if (term.kind === "sym") {
121
+ if (!env.known[term.index]) return unknownValue();
122
+ const sym = spec.symbols[term.index];
123
+ if (sym.sort.kind === "bool") return knownValue("bool", env.code[term.index] === 1 ? 1 : 0);
124
+ if (sym.sort.kind === "int") return knownValue("int", env.code[term.index]);
125
+ return knownValue("enum:" + sym.sort.enumName, env.code[term.index]);
126
+ }
127
+ if (term.kind === "add" || term.kind === "sub") {
128
+ const left = evalValue(term.left, spec, env);
129
+ const right = evalValue(term.right, spec, env);
130
+ if (!left.known || !right.known) return unknownValue();
131
+ if (left.kind !== "int" || right.kind !== "int") return unknownValue();
132
+ const code = term.kind === "add" ? left.code + right.code : left.code - right.code;
133
+ return knownValue("int", code);
134
+ }
135
+ return unknownValue();
136
+ }
137
+
138
+ export function evalAssertion(antecedent: Term | null, consequent: Term, spec: Spec, env: Binding): number {
139
+ const guard = antecedent === null ? T_TRUE : evalBool(antecedent, spec, env);
140
+ if (guard === T_FALSE) return T_TRUE;
141
+ const body = evalBool(consequent, spec, env);
142
+ if (body === T_TRUE) return T_TRUE;
143
+ if (guard === T_TRUE && body === T_FALSE) return T_FALSE;
144
+ return T_UNKNOWN;
145
+ }
146
+
147
+ function isSimple(term: Term): boolean {
148
+ return term.kind === "bool" || term.kind === "int" || term.kind === "enum" || term.kind === "sym";
149
+ }
150
+
151
+ export function formatTerm(term: Term): string {
152
+ if (term.kind === "bool") return term.value ? "true" : "false";
153
+ if (term.kind === "int") return formatInt(term.value);
154
+ if (term.kind === "enum") return term.variant;
155
+ if (term.kind === "sym") {
156
+ if (term.phrase.length > 0) return term.phrase;
157
+ return term.name;
158
+ }
159
+ if (term.kind === "not") {
160
+ if (isSimple(term.term)) return "not " + formatTerm(term.term);
161
+ return "not (" + formatTerm(term.term) + ")";
162
+ }
163
+ if (term.kind === "and") return formatSide(term.left, "and", true) + " and " + formatSide(term.right, "and", false);
164
+ if (term.kind === "or") return formatSide(term.left, "or", true) + " or " + formatSide(term.right, "or", false);
165
+ if (term.kind === "add") return formatSide(term.left, "add", true) + " + " + formatSide(term.right, "add", false);
166
+ if (term.kind === "sub") return formatSide(term.left, "add", true) + " - " + formatSide(term.right, "add", false);
167
+ return formatSide(term.left, "add", true) + " " + comparisonWord(term) + " " + formatSide(term.right, "add", true);
168
+ }
169
+
170
+ function comparisonWord(term: Term): string {
171
+ if (
172
+ term.kind !== "eq" &&
173
+ term.kind !== "neq" &&
174
+ term.kind !== "lt" &&
175
+ term.kind !== "le" &&
176
+ term.kind !== "gt" &&
177
+ term.kind !== "ge"
178
+ ) {
179
+ return "?";
180
+ }
181
+ if (term.left.kind === "sym" && term.left.phrase.length > 0) {
182
+ if (term.kind === "eq") return "is";
183
+ if (term.kind === "neq") return "is not";
184
+ if (term.kind === "lt") return "is less than";
185
+ if (term.kind === "le") return "is at most";
186
+ if (term.kind === "gt") return "is greater than";
187
+ return "is at least";
188
+ }
189
+ return cmpOp(term);
190
+ }
191
+
192
+ function formatSide(term: Term, parent: string, isLeft: boolean): string {
193
+ if (needsParen(term, parent, isLeft)) return "(" + formatTerm(term) + ")";
194
+ return formatTerm(term);
195
+ }
196
+
197
+ function needsParen(term: Term, parent: string, isLeft: boolean): boolean {
198
+ if (parent === "or") return term.kind === "or" && !isLeft;
199
+ if (parent === "and") return term.kind === "or" || (term.kind === "and" && !isLeft);
200
+ if (parent === "add") {
201
+ if (term.kind === "or" || term.kind === "and" || term.kind === "not") return true;
202
+ if (term.kind === "eq" || term.kind === "neq" || term.kind === "lt" || term.kind === "le" || term.kind === "gt" || term.kind === "ge") {
203
+ return true;
204
+ }
205
+ if ((term.kind === "add" || term.kind === "sub") && !isLeft) return true;
206
+ }
207
+ return false;
208
+ }
209
+
210
+ function cmpOp(term: Term): string {
211
+ if (term.kind === "eq") return "=";
212
+ if (term.kind === "neq") return "!=";
213
+ if (term.kind === "lt") return "<";
214
+ if (term.kind === "le") return "<=";
215
+ if (term.kind === "gt") return ">";
216
+ if (term.kind === "ge") return ">=";
217
+ return "?";
218
+ }
219
+
220
+ export function formatSmtTerm(term: Term): string {
221
+ if (term.kind === "bool") return term.value ? "true" : "false";
222
+ if (term.kind === "int") return formatSmtInt(term.value);
223
+ if (term.kind === "enum") return term.variant;
224
+ if (term.kind === "sym") return term.name;
225
+ if (term.kind === "not") return "(not " + formatSmtTerm(term.term) + ")";
226
+ if (term.kind === "and") return "(and " + formatSmtTerm(term.left) + " " + formatSmtTerm(term.right) + ")";
227
+ if (term.kind === "or") return "(or " + formatSmtTerm(term.left) + " " + formatSmtTerm(term.right) + ")";
228
+ if (term.kind === "eq") return "(= " + formatSmtTerm(term.left) + " " + formatSmtTerm(term.right) + ")";
229
+ if (term.kind === "neq") return "(not (= " + formatSmtTerm(term.left) + " " + formatSmtTerm(term.right) + "))";
230
+ if (term.kind === "lt") return "(< " + formatSmtTerm(term.left) + " " + formatSmtTerm(term.right) + ")";
231
+ if (term.kind === "le") return "(<= " + formatSmtTerm(term.left) + " " + formatSmtTerm(term.right) + ")";
232
+ if (term.kind === "gt") return "(> " + formatSmtTerm(term.left) + " " + formatSmtTerm(term.right) + ")";
233
+ if (term.kind === "ge") return "(>= " + formatSmtTerm(term.left) + " " + formatSmtTerm(term.right) + ")";
234
+ if (term.kind === "add") return "(+ " + formatSmtTerm(term.left) + " " + formatSmtTerm(term.right) + ")";
235
+ return "(- " + formatSmtTerm(term.left) + " " + formatSmtTerm(term.right) + ")";
236
+ }
237
+
238
+ export function formatScenario(spec: Spec, env: Binding): string {
239
+ const lines: string[] = [];
240
+ lines.push("{");
241
+ for (let i = 0; i < spec.symbols.length; i++) {
242
+ const sym = spec.symbols[i];
243
+ const comma = i + 1 < spec.symbols.length ? "," : "";
244
+ lines.push(" \"" + sym.name + "\": " + jsonLiteral(spec, sym, env.code[i]) + comma);
245
+ }
246
+ lines.push("}");
247
+ return lines.join("\n");
248
+ }
249
+
250
+ function jsonLiteral(spec: Spec, sym: SymbolDecl, code: number): string {
251
+ if (sym.sort.kind === "bool") return code === 1 ? "true" : "false";
252
+ if (sym.sort.kind === "int") return formatInt(code);
253
+ const decl = findEnum(spec, sym.sort.enumName);
254
+ const variant = decl === null || code < 0 || code >= decl.variants.length ? "?" : decl.variants[code];
255
+ return "\"" + variant + "\"";
256
+ }
257
+
258
+ export function mentionedIndexes(antecedent: Term | null, consequent: Term): number[] {
259
+ const found: number[] = [];
260
+ if (antecedent !== null) collectIndexes(antecedent, found);
261
+ collectIndexes(consequent, found);
262
+ const ordered: number[] = [];
263
+ for (let i = 0; i < found.length; i++) {
264
+ let seen = false;
265
+ for (let j = 0; j < ordered.length; j++) {
266
+ if (ordered[j] === found[i]) seen = true;
267
+ }
268
+ if (!seen) ordered.push(found[i]);
269
+ }
270
+ const sorted: number[] = [];
271
+ for (let i = 0; i < ordered.length; i++) sorted.push(ordered[i]);
272
+ for (let i = 0; i < sorted.length; i++) {
273
+ for (let j = i + 1; j < sorted.length; j++) {
274
+ if (sorted[j] < sorted[i]) {
275
+ const tmp = sorted[i];
276
+ sorted[i] = sorted[j];
277
+ sorted[j] = tmp;
278
+ }
279
+ }
280
+ }
281
+ return sorted;
282
+ }
283
+
284
+ export function termsEqual(left: Term, right: Term): boolean {
285
+ if (left.kind === "bool" && right.kind === "bool") return left.value === right.value;
286
+ if (left.kind === "int" && right.kind === "int") return left.value === right.value;
287
+ if (left.kind === "enum" && right.kind === "enum") return left.enumName === right.enumName && left.index === right.index;
288
+ if (left.kind === "sym" && right.kind === "sym") return left.index === right.index;
289
+ if (left.kind === "not" && right.kind === "not") return termsEqual(left.term, right.term);
290
+ if (left.kind === "and" && right.kind === "and") return termsSamePair(left.left, left.right, right.left, right.right);
291
+ if (left.kind === "or" && right.kind === "or") return termsSamePair(left.left, left.right, right.left, right.right);
292
+ if (left.kind === "eq" && right.kind === "eq") return termsSamePair(left.left, left.right, right.left, right.right);
293
+ if (left.kind === "neq" && right.kind === "neq") return termsSamePair(left.left, left.right, right.left, right.right);
294
+ if (left.kind === "lt" && right.kind === "lt") return termsSamePair(left.left, left.right, right.left, right.right);
295
+ if (left.kind === "le" && right.kind === "le") return termsSamePair(left.left, left.right, right.left, right.right);
296
+ if (left.kind === "gt" && right.kind === "gt") return termsSamePair(left.left, left.right, right.left, right.right);
297
+ if (left.kind === "ge" && right.kind === "ge") return termsSamePair(left.left, left.right, right.left, right.right);
298
+ if (left.kind === "add" && right.kind === "add") return termsSamePair(left.left, left.right, right.left, right.right);
299
+ if (left.kind === "sub" && right.kind === "sub") return termsSamePair(left.left, left.right, right.left, right.right);
300
+ return false;
301
+ }
302
+
303
+ function termsSamePair(a: Term, b: Term, c: Term, d: Term): boolean {
304
+ return termsEqual(a, c) && termsEqual(b, d);
305
+ }
306
+
307
+ function collectIndexes(term: Term, found: number[]): void {
308
+ if (term.kind === "sym") {
309
+ found.push(term.index);
310
+ return;
311
+ }
312
+ if (term.kind === "not") {
313
+ collectIndexes(term.term, found);
314
+ return;
315
+ }
316
+ if (
317
+ term.kind === "and" ||
318
+ term.kind === "or" ||
319
+ term.kind === "eq" ||
320
+ term.kind === "neq" ||
321
+ term.kind === "lt" ||
322
+ term.kind === "le" ||
323
+ term.kind === "gt" ||
324
+ term.kind === "ge" ||
325
+ term.kind === "add" ||
326
+ term.kind === "sub"
327
+ ) {
328
+ collectIndexes(term.left, found);
329
+ collectIndexes(term.right, found);
330
+ }
331
+ }