@josd/eyecode 0.0.0-stage → 0.1.4

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/src/cli.js ADDED
@@ -0,0 +1,126 @@
1
+ // The eyecode command line.
2
+ import { run, unusedClauses } from './engine.js';
3
+ import { loadSources } from './lang.js';
4
+ import { checkProof, checkReport, publicReport } from './proof.js';
5
+ import { EyecodeError } from './terms.js';
6
+ import { VERSION } from './version.js';
7
+
8
+ export const HELP = `Usage: eyecode [OPTION ...] [FILE ...]
9
+ Programs are JavaScript; forward rules run to a fixpoint.
10
+ --proof Print the claims with clause(...) and step(...) records
11
+ --check-proof FILE Print a C1-C7 check report of a proof (- for stdin)
12
+ --json Print the check report as JSON instead
13
+ --goal GOAL Ask a backward goal after forward reasoning; with
14
+ --check-proof, the goal the proof answers
15
+ --strict-proof Reject proofs relying on absence or collection
16
+ --unused List the clauses that make no difference to the conclusions
17
+ --stats Print reasoning statistics to stderr
18
+ --max-depth N Bound backward recursion (default 1000000)
19
+ --max-iterations N Bound forward rounds per stratum (default 1000)
20
+ --max-inferences N Bound reasoning work (default 1000000)
21
+ --version Print the version
22
+ --help Print this help
23
+ Source defaults to stdin; multiple files form one program.
24
+ Exit codes: 0 success, 1 error, 2 invalid proof, 65 contradiction.
25
+ `;
26
+
27
+ const LIMITS = { '--max-depth': 'maxDepth', '--max-iterations': 'maxIterations', '--max-inferences': 'maxInferences' };
28
+
29
+ class UsageError extends Error {}
30
+
31
+ function readSource(path, io) {
32
+ return path === '-' ? io.stdin() : io.readFile(path);
33
+ }
34
+
35
+ // Run the command line; returns the exit code. io writes and reads: stdout,
36
+ // stderr, stdin and readFile, and may check syntax (see node.js).
37
+ export function main(argv, io) {
38
+ const stdout = (text) => io.stdout(text);
39
+ const stderr = (text) => io.stderr(text);
40
+ try {
41
+ const files = [];
42
+ const options = {};
43
+ const goals = [];
44
+ let proofFile = null;
45
+ let stats = false;
46
+ let strictProof = false;
47
+ let asJson = false;
48
+ let unused = false;
49
+ for (let i = 0; i < argv.length; i++) {
50
+ const arg = argv[i];
51
+ if (arg === '--help' || arg === '-h') {
52
+ stdout(HELP);
53
+ return 0;
54
+ }
55
+ if (arg === '--version' || arg === '-v') {
56
+ stdout(`eyecode v${VERSION}\n`);
57
+ return 0;
58
+ }
59
+ if (arg === '--proof' || arg === '-p') {
60
+ options.proof = true;
61
+ } else if (arg === '--stats') {
62
+ stats = true;
63
+ } else if (arg === '--strict-proof') {
64
+ strictProof = true;
65
+ } else if (arg === '--json') {
66
+ asJson = true;
67
+ } else if (arg === '--unused') {
68
+ unused = true;
69
+ } else if (arg === '--check-proof' || arg === '--goal' || Object.hasOwn(LIMITS, arg)) {
70
+ i++;
71
+ if (i >= argv.length) throw new UsageError(`${arg} needs a value`);
72
+ const value = argv[i];
73
+ if (arg === '--check-proof') {
74
+ proofFile = value;
75
+ } else if (arg === '--goal') {
76
+ goals.push(value);
77
+ } else {
78
+ const number = /^[0-9]+$/.test(value.trim()) ? Number(value.trim()) : 0;
79
+ if (number < 1) throw new UsageError(`${arg} needs a positive integer`);
80
+ options[LIMITS[arg]] = number;
81
+ }
82
+ } else if (arg.startsWith('-') && arg !== '-') {
83
+ throw new UsageError(`unknown option ${arg}`);
84
+ } else {
85
+ files.push(arg);
86
+ }
87
+ }
88
+ if (asJson && proofFile === null) throw new UsageError('--json requires --check-proof');
89
+ if (strictProof && proofFile === null) throw new UsageError('--strict-proof requires --check-proof');
90
+ if (proofFile !== null && options.proof) throw new UsageError('--check-proof cannot be combined with --proof');
91
+ if (unused && (proofFile !== null || options.proof)) {
92
+ throw new UsageError('--unused cannot be combined with --proof or --check-proof');
93
+ }
94
+ if (proofFile === '-' && (!files.length || files.includes('-'))) {
95
+ throw new UsageError('stdin holds the proof; name the program as files');
96
+ }
97
+ if (!files.length) files.push('-');
98
+ if (files.filter((file) => file === '-').length > 1) throw new UsageError('stdin can only be read once');
99
+ const program = loadSources(files.map((file) => [readSource(file, io), file === '-' ? '<stdin>' : file]), io);
100
+ if (proofFile !== null) {
101
+ const document = readSource(proofFile, io);
102
+ const report = checkProof(program, document, { goals, allowTrusted: !strictProof });
103
+ stdout(asJson ? `${JSON.stringify(publicReport(report), null, 2)}\n` : checkReport(report));
104
+ // An invalid proof is a result, not an error: it has its own exit code.
105
+ return report.valid ? 0 : 2;
106
+ }
107
+ if (unused) {
108
+ stdout(unusedClauses(program, { goals, ...options }));
109
+ return 0;
110
+ }
111
+ const result = run(program, { goals, ...options });
112
+ stdout(result.stdout);
113
+ if (stats) stderr(`${JSON.stringify(result.stats)}\n`);
114
+ return result.haltCode || 0;
115
+ } catch (error) {
116
+ if (error instanceof UsageError || error instanceof EyecodeError) {
117
+ stderr(`eyecode: ${error.message}\n`);
118
+ return 1;
119
+ }
120
+ if (error && (error.code === 'ENOENT' || error.code === 'EISDIR' || error.code === 'EACCES')) {
121
+ stderr(`eyecode: ${error.message}\n`);
122
+ return 1;
123
+ }
124
+ throw error;
125
+ }
126
+ }
package/src/common.js ADDED
@@ -0,0 +1,120 @@
1
+ // Helpers shared by the engine and the proof checker.
2
+ import { Struct, Var, freshTerm } from './terms.js';
3
+ import { write } from './writer.js';
4
+
5
+ // The canonical spelling of a term, which also serves as its identity.
6
+ export function text(term, env = null) {
7
+ return write(term, env);
8
+ }
9
+
10
+ // Rename a clause apart: head and body share one set of fresh variables,
11
+ // listed in the order the clause is written (a forward rule's body first).
12
+ export function freshClause(clause, suffix) {
13
+ const renamed = renaming(clause);
14
+ if (renamed === null) {
15
+ const names = new Map();
16
+ let head;
17
+ let body;
18
+ if (clause.forward) {
19
+ body = clause.body.map((goal) => freshTerm(goal, suffix, names));
20
+ head = freshTerm(clause.head, suffix, names);
21
+ } else {
22
+ head = freshTerm(clause.head, suffix, names);
23
+ body = clause.body.map((goal) => freshTerm(goal, suffix, names));
24
+ }
25
+ return [head, body, names];
26
+ }
27
+ const tag = String(suffix);
28
+ const values = renamed.prefixes.map((prefix) => new Var(prefix + tag));
29
+ let names = new Map(renamed.names.map((name, i) => [name, values[i]]));
30
+ if (renamed.written !== null) names = new Map(renamed.written.map((name) => [name, names.get(name)]));
31
+ return [renamed.head(values), renamed.body(values), names];
32
+ }
33
+
34
+ // How to rename a clause apart quickly, or null while it has been used too
35
+ // seldom to be worth working out: most clauses of a large program are used
36
+ // once, and renaming those directly is cheaper.
37
+ export function renaming(clause) {
38
+ let renamed = clause.renaming;
39
+ if (renamed === null) {
40
+ clause.uses++;
41
+ if (clause.uses < WORK_OUT_AFTER) return null;
42
+ renamed = clause.renaming = workOut(clause);
43
+ }
44
+ return renamed;
45
+ }
46
+
47
+ const WORK_OUT_AFTER = 3;
48
+
49
+ function collectNames(terms, index) {
50
+ for (const term of terms) {
51
+ const pending = [term];
52
+ while (pending.length) {
53
+ const item = pending.pop();
54
+ if (item instanceof Var) {
55
+ if (!index.has(item.name)) index.set(item.name, index.size);
56
+ } else if (item instanceof Struct) {
57
+ for (let i = item.args.length - 1; i >= 0; i--) pending.push(item.args[i]);
58
+ }
59
+ }
60
+ }
61
+ }
62
+
63
+ // The clause's variables are numbered in order of first occurrence, head
64
+ // first; head(values) and body(values) build the renamed head and body from
65
+ // fresh variables in that order, the first headCount of them sufficing for
66
+ // the head. Subterms without variables are shared.
67
+ function workOut(clause) {
68
+ const index = new Map();
69
+ collectNames([clause.head], index);
70
+ const headCount = index.size;
71
+ collectNames(clause.body, index);
72
+ const names = [...index.keys()];
73
+ let written = null;
74
+ if (clause.forward) {
75
+ // A forward rule is written premise first, implies(Body, Head), and its
76
+ // variables are listed in that order.
77
+ const order = new Map();
78
+ collectNames([...clause.body, clause.head], order);
79
+ written = [...order.keys()];
80
+ }
81
+ return {
82
+ names,
83
+ prefixes: names.map((name) => `${name}#`),
84
+ headCount,
85
+ written,
86
+ head: (values) => substitute(clause.head, index, values),
87
+ body: (values) => clause.body.map((goal) => substitute(goal, index, values)),
88
+ };
89
+ }
90
+
91
+ function substitute(term, index, values) {
92
+ if (term instanceof Var) return values[index.get(term.name)];
93
+ if (!(term instanceof Struct)) return term;
94
+ const args = term.args;
95
+ let copied = null;
96
+ for (let position = 0; position < args.length; position++) {
97
+ const arg = args[position];
98
+ const copy = arg instanceof Var || arg instanceof Struct ? substitute(arg, index, values) : arg;
99
+ if (copied === null) {
100
+ if (copy === arg) continue;
101
+ copied = args.slice(0, position);
102
+ }
103
+ copied.push(copy);
104
+ }
105
+ return copied === null ? term : new Struct(term.name, copied);
106
+ }
107
+
108
+ // A key that preserves sharing while forgetting variable names.
109
+ export function variant(term) {
110
+ const names = new Map();
111
+ function visit(t) {
112
+ if (t instanceof Var) {
113
+ if (!names.has(t.name)) names.set(t.name, new Var(`V${names.size}`));
114
+ return names.get(t.name);
115
+ }
116
+ if (t instanceof Struct) return new Struct(t.name, t.args.map(visit));
117
+ return t;
118
+ }
119
+ return text(visit(term));
120
+ }