visualproof 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.
- package/LICENSE +202 -0
- package/README.md +99 -2
- package/dist/assets/fonts/JuliaMono-Regular.ttf +0 -0
- package/dist/assets/fonts/JuliaMono-RegularItalic.ttf +0 -0
- package/dist/assets/fonts/JuliaMono-SemiBold.ttf +0 -0
- package/dist/assets/fonts/LICENSE-JuliaMono.txt +93 -0
- package/dist/assets/latex/visualproof.sty +308 -0
- package/dist/extractor/Extract.lean +1180 -0
- package/dist/extractor/Usage.lean +900 -0
- package/dist/mcp/visualproof-mcp.mjs +476 -0
- package/dist/server/access.js +95 -0
- package/dist/server/agent.js +3235 -0
- package/dist/server/agentdocs.js +35 -0
- package/dist/server/agentlibrary.js +202 -0
- package/dist/server/agentnotes.js +701 -0
- package/dist/server/agentviews.js +703 -0
- package/dist/server/askfollow.js +286 -0
- package/dist/server/boardspots.js +159 -0
- package/dist/server/dirs.js +175 -0
- package/dist/server/doctor.js +186 -0
- package/dist/server/events.js +198 -0
- package/dist/server/export.js +420 -0
- package/dist/server/extract.js +204 -0
- package/dist/server/fetchurl.js +321 -0
- package/dist/server/figures.js +158 -0
- package/dist/server/history.js +741 -0
- package/dist/server/historyapi.js +304 -0
- package/dist/server/hover.js +595 -0
- package/dist/server/identity.js +114 -0
- package/dist/server/invites.js +241 -0
- package/dist/server/layoutthread.js +51 -0
- package/dist/server/library.js +600 -0
- package/dist/server/libraryroute.js +211 -0
- package/dist/server/live.js +1101 -0
- package/dist/server/lostfound.js +199 -0
- package/dist/server/main.js +1394 -0
- package/dist/server/mcptools.js +497 -0
- package/dist/server/pastdoc.js +73 -0
- package/dist/server/pipeline.js +156 -0
- package/dist/server/poppler.js +251 -0
- package/dist/server/presence.js +252 -0
- package/dist/server/project.js +222 -0
- package/dist/server/prompt.js +53 -0
- package/dist/server/readonly.js +152 -0
- package/dist/server/readonlygate.js +123 -0
- package/dist/server/renames.js +268 -0
- package/dist/server/review.js +334 -0
- package/dist/server/reviewapi.js +188 -0
- package/dist/server/snapshot.js +49 -0
- package/dist/server/sourcestore.js +473 -0
- package/dist/server/status.js +360 -0
- package/dist/server/talk.js +476 -0
- package/dist/server/tokens.js +221 -0
- package/dist/server/usage.js +501 -0
- package/dist/server/versions.js +82 -0
- package/dist/server/view.js +157 -0
- package/dist/server/views.js +1167 -0
- package/dist/shared/askfollow.js +46 -0
- package/dist/shared/board.js +332 -0
- package/dist/shared/build.js +2 -0
- package/dist/shared/collab.js +148 -0
- package/dist/shared/comment.js +285 -0
- package/dist/shared/context.js +157 -0
- package/dist/shared/doc/authors.js +98 -0
- package/dist/shared/doc/build.js +358 -0
- package/dist/shared/doc/latex.js +455 -0
- package/dist/shared/doc/markdown.js +159 -0
- package/dist/shared/doc/model.js +37 -0
- package/dist/shared/doc/order.js +107 -0
- package/dist/shared/doc/proof.js +272 -0
- package/dist/shared/doc/sections.js +93 -0
- package/dist/shared/doc/settings.js +75 -0
- package/dist/shared/edgecodec.js +102 -0
- package/dist/shared/editor.js +64 -0
- package/dist/shared/excerpt.js +87 -0
- package/dist/shared/graph.js +16 -0
- package/dist/shared/items.js +92 -0
- package/dist/shared/keys.js +203 -0
- package/dist/shared/layout.js +926 -0
- package/dist/shared/lexer.js +130 -0
- package/dist/shared/library.js +711 -0
- package/dist/shared/lostfound.js +150 -0
- package/dist/shared/moves.js +243 -0
- package/dist/shared/notecard.js +295 -0
- package/dist/shared/noteplace.js +505 -0
- package/dist/shared/palette.js +46 -0
- package/dist/shared/pastgraph.js +95 -0
- package/dist/shared/places.js +80 -0
- package/dist/shared/projectnotes.js +244 -0
- package/dist/shared/query.js +810 -0
- package/dist/shared/review.js +642 -0
- package/dist/shared/reviewqueue.js +333 -0
- package/dist/shared/ring.js +366 -0
- package/dist/shared/scope.js +255 -0
- package/dist/shared/scopeedit.js +81 -0
- package/dist/shared/search.js +191 -0
- package/dist/shared/separate.js +178 -0
- package/dist/shared/sharesnap.js +280 -0
- package/dist/shared/sticky.js +501 -0
- package/dist/shared/timeline.js +73 -0
- package/dist/shared/titles.js +298 -0
- package/dist/shared/viewgraph.js +139 -0
- package/dist/shared/views.js +859 -0
- package/dist/web/bundle/app.js +48733 -0
- package/dist/web/bundle/edges.js +409 -0
- package/dist/web/bundle/layout.js +1010 -0
- package/dist/web/bundle/pdfworker.js +48322 -0
- package/dist/web/index.html +239 -0
- package/dist/web/src/motion.js +430 -0
- package/dist/web/src/past.js +224 -0
- package/dist/web/src/scene.js +787 -0
- package/dist/web/style.css +2312 -0
- package/docs/agent-api/README.md +91 -0
- package/docs/agent-api/advice.md +108 -0
- package/docs/agent-api/export.md +120 -0
- package/docs/agent-api/following.md +140 -0
- package/docs/agent-api/history.md +142 -0
- package/docs/agent-api/library.md +65 -0
- package/docs/agent-api/mcp.md +104 -0
- package/docs/agent-api/notes.md +93 -0
- package/docs/agent-api/reading.md +166 -0
- package/docs/agent-api/requests.md +94 -0
- package/docs/agent-api/talking.md +100 -0
- package/docs/agent-api/tcb.md +135 -0
- package/docs/agent-api/views.md +124 -0
- package/docs/agent-api/words.md +56 -0
- package/package.json +39 -4
- package/test/toolchains.json +5 -0
|
@@ -0,0 +1,810 @@
|
|
|
1
|
+
// One query language for what is on a board and for what lights up (ViewSpec decision 4): a view's
|
|
2
|
+
// scope is a query with fixed roots (`tcb(fermatLastTheoremFour) except trusted`), and so is what the
|
|
3
|
+
// reader selected, which is what is lit: one part per click, joined by `or`
|
|
4
|
+
// (`decl(A.b) or tcb(ns(A.C) except external)`, web/src/selection.ts). The same query has a structured form (a
|
|
5
|
+
// JSON tree, what the server stores and agents may send) and a short text form (what the address,
|
|
6
|
+
// agents and the scope chips carry); the two are parsed and printed both ways, and the text printed
|
|
7
|
+
// is canonical, so two scopes are the same when their texts are.
|
|
8
|
+
//
|
|
9
|
+
// The text form, left to right:
|
|
10
|
+
//
|
|
11
|
+
// all | selection | decl(A, B) | tcb(A) | uses(A, B) | usedBy(selection)
|
|
12
|
+
// tcb(ns(Fermat42) except external) a closure of the set a query holds
|
|
13
|
+
// ns(Fermat42) declarations whose name is under the prefix
|
|
14
|
+
// file(Mathlib.NumberTheory) declarations of the modules under the prefix
|
|
15
|
+
// trusted | external | sorry | nonstandard
|
|
16
|
+
// kind(theorem, def) | status(unreviewed, stale) | advice(comment)
|
|
17
|
+
// key(fermatLastTheoremFour) the target and the declarations marked key for it
|
|
18
|
+
// key(targets) the same for every target
|
|
19
|
+
// X and Y | X or Y | X except Y left to right, one level; parentheses group
|
|
20
|
+
//
|
|
21
|
+
// A name that holds a space, a parenthesis, a comma or a quote — or is the word `selection`, or
|
|
22
|
+
// `targets` inside key(…) — is written as a JSON string. Inside a closure's parentheses, what starts with a parenthesis, with a
|
|
23
|
+
// word and a parenthesis (`ns(`), or joins with and, or, except is a query; a query that is one
|
|
24
|
+
// plain word is written in parentheses there (`tcb((external))`). Words are read without regard to case (`usedby`); `namespace` and
|
|
25
|
+
// `module` may stand for `ns` and `file`.
|
|
26
|
+
import { adviceOf, keyMarks, STANDARD_AXIOMS, statusOf, trusted, under } from "./review.js";
|
|
27
|
+
export const KINDS = ["theorem", "def", "abbrev", "instance", "structure", "class", "inductive", "opaque", "axiom", "quot", "constructor", "recursor", "notation", "unknown"];
|
|
28
|
+
export const STATUSES = ["unreviewed", "approved", "commented", "stale"];
|
|
29
|
+
const VERDICTS = ["approve", "comment"];
|
|
30
|
+
const CLOSURES = ["tcb", "uses", "usedBy"];
|
|
31
|
+
const PLAIN = ["all", "selection", "trusted", "external", "sorry", "nonstandard"];
|
|
32
|
+
const COMBINATORS = ["and", "or", "except"];
|
|
33
|
+
/** words for the same thing, read as the first */
|
|
34
|
+
const ALIASES = { usedby: "usedBy", namespace: "ns", module: "file", decls: "decl" };
|
|
35
|
+
/** Lighting up (ViewSpec decision 4): the tools that are closures of the graph, as queries relative
|
|
36
|
+
* to the selection (the Keys step of Rests on is keysQuery). */
|
|
37
|
+
export const LIGHT = {
|
|
38
|
+
plain: { op: "selection" },
|
|
39
|
+
tcb: { op: "tcb", of: "selection" },
|
|
40
|
+
deps: { op: "uses", of: "selection" },
|
|
41
|
+
apps: { op: "usedBy", of: "selection" },
|
|
42
|
+
};
|
|
43
|
+
/** Which closure each closure word is, as the graph computes it (shared/viewgraph.ts). */
|
|
44
|
+
const CLOSURE_MODE = { tcb: "tcb", uses: "deps", usedBy: "apps" };
|
|
45
|
+
// ---------------------------------------------------------------- how large a query may be
|
|
46
|
+
/** How large a query may be: one set of bounds for its text and its JSON form, checked when either
|
|
47
|
+
* is read (parseQuery, cleanQuery), so that what one form takes the other takes too — a scope
|
|
48
|
+
* saved from its text is read back from its JSON at the next start. The bounds hold what a scope
|
|
49
|
+
* or a selection needs with room to spare (900 picks of a selection fit), and keep reading,
|
|
50
|
+
* printing and evaluating any query a matter of milliseconds: a closure walks the whole graph, and
|
|
51
|
+
* every function on a query is recursive. */
|
|
52
|
+
export const QUERY_LIMITS = {
|
|
53
|
+
/** characters of its canonical text (printQuery), which is what the JSON form stands for */
|
|
54
|
+
text: 20_000,
|
|
55
|
+
/** nesting: a closure of a query, and a combination on the right of and, or, except — what the
|
|
56
|
+
* text writes in parentheses — each one level deeper (`tcb(ns(A) except (B or C))`: 2) */
|
|
57
|
+
depth: 32,
|
|
58
|
+
/** its parts: all, decl(…), tcb(…), ns(…), kind(…), … wherever they stand */
|
|
59
|
+
terms: 1_000,
|
|
60
|
+
/** names and prefixes, all together */
|
|
61
|
+
names: 2_000,
|
|
62
|
+
/** characters of one name or prefix */
|
|
63
|
+
name: 2_000,
|
|
64
|
+
/** closures (tcb, uses, usedBy), each a walk of the graph; each target of a `key(…)` counts as
|
|
65
|
+
* one too, since its review set is walked to keep only the marks still in it. `key(targets)`
|
|
66
|
+
* counts as one: how many targets there are is the project's, not the query's, and every
|
|
67
|
+
* `key(targets)` of a query shares the same walks (evaluateIn) */
|
|
68
|
+
closures: 64,
|
|
69
|
+
};
|
|
70
|
+
/** Parentheses within parentheses the reader of the text follows: twice the depth, since a
|
|
71
|
+
* closure of one plain word takes two (`tcb((all))`), with a margin. Only a guard for the stack;
|
|
72
|
+
* what a query may hold is QUERY_LIMITS, checked on what was read. */
|
|
73
|
+
const MAX_PARENS = 2 * QUERY_LIMITS.depth + 8;
|
|
74
|
+
/** Why a query is larger than QUERY_LIMITS allows, or null. Called on a tree already bounded in
|
|
75
|
+
* size by whoever built it, so that its recursion is shallow. */
|
|
76
|
+
export function beyondLimits(q) {
|
|
77
|
+
let terms = 0, names = 0, closures = 0, longest = 0;
|
|
78
|
+
const name = (s) => {
|
|
79
|
+
names++;
|
|
80
|
+
longest = Math.max(longest, s.length);
|
|
81
|
+
};
|
|
82
|
+
const depth = (x) => {
|
|
83
|
+
switch (x.op) {
|
|
84
|
+
case "and":
|
|
85
|
+
case "or":
|
|
86
|
+
case "except":
|
|
87
|
+
return Math.max(depth(x.a), depth(x.b) + (COMBINATORS.includes(x.b.op) ? 1 : 0));
|
|
88
|
+
case "tcb":
|
|
89
|
+
case "uses":
|
|
90
|
+
case "usedBy":
|
|
91
|
+
closures++;
|
|
92
|
+
if (x.of !== "selection" && !Array.isArray(x.of)) {
|
|
93
|
+
terms++;
|
|
94
|
+
return 1 + depth(x.of);
|
|
95
|
+
}
|
|
96
|
+
if (Array.isArray(x.of))
|
|
97
|
+
x.of.forEach(name);
|
|
98
|
+
terms++;
|
|
99
|
+
return 0;
|
|
100
|
+
case "decl":
|
|
101
|
+
x.names.forEach(name);
|
|
102
|
+
terms++;
|
|
103
|
+
return 0;
|
|
104
|
+
case "key":
|
|
105
|
+
if (x.of === "targets")
|
|
106
|
+
closures++;
|
|
107
|
+
else {
|
|
108
|
+
x.of.forEach(name);
|
|
109
|
+
closures += x.of.length;
|
|
110
|
+
}
|
|
111
|
+
terms++;
|
|
112
|
+
return 0;
|
|
113
|
+
case "ns":
|
|
114
|
+
case "file":
|
|
115
|
+
name(x.prefix);
|
|
116
|
+
terms++;
|
|
117
|
+
return 0;
|
|
118
|
+
default:
|
|
119
|
+
terms++;
|
|
120
|
+
return 0;
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
const L = QUERY_LIMITS;
|
|
124
|
+
const d = depth(q);
|
|
125
|
+
if (d > L.depth)
|
|
126
|
+
return `the query nests ${d} levels deep, more than ${L.depth}`;
|
|
127
|
+
if (terms > L.terms)
|
|
128
|
+
return `the query has ${terms} parts, more than ${L.terms.toLocaleString("en")}`;
|
|
129
|
+
if (names > L.names)
|
|
130
|
+
return `the query names ${names} declarations or prefixes, more than ${L.names.toLocaleString("en")}`;
|
|
131
|
+
if (longest > L.name)
|
|
132
|
+
return `a name in the query is longer than ${L.name.toLocaleString("en")} characters`;
|
|
133
|
+
if (closures > L.closures)
|
|
134
|
+
return `the query has ${closures} closures (tcb, uses, usedBy), more than ${L.closures}`;
|
|
135
|
+
const length = printQuery(q).length;
|
|
136
|
+
if (length > L.text)
|
|
137
|
+
return `the query is ${length.toLocaleString("en")} characters long as printed, more than ${L.text.toLocaleString("en")}`;
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
// ---------------------------------------------------------------- the text form
|
|
141
|
+
export class QueryError extends Error {
|
|
142
|
+
/** where in the text it went wrong */
|
|
143
|
+
at;
|
|
144
|
+
constructor(message, at) {
|
|
145
|
+
super(message);
|
|
146
|
+
this.at = at;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function tokens(text) {
|
|
150
|
+
const out = [];
|
|
151
|
+
let i = 0;
|
|
152
|
+
while (i < text.length) {
|
|
153
|
+
const c = text[i];
|
|
154
|
+
if (/\s/.test(c)) {
|
|
155
|
+
i++;
|
|
156
|
+
}
|
|
157
|
+
else if (c === "(" || c === ")" || c === ",") {
|
|
158
|
+
out.push({ t: c, at: i });
|
|
159
|
+
i++;
|
|
160
|
+
}
|
|
161
|
+
else if (c === '"') {
|
|
162
|
+
// a JSON string: escapes as JSON has them
|
|
163
|
+
let j = i + 1;
|
|
164
|
+
while (j < text.length && text[j] !== '"')
|
|
165
|
+
j += text[j] === "\\" ? 2 : 1;
|
|
166
|
+
if (j >= text.length)
|
|
167
|
+
throw new QueryError("a quoted name is not closed", i);
|
|
168
|
+
let v;
|
|
169
|
+
try {
|
|
170
|
+
v = JSON.parse(text.slice(i, j + 1));
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
throw new QueryError("a quoted name is not a JSON string", i);
|
|
174
|
+
}
|
|
175
|
+
out.push({ t: "string", v: v, at: i });
|
|
176
|
+
i = j + 1;
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
let j = i;
|
|
180
|
+
while (j < text.length && !/[\s(),"]/.test(text[j]))
|
|
181
|
+
j++;
|
|
182
|
+
out.push({ t: "word", v: text.slice(i, j), at: i });
|
|
183
|
+
i = j;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return out;
|
|
187
|
+
}
|
|
188
|
+
/** The query a text says, within QUERY_LIMITS; throws a QueryError that says where it went wrong,
|
|
189
|
+
* or what is too large. */
|
|
190
|
+
export function parseQuery(text) {
|
|
191
|
+
if (text.length > QUERY_LIMITS.text)
|
|
192
|
+
throw new QueryError(`the query is longer than ${QUERY_LIMITS.text.toLocaleString("en")} characters`, 0);
|
|
193
|
+
let q;
|
|
194
|
+
try {
|
|
195
|
+
q = parse(text);
|
|
196
|
+
}
|
|
197
|
+
catch (e) {
|
|
198
|
+
// the reader follows parentheses by recursion; a guard stops it long before the stack ends,
|
|
199
|
+
// but whatever runs out of stack is a query too large, said as one (a 400, not a 500)
|
|
200
|
+
if (e instanceof RangeError)
|
|
201
|
+
throw new QueryError("the query is nested too deeply to read", 0);
|
|
202
|
+
throw e;
|
|
203
|
+
}
|
|
204
|
+
const why = beyondLimits(q);
|
|
205
|
+
if (why)
|
|
206
|
+
throw new QueryError(why, 0);
|
|
207
|
+
return q;
|
|
208
|
+
}
|
|
209
|
+
function parse(text) {
|
|
210
|
+
const ts = tokens(text);
|
|
211
|
+
let k = 0;
|
|
212
|
+
// what has been read so far, against the bounds: the reader stops as soon as one is passed, so
|
|
213
|
+
// that what it builds stays small enough for everything after it
|
|
214
|
+
let parens = 0, terms = 0, names = 0;
|
|
215
|
+
const nest = (at, read) => {
|
|
216
|
+
if (++parens > MAX_PARENS)
|
|
217
|
+
throw new QueryError(`the query nests more than ${QUERY_LIMITS.depth} levels deep`, at);
|
|
218
|
+
const r = read();
|
|
219
|
+
parens--;
|
|
220
|
+
return r;
|
|
221
|
+
};
|
|
222
|
+
const end = text.length;
|
|
223
|
+
const peek = () => ts[k];
|
|
224
|
+
const where = () => ts[k]?.at ?? end;
|
|
225
|
+
const word = (t) => (t && t.t === "word" ? ALIASES[t.v.toLowerCase()] ?? t.v : null);
|
|
226
|
+
const keyword = (t) => {
|
|
227
|
+
const w = word(t);
|
|
228
|
+
if (w === null)
|
|
229
|
+
return null;
|
|
230
|
+
const lower = w.toLowerCase();
|
|
231
|
+
// keywords are read without regard to case; `usedBy` keeps its capital
|
|
232
|
+
return lower === "usedby" ? "usedBy" : lower;
|
|
233
|
+
};
|
|
234
|
+
const text1 = (t) => ("v" in t ? t.v : t.t);
|
|
235
|
+
const args = () => {
|
|
236
|
+
if (peek()?.t !== "(")
|
|
237
|
+
throw new QueryError(`${text1(ts[k - 1])} takes its arguments in parentheses, e.g. ${text1(ts[k - 1])}(A.b)`, where());
|
|
238
|
+
k++;
|
|
239
|
+
const out = [];
|
|
240
|
+
if (peek()?.t === ")") {
|
|
241
|
+
k++;
|
|
242
|
+
return out;
|
|
243
|
+
}
|
|
244
|
+
for (;;) {
|
|
245
|
+
const t = peek();
|
|
246
|
+
if (!t || (t.t !== "word" && t.t !== "string"))
|
|
247
|
+
throw new QueryError("a name or a word was expected", where());
|
|
248
|
+
if (++names > QUERY_LIMITS.names)
|
|
249
|
+
throw new QueryError(`the query names more than ${QUERY_LIMITS.names.toLocaleString("en")} declarations or prefixes`, t.at);
|
|
250
|
+
if (t.v.length > QUERY_LIMITS.name)
|
|
251
|
+
throw new QueryError(`a name is longer than ${QUERY_LIMITS.name.toLocaleString("en")} characters`, t.at);
|
|
252
|
+
out.push(t);
|
|
253
|
+
k++;
|
|
254
|
+
const sep = peek();
|
|
255
|
+
if (sep?.t === ")") {
|
|
256
|
+
k++;
|
|
257
|
+
return out;
|
|
258
|
+
}
|
|
259
|
+
if (sep?.t !== ",")
|
|
260
|
+
throw new QueryError("a comma or a closing parenthesis was expected", where());
|
|
261
|
+
k++;
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
const choose = (xs, allowed, what) => {
|
|
265
|
+
if (!xs.length)
|
|
266
|
+
throw new QueryError(`${what}: give at least one`, where());
|
|
267
|
+
return xs.map((x) => {
|
|
268
|
+
const v = x.t === "word" ? x.v.toLowerCase() : null;
|
|
269
|
+
const hit = allowed.find((a) => a.toLowerCase() === v);
|
|
270
|
+
if (!hit)
|
|
271
|
+
throw new QueryError(`${text1(x)} is not one of ${allowed.join(", ")}`, x.at);
|
|
272
|
+
return hit;
|
|
273
|
+
});
|
|
274
|
+
};
|
|
275
|
+
const atom = () => {
|
|
276
|
+
const t = peek();
|
|
277
|
+
if (!t)
|
|
278
|
+
throw new QueryError("the query ends too early", end);
|
|
279
|
+
if (t.t === "(") {
|
|
280
|
+
k++;
|
|
281
|
+
const q = nest(t.at, expr);
|
|
282
|
+
if (peek()?.t !== ")")
|
|
283
|
+
throw new QueryError("a closing parenthesis was expected", where());
|
|
284
|
+
k++;
|
|
285
|
+
return q;
|
|
286
|
+
}
|
|
287
|
+
const w = keyword(t);
|
|
288
|
+
if (w === null)
|
|
289
|
+
throw new QueryError(t.t === "string" ? "a quoted name stands only inside decl(…), tcb(…), uses(…) or usedBy(…)" : `${t.t} is out of place`, t.at);
|
|
290
|
+
if (++terms > QUERY_LIMITS.terms)
|
|
291
|
+
throw new QueryError(`the query has more than ${QUERY_LIMITS.terms.toLocaleString("en")} parts`, t.at);
|
|
292
|
+
k++;
|
|
293
|
+
if (PLAIN.includes(w))
|
|
294
|
+
return { op: w };
|
|
295
|
+
if (CLOSURES.includes(w)) {
|
|
296
|
+
// a query inside: it starts with a parenthesis or a word and its parenthesis, or a name is
|
|
297
|
+
// joined to something (a list of names never is)
|
|
298
|
+
const [a, b] = [ts[k + 1], ts[k + 2]];
|
|
299
|
+
const join = keyword(b);
|
|
300
|
+
if (peek()?.t === "(" && (a?.t === "(" || (a?.t === "word" && (b?.t === "(" || (join !== null && COMBINATORS.includes(join)))))) {
|
|
301
|
+
k++;
|
|
302
|
+
const of = nest(t.at, expr);
|
|
303
|
+
if (peek()?.t !== ")")
|
|
304
|
+
throw new QueryError("a closing parenthesis was expected", where());
|
|
305
|
+
k++;
|
|
306
|
+
return { op: w, of };
|
|
307
|
+
}
|
|
308
|
+
const xs = args();
|
|
309
|
+
if (xs.length === 1 && xs[0].t === "word" && xs[0].v === "selection")
|
|
310
|
+
return { op: w, of: "selection" };
|
|
311
|
+
if (!xs.length)
|
|
312
|
+
throw new QueryError(`${w}: give the declarations it starts from`, where());
|
|
313
|
+
return { op: w, of: xs.map(text1) };
|
|
314
|
+
}
|
|
315
|
+
if (w === "decl") {
|
|
316
|
+
const xs = args();
|
|
317
|
+
if (!xs.length)
|
|
318
|
+
throw new QueryError("decl: give at least one name", where());
|
|
319
|
+
return { op: "decl", names: xs.map(text1) };
|
|
320
|
+
}
|
|
321
|
+
if (w === "key") {
|
|
322
|
+
const xs = args();
|
|
323
|
+
if (!xs.length)
|
|
324
|
+
throw new QueryError("key: give at least one target", where());
|
|
325
|
+
if (xs.length === 1 && xs[0].t === "word" && xs[0].v === "targets")
|
|
326
|
+
return { op: "key", of: "targets" };
|
|
327
|
+
return { op: "key", of: xs.map(text1) };
|
|
328
|
+
}
|
|
329
|
+
if (w === "ns" || w === "file") {
|
|
330
|
+
const xs = args();
|
|
331
|
+
if (xs.length !== 1)
|
|
332
|
+
throw new QueryError(`${w}: give one prefix`, where());
|
|
333
|
+
return { op: w, prefix: text1(xs[0]) };
|
|
334
|
+
}
|
|
335
|
+
if (w === "kind")
|
|
336
|
+
return { op: "kind", kinds: choose(args(), KINDS, "kind") };
|
|
337
|
+
if (w === "status")
|
|
338
|
+
return { op: "status", statuses: choose(args(), STATUSES, "status") };
|
|
339
|
+
if (w === "advice")
|
|
340
|
+
return { op: "advice", verdicts: choose(args(), VERDICTS, "advice") };
|
|
341
|
+
throw new QueryError(`${text1(t)} is not a word of the query language (all, tcb, uses, usedBy, decl, key, ns, file, trusted, external, kind, status, advice, sorry, nonstandard)`, t.at);
|
|
342
|
+
};
|
|
343
|
+
const expr = () => {
|
|
344
|
+
let q = atom();
|
|
345
|
+
for (;;) {
|
|
346
|
+
const op = keyword(peek());
|
|
347
|
+
if (!op || !COMBINATORS.includes(op))
|
|
348
|
+
return q;
|
|
349
|
+
k++;
|
|
350
|
+
q = { op: op, a: q, b: atom() };
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
const q = expr();
|
|
354
|
+
if (k < ts.length)
|
|
355
|
+
throw new QueryError(`"${text1(ts[k])}" was not expected here; join conditions with and, or, except`, ts[k].at);
|
|
356
|
+
return q;
|
|
357
|
+
}
|
|
358
|
+
/** A name as the text form writes it: bare where that reads back the same, quoted otherwise. */
|
|
359
|
+
function arg(s) {
|
|
360
|
+
return /^[^\s(),"]+$/.test(s) && s !== "selection" ? s : JSON.stringify(s);
|
|
361
|
+
}
|
|
362
|
+
/** The canonical text of a query. */
|
|
363
|
+
export function printQuery(q) {
|
|
364
|
+
switch (q.op) {
|
|
365
|
+
case "all":
|
|
366
|
+
case "selection":
|
|
367
|
+
case "trusted":
|
|
368
|
+
case "external":
|
|
369
|
+
case "sorry":
|
|
370
|
+
case "nonstandard":
|
|
371
|
+
return q.op;
|
|
372
|
+
case "decl":
|
|
373
|
+
return `decl(${q.names.map(arg).join(", ")})`;
|
|
374
|
+
case "key":
|
|
375
|
+
// a target named `targets` is a name, quoted: the bare word is every target
|
|
376
|
+
return `key(${q.of === "targets" ? "targets" : q.of.map((t) => (t === "targets" ? JSON.stringify(t) : arg(t))).join(", ")})`;
|
|
377
|
+
case "tcb":
|
|
378
|
+
case "uses":
|
|
379
|
+
case "usedBy": {
|
|
380
|
+
if (q.of === "selection" || Array.isArray(q.of))
|
|
381
|
+
return `${q.op}(${q.of === "selection" ? "selection" : q.of.map(arg).join(", ")})`;
|
|
382
|
+
// a query of one plain word would read as a name
|
|
383
|
+
const inner = printQuery(q.of);
|
|
384
|
+
return `${q.op}(${PLAIN.includes(q.of.op) ? `(${inner})` : inner})`;
|
|
385
|
+
}
|
|
386
|
+
case "ns":
|
|
387
|
+
case "file":
|
|
388
|
+
return `${q.op}(${arg(q.prefix)})`;
|
|
389
|
+
case "kind":
|
|
390
|
+
return `kind(${q.kinds.join(", ")})`;
|
|
391
|
+
case "status":
|
|
392
|
+
return `status(${q.statuses.join(", ")})`;
|
|
393
|
+
case "advice":
|
|
394
|
+
return `advice(${q.verdicts.join(", ")})`;
|
|
395
|
+
case "and":
|
|
396
|
+
case "or":
|
|
397
|
+
case "except": {
|
|
398
|
+
// read left to right: only a combination on the right needs its parentheses
|
|
399
|
+
const b = COMBINATORS.includes(q.b.op) ? `(${printQuery(q.b)})` : printQuery(q.b);
|
|
400
|
+
return `${printQuery(q.a)} ${q.op} ${b}`;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
/** A query sent as JSON, checked and bounded as its text would be (QUERY_LIMITS); null when it is
|
|
405
|
+
* not one. Text is parsed. */
|
|
406
|
+
export function cleanQuery(x) {
|
|
407
|
+
return typeof x === "string" ? readText(x) : readJson(x);
|
|
408
|
+
}
|
|
409
|
+
function readText(x) {
|
|
410
|
+
try {
|
|
411
|
+
return parseQuery(x);
|
|
412
|
+
}
|
|
413
|
+
catch {
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
/** Why a query's JSON form is not one, or is too large; null when it is fine. */
|
|
418
|
+
export function invalidQuery(x) {
|
|
419
|
+
if (typeof x === "string") {
|
|
420
|
+
try {
|
|
421
|
+
parseQuery(x);
|
|
422
|
+
return null;
|
|
423
|
+
}
|
|
424
|
+
catch (e) {
|
|
425
|
+
return e instanceof QueryError ? e.message : String(e);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
const q = readJson(x, true);
|
|
429
|
+
return typeof q === "string" ? q : null;
|
|
430
|
+
}
|
|
431
|
+
function readJson(x, why = false) {
|
|
432
|
+
// whatever names a text could hold, a JSON string can (an empty one included: `decl("")`); a
|
|
433
|
+
// list longer than a query may name is refused before it is looked at
|
|
434
|
+
const str = (s) => typeof s === "string";
|
|
435
|
+
let large = null;
|
|
436
|
+
const names = (xs) => {
|
|
437
|
+
if (Array.isArray(xs) && xs.length > QUERY_LIMITS.names)
|
|
438
|
+
large = `the query names more than ${QUERY_LIMITS.names.toLocaleString("en")} declarations or prefixes`;
|
|
439
|
+
return Array.isArray(xs) && xs.length > 0 && xs.length <= QUERY_LIMITS.names && xs.every(str);
|
|
440
|
+
};
|
|
441
|
+
// a JSON tree can be as deep as it likes: the walk stops once it has seen more parts than a
|
|
442
|
+
// query may have, so that its own recursion, and everything's after it, stays shallow
|
|
443
|
+
let nodes = 0;
|
|
444
|
+
const walk = (v) => {
|
|
445
|
+
if (!v || typeof v !== "object" || ++nodes > 2 * QUERY_LIMITS.terms + 1)
|
|
446
|
+
return null;
|
|
447
|
+
const o = v;
|
|
448
|
+
switch (o.op) {
|
|
449
|
+
case "all":
|
|
450
|
+
case "selection":
|
|
451
|
+
case "trusted":
|
|
452
|
+
case "external":
|
|
453
|
+
case "sorry":
|
|
454
|
+
case "nonstandard":
|
|
455
|
+
return { op: o.op };
|
|
456
|
+
case "decl":
|
|
457
|
+
return names(o.names) ? { op: "decl", names: [...o.names] } : null;
|
|
458
|
+
case "key":
|
|
459
|
+
if (o.of === "targets")
|
|
460
|
+
return { op: "key", of: "targets" };
|
|
461
|
+
return names(o.of) ? { op: "key", of: [...o.of] } : null;
|
|
462
|
+
case "tcb":
|
|
463
|
+
case "uses":
|
|
464
|
+
case "usedBy": {
|
|
465
|
+
if (o.of === "selection")
|
|
466
|
+
return { op: o.op, of: "selection" };
|
|
467
|
+
if (o.of && typeof o.of === "object" && !Array.isArray(o.of)) {
|
|
468
|
+
const of = walk(o.of);
|
|
469
|
+
return of ? { op: o.op, of } : null;
|
|
470
|
+
}
|
|
471
|
+
return names(o.of) ? { op: o.op, of: [...o.of] } : null;
|
|
472
|
+
}
|
|
473
|
+
case "ns":
|
|
474
|
+
case "file":
|
|
475
|
+
return str(o.prefix) ? { op: o.op, prefix: o.prefix } : null;
|
|
476
|
+
case "kind":
|
|
477
|
+
return Array.isArray(o.kinds) && o.kinds.length && o.kinds.every((k) => KINDS.includes(k)) ? { op: "kind", kinds: [...o.kinds] } : null;
|
|
478
|
+
case "status":
|
|
479
|
+
return Array.isArray(o.statuses) && o.statuses.length && o.statuses.every((s) => STATUSES.includes(s))
|
|
480
|
+
? { op: "status", statuses: [...o.statuses] }
|
|
481
|
+
: null;
|
|
482
|
+
case "advice":
|
|
483
|
+
return Array.isArray(o.verdicts) && o.verdicts.length && o.verdicts.every((s) => VERDICTS.includes(s)) ? { op: "advice", verdicts: [...o.verdicts] } : null;
|
|
484
|
+
case "and":
|
|
485
|
+
case "or":
|
|
486
|
+
case "except": {
|
|
487
|
+
const a = walk(o.a), b = a && walk(o.b);
|
|
488
|
+
return a && b ? { op: o.op, a, b } : null;
|
|
489
|
+
}
|
|
490
|
+
default:
|
|
491
|
+
return null;
|
|
492
|
+
}
|
|
493
|
+
};
|
|
494
|
+
const q = walk(x);
|
|
495
|
+
if (nodes > 2 * QUERY_LIMITS.terms + 1)
|
|
496
|
+
large = `the query has more than ${QUERY_LIMITS.terms.toLocaleString("en")} parts`;
|
|
497
|
+
if (!q)
|
|
498
|
+
return why ? large ?? "it is neither a query's text nor its JSON form" : null;
|
|
499
|
+
const over = beyondLimits(q);
|
|
500
|
+
return over ? (why ? over : null) : q;
|
|
501
|
+
}
|
|
502
|
+
/** Whether two queries say the same: their canonical texts are one. */
|
|
503
|
+
export function sameQuery(a, b) {
|
|
504
|
+
return printQuery(a) === printQuery(b);
|
|
505
|
+
}
|
|
506
|
+
export function chainOf(q) {
|
|
507
|
+
const conditions = [];
|
|
508
|
+
let x = q;
|
|
509
|
+
while (x.op === "and" || x.op === "or" || x.op === "except") {
|
|
510
|
+
conditions.unshift({ op: x.op, q: x.b });
|
|
511
|
+
x = x.a;
|
|
512
|
+
}
|
|
513
|
+
return { base: x, conditions };
|
|
514
|
+
}
|
|
515
|
+
export function fromChain(base, conditions) {
|
|
516
|
+
return conditions.reduce((a, c) => ({ op: c.op, a, b: c.q }), base);
|
|
517
|
+
}
|
|
518
|
+
/** What a view opened from a board holds (Zoom in): `q` among what the board holds — the board's
|
|
519
|
+
* whole scope, not only the conditions it ends with, so that the view holds exactly what was lit
|
|
520
|
+
* there (on a board of a target's review set, what is in the target's TCB). On a board of everything, or one that is
|
|
521
|
+
* `q` already, `q` itself. (Whether `q` holds anything the board leaves out is a question of sets:
|
|
522
|
+
* shared/board.ts, zoomScope.) */
|
|
523
|
+
export function within(q, board) {
|
|
524
|
+
return board.op === "all" || sameQuery(q, board) ? q : { op: "and", a: q, b: board };
|
|
525
|
+
}
|
|
526
|
+
/** Every declaration a query names (its roots and decl lists), once each. */
|
|
527
|
+
export function namesIn(q) {
|
|
528
|
+
const out = new Set();
|
|
529
|
+
const walk = (x) => {
|
|
530
|
+
if (x.op === "decl")
|
|
531
|
+
x.names.forEach((n) => out.add(n));
|
|
532
|
+
else if (x.op === "key" && x.of !== "targets")
|
|
533
|
+
x.of.forEach((n) => out.add(n));
|
|
534
|
+
else if (x.op === "tcb" || x.op === "uses" || x.op === "usedBy") {
|
|
535
|
+
if (Array.isArray(x.of))
|
|
536
|
+
x.of.forEach((n) => out.add(n));
|
|
537
|
+
else if (x.of !== "selection")
|
|
538
|
+
walk(x.of);
|
|
539
|
+
}
|
|
540
|
+
else if (x.op === "and" || x.op === "or" || x.op === "except") {
|
|
541
|
+
walk(x.a);
|
|
542
|
+
walk(x.b);
|
|
543
|
+
}
|
|
544
|
+
};
|
|
545
|
+
walk(q);
|
|
546
|
+
return [...out];
|
|
547
|
+
}
|
|
548
|
+
/** Whether the query reads the review (trust, statuses, advice): its set changes with opinions. */
|
|
549
|
+
export function readsReview(q) {
|
|
550
|
+
if (q.op === "trusted" || q.op === "status" || q.op === "advice" || q.op === "key")
|
|
551
|
+
return true;
|
|
552
|
+
if ((q.op === "tcb" || q.op === "uses" || q.op === "usedBy") && q.of !== "selection" && !Array.isArray(q.of))
|
|
553
|
+
return readsReview(q.of);
|
|
554
|
+
if (q.op === "and" || q.op === "or" || q.op === "except")
|
|
555
|
+
return readsReview(q.a) || readsReview(q.b);
|
|
556
|
+
return false;
|
|
557
|
+
}
|
|
558
|
+
/** The query with the selection replaced by `names`: what is lit now, as a scope (Zoom in). */
|
|
559
|
+
export function withSelection(q, names) {
|
|
560
|
+
switch (q.op) {
|
|
561
|
+
case "selection":
|
|
562
|
+
return { op: "decl", names: [...names] };
|
|
563
|
+
case "tcb":
|
|
564
|
+
case "uses":
|
|
565
|
+
case "usedBy":
|
|
566
|
+
if (q.of === "selection")
|
|
567
|
+
return { op: q.op, of: [...names] };
|
|
568
|
+
return Array.isArray(q.of) ? q : { op: q.op, of: withSelection(q.of, names) };
|
|
569
|
+
case "and":
|
|
570
|
+
case "or":
|
|
571
|
+
case "except":
|
|
572
|
+
return { op: q.op, a: withSelection(q.a, names), b: withSelection(q.b, names) };
|
|
573
|
+
default:
|
|
574
|
+
return q;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
/** A target's review set (ReviewSpec decision 3): what its review counts (decided 2026-10-08,
|
|
578
|
+
* shared/reviewqueue.ts) — its TCB without what is trusted, an untrusted outside declaration
|
|
579
|
+
* included, and the non-standard axioms it rests on through statements, bodies or proofs, which
|
|
580
|
+
* no prefix trusts: `tcb(T) except trusted or (uses(T) and kind(axiom) and nonstandard)`. */
|
|
581
|
+
export function reviewScope(target) {
|
|
582
|
+
const axioms = { op: "and", a: { op: "and", a: { op: "uses", of: [target] }, b: { op: "kind", kinds: ["axiom"] } }, b: { op: "nonstandard" } };
|
|
583
|
+
return fromChain({ op: "tcb", of: [target] }, [
|
|
584
|
+
{ op: "except", q: { op: "trusted" } },
|
|
585
|
+
{ op: "or", q: axioms },
|
|
586
|
+
]);
|
|
587
|
+
}
|
|
588
|
+
/** What the Keys step of Rests on selects (ViewSpec decision 20): what was clicked — a declaration,
|
|
589
|
+
* or the set a cluster holds — and the key declarations in its TCB, of the targets `keys` names
|
|
590
|
+
* (`targets`: every target). That is `decl(X) or (tcb(X) and key(T…))`, written
|
|
591
|
+
* `tcb(X) and (key(T…) or decl(X))`: the same set, since a TCB holds what it starts from, as one
|
|
592
|
+
* part that a selection's `or`s never split (web/src/selection.ts, picksOf). */
|
|
593
|
+
export function keysQuery(from, keys) {
|
|
594
|
+
const self = typeof from === "string" ? { op: "decl", names: [from] } : from;
|
|
595
|
+
return { op: "and", a: { op: "tcb", of: typeof from === "string" ? [from] : from }, b: { op: "or", a: { op: "key", of: keys === "targets" ? keys : [...keys] }, b: self } };
|
|
596
|
+
}
|
|
597
|
+
/** What a query of the Keys step starts from, and whose key declarations it holds; null for any
|
|
598
|
+
* other query. */
|
|
599
|
+
export function readKeys(q) {
|
|
600
|
+
if (q.op !== "and" || q.a.op !== "tcb" || q.b.op !== "or" || q.b.a.op !== "key")
|
|
601
|
+
return null;
|
|
602
|
+
const of = q.a.of;
|
|
603
|
+
const from = of === "selection" ? null : Array.isArray(of) ? (of.length === 1 ? of[0] : null) : of;
|
|
604
|
+
return from !== null && sameQuery(keysQuery(from, q.b.a.of), q) ? { from, keys: q.b.a.of } : null;
|
|
605
|
+
}
|
|
606
|
+
/** Whether the board leaves the outside world out: a condition `except external` on its chain. */
|
|
607
|
+
export function withoutOutside(q) {
|
|
608
|
+
return chainOf(q).conditions.some((c) => c.op === "except" && c.q.op === "external");
|
|
609
|
+
}
|
|
610
|
+
/** The scope with the outside world in (every `except external` taken off its chain) or out (one
|
|
611
|
+
* added at the end): what the External switch did, now a condition of the board. */
|
|
612
|
+
export function setOutside(q, on) {
|
|
613
|
+
const { base, conditions } = chainOf(q);
|
|
614
|
+
const rest = conditions.filter((c) => !(c.op === "except" && c.q.op === "external"));
|
|
615
|
+
return fromChain(base, on ? rest : [...rest, { op: "except", q: { op: "external" } }]);
|
|
616
|
+
}
|
|
617
|
+
// ---------------------------------------------------------------- in words
|
|
618
|
+
const last = (name) => {
|
|
619
|
+
const parts = name.split(".");
|
|
620
|
+
const tail = parts[parts.length - 1];
|
|
621
|
+
return /^\d+$/.test(tail) || tail.length <= 2 ? parts.slice(-2).join(".") : tail;
|
|
622
|
+
};
|
|
623
|
+
const list = (names) => (names.length > 2 ? `${names.slice(0, 2).map(last).join(", ")} +${names.length - 2}` : names.map(last).join(", "));
|
|
624
|
+
const roots = (of) => (of === "selection" ? "the selection" : Array.isArray(of) ? list(of) : describe(of));
|
|
625
|
+
const or = (xs) => (xs.length > 1 ? `${xs.slice(0, -1).join(", ")} or ${xs[xs.length - 1]}` : xs[0]);
|
|
626
|
+
// a Map, not an object: `constructor` is a kind, and an object's own property besides
|
|
627
|
+
const PLURAL = new Map([["theorem", "theorems"], ["def", "definitions"], ["abbrev", "abbreviations"], ["instance", "instances"], ["structure", "structures"], ["class", "classes"], ["inductive", "inductive types"], ["opaque", "opaque constants"], ["axiom", "axioms"], ["notation", "notations"]]);
|
|
628
|
+
/** A set in a few words, for a chip: "TCB of fermatLastTheoremFour", "everything". */
|
|
629
|
+
export function describe(q) {
|
|
630
|
+
// the Keys step's one part (keysQuery), said as what it is rather than as its and and or
|
|
631
|
+
const k = readKeys(q);
|
|
632
|
+
if (k) {
|
|
633
|
+
const what = typeof k.from === "string" ? last(k.from) : describe(k.from);
|
|
634
|
+
const whose = k.keys === "targets" || (k.keys.length === 1 && k.keys[0] === k.from) ? "" : `of ${list(k.keys)} that `;
|
|
635
|
+
return `key declarations ${whose}${what} rests on`;
|
|
636
|
+
}
|
|
637
|
+
switch (q.op) {
|
|
638
|
+
case "all":
|
|
639
|
+
return "everything";
|
|
640
|
+
case "selection":
|
|
641
|
+
return "the selection";
|
|
642
|
+
case "decl":
|
|
643
|
+
return list(q.names);
|
|
644
|
+
case "key":
|
|
645
|
+
return q.of === "targets" ? "key declarations of every target" : `key declarations of ${list(q.of)}`;
|
|
646
|
+
case "tcb":
|
|
647
|
+
return `TCB of ${roots(q.of)}`;
|
|
648
|
+
case "uses":
|
|
649
|
+
return `what ${roots(q.of)} uses`;
|
|
650
|
+
case "usedBy":
|
|
651
|
+
return `what uses ${roots(q.of)}`;
|
|
652
|
+
case "ns":
|
|
653
|
+
return `namespace ${q.prefix}`;
|
|
654
|
+
case "file":
|
|
655
|
+
return `files under ${q.prefix}`;
|
|
656
|
+
case "trusted":
|
|
657
|
+
return "what is trusted";
|
|
658
|
+
case "external":
|
|
659
|
+
return "the outside world";
|
|
660
|
+
case "sorry":
|
|
661
|
+
return "what uses sorry";
|
|
662
|
+
case "nonstandard":
|
|
663
|
+
return "what rests on non-standard axioms";
|
|
664
|
+
case "kind":
|
|
665
|
+
return or(q.kinds.map((k) => PLURAL.get(k) ?? k));
|
|
666
|
+
case "status":
|
|
667
|
+
return or(q.statuses.map((s) => (s === "unreviewed" ? "not yet reviewed" : s)));
|
|
668
|
+
case "advice":
|
|
669
|
+
return `what an agent ${or(q.verdicts.map((v) => (v === "approve" ? "approves" : "comments on")))}`;
|
|
670
|
+
case "and":
|
|
671
|
+
case "or":
|
|
672
|
+
case "except":
|
|
673
|
+
// read left to right, as the text is: a combination on the right in parentheses
|
|
674
|
+
return `${describe(q.a)} ${q.op} ${COMBINATORS.includes(q.b.op) ? `(${describe(q.b)})` : describe(q.b)}`;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
/** A condition in a few words, as its chip reads: "without the outside world", "only theorems". */
|
|
678
|
+
export function describeCondition(c) {
|
|
679
|
+
const q = c.q;
|
|
680
|
+
if (c.op === "except" && q.op === "external")
|
|
681
|
+
return "without the outside world";
|
|
682
|
+
if (c.op === "except" && q.op === "trusted")
|
|
683
|
+
return "except what is trusted";
|
|
684
|
+
if (c.op === "and") {
|
|
685
|
+
if (q.op === "sorry")
|
|
686
|
+
return "only with sorry";
|
|
687
|
+
if (q.op === "nonstandard")
|
|
688
|
+
return "only with non-standard axioms";
|
|
689
|
+
if (q.op === "ns")
|
|
690
|
+
return `only in namespace ${q.prefix}`;
|
|
691
|
+
if (q.op === "file")
|
|
692
|
+
return `only in files under ${q.prefix}`;
|
|
693
|
+
if (q.op === "kind" || q.op === "status" || q.op === "advice" || q.op === "key")
|
|
694
|
+
return `only ${describe(q)}`;
|
|
695
|
+
return `and ${describe(q)}`;
|
|
696
|
+
}
|
|
697
|
+
if (c.op === "except")
|
|
698
|
+
return `except ${describe(q)}`;
|
|
699
|
+
return `or ${describe(q)}`;
|
|
700
|
+
}
|
|
701
|
+
/** The declarations of `ctx.g` a query holds, as a 0/1 array. Names the graph does not hold stand
|
|
702
|
+
* for nothing. */
|
|
703
|
+
export function evaluate(q, ctx) {
|
|
704
|
+
return evaluateIn(q, ctx, new Map());
|
|
705
|
+
}
|
|
706
|
+
/** `evaluate`, with the closures this evaluation has walked so far, by what each is of: the same
|
|
707
|
+
* closure asked twice in one query (`uses(usedBy(X)) or usedBy(X)`) walks the graph once. With
|
|
708
|
+
* QUERY_LIMITS bounding how many a query holds, no query costs more than a few dozen walks. */
|
|
709
|
+
function evaluateIn(q, ctx, walks) {
|
|
710
|
+
const { g, review: d } = ctx;
|
|
711
|
+
const n = g.n;
|
|
712
|
+
const out = new Uint8Array(n);
|
|
713
|
+
const each = (f) => {
|
|
714
|
+
for (let i = 0; i < n; i++)
|
|
715
|
+
if (f(i))
|
|
716
|
+
out[i] = 1;
|
|
717
|
+
return out;
|
|
718
|
+
};
|
|
719
|
+
const idx = (names) => names.map((x) => g.byName.get(x)).filter((i) => i !== undefined);
|
|
720
|
+
const roots = (of) => {
|
|
721
|
+
if (of === "selection")
|
|
722
|
+
return (ctx.selection ?? []).filter((i) => i >= 0 && i < n);
|
|
723
|
+
if (Array.isArray(of))
|
|
724
|
+
return idx(of);
|
|
725
|
+
const set = evaluateIn(of, ctx, walks);
|
|
726
|
+
const out = [];
|
|
727
|
+
for (let i = 0; i < n; i++)
|
|
728
|
+
if (set[i])
|
|
729
|
+
out.push(i);
|
|
730
|
+
return out;
|
|
731
|
+
};
|
|
732
|
+
switch (q.op) {
|
|
733
|
+
case "all":
|
|
734
|
+
return out.fill(1);
|
|
735
|
+
case "selection":
|
|
736
|
+
for (const i of roots("selection"))
|
|
737
|
+
out[i] = 1;
|
|
738
|
+
return out;
|
|
739
|
+
case "decl":
|
|
740
|
+
for (const i of idx(q.names))
|
|
741
|
+
out[i] = 1;
|
|
742
|
+
return out;
|
|
743
|
+
case "key":
|
|
744
|
+
// each target, and what was marked key for it that its review still holds (its Review
|
|
745
|
+
// view's set, shared/reviewqueue.ts): a mark on what has left the review counts for nothing
|
|
746
|
+
for (const t of q.of === "targets" ? Object.keys(d.targets) : q.of) {
|
|
747
|
+
const root = g.byName.get(t);
|
|
748
|
+
if (root === undefined)
|
|
749
|
+
continue;
|
|
750
|
+
out[root] = 1;
|
|
751
|
+
const marks = keyMarks(d, t).map((x) => g.byName.get(x)).filter((i) => i !== undefined);
|
|
752
|
+
if (!marks.length)
|
|
753
|
+
continue;
|
|
754
|
+
const set = evaluateIn(reviewScope(t), ctx, walks);
|
|
755
|
+
for (const i of marks)
|
|
756
|
+
if (set[i])
|
|
757
|
+
out[i] = 1;
|
|
758
|
+
}
|
|
759
|
+
return out;
|
|
760
|
+
case "tcb":
|
|
761
|
+
case "uses":
|
|
762
|
+
case "usedBy": {
|
|
763
|
+
const key = `${q.op}\u0000${q.of === "selection" ? "\u0001" : Array.isArray(q.of) ? JSON.stringify(q.of) : printQuery(q.of)}`;
|
|
764
|
+
let walked = walks.get(key);
|
|
765
|
+
if (!walked) {
|
|
766
|
+
const r = roots(q.of);
|
|
767
|
+
walked = r.length ? g.closure(CLOSURE_MODE[q.op], r) : out;
|
|
768
|
+
walks.set(key, walked);
|
|
769
|
+
}
|
|
770
|
+
// a copy: what combines it below writes into the set it is given
|
|
771
|
+
return walked.slice();
|
|
772
|
+
}
|
|
773
|
+
case "ns":
|
|
774
|
+
return each((i) => under(g.nodes[i].name, q.prefix));
|
|
775
|
+
case "file":
|
|
776
|
+
return each((i) => under(g.nodes[i].module, q.prefix));
|
|
777
|
+
case "trusted":
|
|
778
|
+
return each((i) => trusted(d, g.nodes[i].name, g.nodes[i].module, g.isExt(i)));
|
|
779
|
+
case "external":
|
|
780
|
+
return each((i) => g.isExt(i));
|
|
781
|
+
case "sorry":
|
|
782
|
+
return each((i) => g.axiomsOf(i).includes("sorryAx"));
|
|
783
|
+
case "nonstandard":
|
|
784
|
+
return each((i) => g.axiomsOf(i).some((a) => !STANDARD_AXIOMS.includes(a)));
|
|
785
|
+
case "kind": {
|
|
786
|
+
const ks = new Set(q.kinds);
|
|
787
|
+
return each((i) => ks.has(g.nodes[i].kind));
|
|
788
|
+
}
|
|
789
|
+
case "status": {
|
|
790
|
+
const ss = new Set(q.statuses.map((s) => (s === "unreviewed" ? "none" : s)));
|
|
791
|
+
return each((i) => ss.has(statusOf(d, g.nodes[i].name, g.nodes[i].hash)));
|
|
792
|
+
}
|
|
793
|
+
case "advice": {
|
|
794
|
+
const vs = new Set(q.verdicts);
|
|
795
|
+
return each((i) => {
|
|
796
|
+
const a = adviceOf(d, g.nodes[i].name, g.nodes[i].hash);
|
|
797
|
+
return a !== null && vs.has(a);
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
case "and":
|
|
801
|
+
case "or":
|
|
802
|
+
case "except": {
|
|
803
|
+
const a = evaluateIn(q.a, ctx, walks);
|
|
804
|
+
const b = evaluateIn(q.b, ctx, walks);
|
|
805
|
+
for (let i = 0; i < n; i++)
|
|
806
|
+
a[i] = q.op === "and" ? a[i] & b[i] : q.op === "or" ? a[i] | b[i] : a[i] & (1 - b[i]);
|
|
807
|
+
return a;
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
}
|