@dallaylaen/ski-interpreter 2.1.0 → 2.2.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/CHANGELOG.md +23 -0
- package/README.md +1 -1
- package/bin/ski.js +1 -1
- package/lib/ski-interpreter.cjs.js +2047 -0
- package/lib/ski-interpreter.cjs.js.map +7 -0
- package/lib/ski-interpreter.esm.js +2052 -0
- package/lib/ski-interpreter.esm.js.map +7 -0
- package/package.json +13 -6
- package/types/index.d.ts +3 -7
- package/types/{lib → src}/expr.d.ts +35 -10
- package/types/{lib → src}/extras.d.ts +1 -0
- package/types/{lib → src}/parser.d.ts +6 -4
- package/index.js +0 -16
- package/lib/expr.js +0 -1417
- package/lib/extras.js +0 -140
- package/lib/internal.js +0 -105
- package/lib/parser.js +0 -434
- package/lib/quest.js +0 -431
- /package/types/{lib → src}/internal.d.ts +0 -0
- /package/types/{lib → src}/quest.d.ts +0 -0
package/lib/extras.js
DELETED
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const { Expr } = require('./expr')
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* @desc Extra utilities that do not belong in the core.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* @experimental
|
|
11
|
-
* @desc Look for an expression that matches the predicate,
|
|
12
|
-
* starting with the seed and applying the terms to one another.
|
|
13
|
-
*
|
|
14
|
-
* A predicate returning 0 (or nothing) means "keep looking",
|
|
15
|
-
* a positive number stands for "found",
|
|
16
|
-
* and a negative means "discard this term from further applications".
|
|
17
|
-
*
|
|
18
|
-
* The order of search is from shortest to longest expressions.
|
|
19
|
-
*
|
|
20
|
-
* @param {Expr[]} seed
|
|
21
|
-
* @param {object} options
|
|
22
|
-
* @param {number} [options.depth] - maximum generation to search for
|
|
23
|
-
* @param {number} [options.tries] - maximum number of tries before giving up
|
|
24
|
-
* @param {boolean} [options.infer] - whether to call infer(), default true.
|
|
25
|
-
* @param {number} [options.maxArgs] - arguments in infer()
|
|
26
|
-
* @param {number} [options.max] - step limit in infer()
|
|
27
|
-
* @param {boolean} [options.noskip] - prevents skipping equivalent terms. Always true if infer is false.
|
|
28
|
-
* @param {boolean} [retain] - if true. also add the whole cache to returned value
|
|
29
|
-
* @param {({gen: number, total: number, probed: number, step: boolean}) => void} [options.progress]
|
|
30
|
-
* @param {number} [options.progressInterval] - minimum number of tries between calls to options.progress, default 1000.
|
|
31
|
-
* @param {(e: Expr, props: {}) => number?} predicate
|
|
32
|
-
* @return {{expr?: Expr, total: number, probed: number, gen: number, cache?: Expr[][]}}
|
|
33
|
-
*/
|
|
34
|
-
function search (seed, options, predicate) {
|
|
35
|
-
const {
|
|
36
|
-
depth = 16,
|
|
37
|
-
infer = true,
|
|
38
|
-
progressInterval = 1000,
|
|
39
|
-
} = options;
|
|
40
|
-
const hasSeen = infer && !options.noskip;
|
|
41
|
-
|
|
42
|
-
// cache[i] = ith generation, 0 is empty
|
|
43
|
-
const cache = [[]];
|
|
44
|
-
let total = 0;
|
|
45
|
-
let probed = 0;
|
|
46
|
-
const seen = {};
|
|
47
|
-
|
|
48
|
-
const maybeProbe = term => {
|
|
49
|
-
total++;
|
|
50
|
-
const props = infer ? term.infer({ max: options.max, maxArgs: options.maxArgs }) : null;
|
|
51
|
-
if (hasSeen && props.expr) {
|
|
52
|
-
if (seen[props.expr])
|
|
53
|
-
return { res: -1 };
|
|
54
|
-
seen[props.expr] = true;
|
|
55
|
-
}
|
|
56
|
-
probed++;
|
|
57
|
-
const res = predicate(term, props);
|
|
58
|
-
return { res, props };
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
// sieve through the seed
|
|
62
|
-
for (const term of seed) {
|
|
63
|
-
const { res } = maybeProbe(term);
|
|
64
|
-
if (res > 0)
|
|
65
|
-
return { expr: term, total, probed, gen: 1 };
|
|
66
|
-
else if (res < 0)
|
|
67
|
-
continue;
|
|
68
|
-
|
|
69
|
-
cache[0].push(term);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
let lastProgress;
|
|
73
|
-
|
|
74
|
-
for (let gen = 1; gen < depth; gen++) {
|
|
75
|
-
if (options.progress) {
|
|
76
|
-
options.progress({ gen, total, probed, step: true });
|
|
77
|
-
lastProgress = total;
|
|
78
|
-
}
|
|
79
|
-
for (let i = 0; i < gen; i++) {
|
|
80
|
-
for (const a of cache[gen - i - 1] || []) {
|
|
81
|
-
for (const b of cache[i] || []) {
|
|
82
|
-
if (total >= options.tries)
|
|
83
|
-
return { total, probed, gen, ...(options.retain ? { cache } : {}) };
|
|
84
|
-
if (options.progress && total - lastProgress >= progressInterval) {
|
|
85
|
-
options.progress({ gen, total, probed, step: false });
|
|
86
|
-
lastProgress = total;
|
|
87
|
-
}
|
|
88
|
-
const term = a.apply(b);
|
|
89
|
-
const { res, props } = maybeProbe(term);
|
|
90
|
-
|
|
91
|
-
if (res > 0)
|
|
92
|
-
return { expr: term, total, probed, gen, ...(options.retain ? { cache } : {}) };
|
|
93
|
-
else if (res < 0)
|
|
94
|
-
continue;
|
|
95
|
-
|
|
96
|
-
// if the term is not reducible, it is more likely to be a dead end, so we push it further away
|
|
97
|
-
const offset = infer
|
|
98
|
-
? ((props.expr ? 0 : 3) + (props.dup ? 1 : 0) + (props.proper ? 0 : 1))
|
|
99
|
-
: 0;
|
|
100
|
-
if (!cache[gen + offset])
|
|
101
|
-
cache[gen + offset] = [];
|
|
102
|
-
cache[gen + offset].push(term);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
return { total, probed, gen: depth, ...(options.retain ? { cache } : {}) };
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* @desc Recursively replace all instances of Expr in a data structure with
|
|
113
|
-
* respective string representation using the format() options.
|
|
114
|
-
* Objects of other types and primitive values are eft as is.
|
|
115
|
-
*
|
|
116
|
-
* May be useful for debugging or diagnostic output.
|
|
117
|
-
*
|
|
118
|
-
* @experimental
|
|
119
|
-
*
|
|
120
|
-
* @param {any} obj
|
|
121
|
-
* @param {object} [options] - see Expr.format()
|
|
122
|
-
* @returns {any}
|
|
123
|
-
*/
|
|
124
|
-
function deepFormat (obj, options = {}) {
|
|
125
|
-
if (obj instanceof Expr)
|
|
126
|
-
return obj.format(options);
|
|
127
|
-
if (Array.isArray(obj))
|
|
128
|
-
return obj.map(deepFormat);
|
|
129
|
-
if (typeof obj !== 'object' || obj === null || obj.constructor !== Object)
|
|
130
|
-
return obj;
|
|
131
|
-
|
|
132
|
-
// default = plain object
|
|
133
|
-
const out = {};
|
|
134
|
-
for (const key in obj)
|
|
135
|
-
out[key] = deepFormat(obj[key]);
|
|
136
|
-
|
|
137
|
-
return out;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
module.exports = { search, deepFormat };
|
package/lib/internal.js
DELETED
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
class Tokenizer {
|
|
2
|
-
/**
|
|
3
|
-
* @desc Create a tokenizer that splits strings into tokens according to the given terms.
|
|
4
|
-
* The terms are interpreted as regular expressions, and are sorted by length
|
|
5
|
-
* to ensure that longer matches are preferred over shorter ones.
|
|
6
|
-
* @param {...string|RegExp} terms
|
|
7
|
-
*/
|
|
8
|
-
constructor (...terms) {
|
|
9
|
-
const src = '$|(\\s+)|' + terms
|
|
10
|
-
.map(s => '(?:' + s + ')')
|
|
11
|
-
.sort((a, b) => b.length - a.length)
|
|
12
|
-
.join('|');
|
|
13
|
-
this.rex = new RegExp(src, 'gys');
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* @desc Split the given string into tokens according to the terms specified in the constructor.
|
|
18
|
-
* @param {string} str
|
|
19
|
-
* @return {string[]}
|
|
20
|
-
*/
|
|
21
|
-
split (str) {
|
|
22
|
-
this.rex.lastIndex = 0;
|
|
23
|
-
const list = [...str.matchAll(this.rex)];
|
|
24
|
-
|
|
25
|
-
// did we parse everything?
|
|
26
|
-
const eol = list.pop();
|
|
27
|
-
const last = eol?.index ?? 0;
|
|
28
|
-
|
|
29
|
-
if (last !== str.length) {
|
|
30
|
-
throw new Error('Unknown tokens at pos ' + last + '/' + str.length
|
|
31
|
-
+ ' starting with ' + str.substring(last));
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// skip whitespace
|
|
35
|
-
return list.filter(x => x[1] === undefined).map(x => x[0]);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const tokRestrict = new Tokenizer('[-=+]', '[A-Z]', '\\b[a-z_][a-z_0-9]*\\b');
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* @desc Add ot remove tokens from a set according to a spec string.
|
|
43
|
-
* The spec string is a sequence of tokens, with each group optionally prefixed
|
|
44
|
-
* by one of the operators '=', '+', or '-'.
|
|
45
|
-
* The '=' operator resets the set to contain only the following token(s).
|
|
46
|
-
* @param {Set<string>} set
|
|
47
|
-
* @param {string} [spec]
|
|
48
|
-
* @returns {Set<string>}
|
|
49
|
-
*/
|
|
50
|
-
function restrict (set, spec) {
|
|
51
|
-
if (!spec)
|
|
52
|
-
return set;
|
|
53
|
-
let out = new Set([...set]);
|
|
54
|
-
const act = {
|
|
55
|
-
'=': sym => { out = new Set([sym]); mode = '+'; },
|
|
56
|
-
'+': sym => { out.add(sym); },
|
|
57
|
-
'-': sym => { out.delete(sym); },
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
let mode = '=';
|
|
61
|
-
for (const sym of tokRestrict.split(spec)) {
|
|
62
|
-
if (act[sym])
|
|
63
|
-
mode = sym;
|
|
64
|
-
else
|
|
65
|
-
act[mode](sym);
|
|
66
|
-
}
|
|
67
|
-
return out;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
class ActionWrapper {
|
|
71
|
-
/**
|
|
72
|
-
* @template T
|
|
73
|
-
* @param {T} value
|
|
74
|
-
* @param {string} action
|
|
75
|
-
*/
|
|
76
|
-
constructor (value, action) {
|
|
77
|
-
this.value = value;
|
|
78
|
-
this.action = action;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* @private
|
|
84
|
-
* @template T
|
|
85
|
-
* @param {T|ActionWrapper<T>} value
|
|
86
|
-
* @returns {[T?, string|undefined]}
|
|
87
|
-
*/
|
|
88
|
-
function unwrap (value) {
|
|
89
|
-
if (value instanceof ActionWrapper)
|
|
90
|
-
return [value.value ?? undefined, value.action];
|
|
91
|
-
return [value ?? undefined, undefined];
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
*
|
|
96
|
-
* @private
|
|
97
|
-
* @template T
|
|
98
|
-
* @param {string} action
|
|
99
|
-
* @returns {function(T): ActionWrapper<T>}
|
|
100
|
-
*/
|
|
101
|
-
function prepareWrapper (action) {
|
|
102
|
-
return value => new ActionWrapper(value, action);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
module.exports = { Tokenizer, restrict, unwrap, prepareWrapper };
|
package/lib/parser.js
DELETED
|
@@ -1,434 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Combinatory logic simulator
|
|
3
|
-
*/
|
|
4
|
-
'use strict';
|
|
5
|
-
|
|
6
|
-
const { Tokenizer, restrict } = require('./internal');
|
|
7
|
-
const classes = require('./expr');
|
|
8
|
-
|
|
9
|
-
const { Expr, Named, Native, Alias, FreeVar, Lambda, Church } = classes;
|
|
10
|
-
const { native, declare } = Expr;
|
|
11
|
-
|
|
12
|
-
class Empty extends Expr {
|
|
13
|
-
apply (...args) {
|
|
14
|
-
return args.length ? args.shift().apply(...args) : this;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
postParse () {
|
|
18
|
-
throw new Error('Attempt to use empty expression () as a term');
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
class PartialLambda extends Empty {
|
|
23
|
-
// TODO mutable! rewrite ro when have time
|
|
24
|
-
constructor (term, known = {}) {
|
|
25
|
-
super();
|
|
26
|
-
this.impl = new Empty();
|
|
27
|
-
if (term instanceof FreeVar)
|
|
28
|
-
this.terms = [term];
|
|
29
|
-
else if (term instanceof PartialLambda) {
|
|
30
|
-
if (!(term.impl instanceof FreeVar))
|
|
31
|
-
throw new Error('Expected FreeVar->...->FreeVar->Expr');
|
|
32
|
-
this.terms = [...term.terms, term.impl];
|
|
33
|
-
} else
|
|
34
|
-
throw new Error('Expected FreeVar or PartialLambda');
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
apply (term, ...tail) {
|
|
38
|
-
if (term === null || tail.length !== 0 )
|
|
39
|
-
throw new Error('bad syntax in partial lambda expr');
|
|
40
|
-
this.impl = this.impl.apply(term);
|
|
41
|
-
return this;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
postParse () {
|
|
45
|
-
return new Lambda(this.terms, this.impl);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// uncomment if debugging with prints
|
|
49
|
-
/* toString () {
|
|
50
|
-
return this.terms.join('->') + '->' + (this.impl ?? '???');
|
|
51
|
-
} */
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function postParse (expr) {
|
|
55
|
-
return expr.postParse ? expr.postParse() : expr;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const combChars = new Tokenizer(
|
|
59
|
-
'[()]', '[A-Z]', '[a-z_][a-z_0-9]*', '\\b[0-9]+\\b', '->', '\\+'
|
|
60
|
-
);
|
|
61
|
-
|
|
62
|
-
class SKI {
|
|
63
|
-
/**
|
|
64
|
-
*
|
|
65
|
-
* @param {{
|
|
66
|
-
* allow?: string,
|
|
67
|
-
* numbers?: boolean,
|
|
68
|
-
* lambdas?: boolean,
|
|
69
|
-
* terms?: { [key: string]: Expr|string} | string[],
|
|
70
|
-
* annotate?: boolean,
|
|
71
|
-
* }} [options]
|
|
72
|
-
*/
|
|
73
|
-
constructor (options = {}) {
|
|
74
|
-
this.annotate = options.annotate ?? false;
|
|
75
|
-
this.known = { ...native };
|
|
76
|
-
this.hasNumbers = true;
|
|
77
|
-
this.hasLambdas = true;
|
|
78
|
-
this.allow = new Set(Object.keys(this.known));
|
|
79
|
-
|
|
80
|
-
// Import terms, if any. Omit native ones
|
|
81
|
-
if (Array.isArray(options.terms))
|
|
82
|
-
this.bulkAdd(options.terms);
|
|
83
|
-
else if (options.terms) {
|
|
84
|
-
for (const name in options.terms) {
|
|
85
|
-
// Native terms already handled by allow
|
|
86
|
-
if (!options.terms[name].match(/^Native:/))
|
|
87
|
-
this.add(name, options.terms[name]);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Finally, impose restrictions
|
|
92
|
-
// We must do it after recreating terms, or else terms reliant on forbidden terms will fail
|
|
93
|
-
this.hasNumbers = options.numbers ?? true;
|
|
94
|
-
this.hasLambdas = options.lambdas ?? true;
|
|
95
|
-
if (options.allow)
|
|
96
|
-
this.restrict(options.allow);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* @desc Declare a new term
|
|
101
|
-
* If the first argument is an Alias, it is added as is.
|
|
102
|
-
* Otherwise, a new Alias or Native term (depending on impl type) is created.
|
|
103
|
-
* If note is not provided and this.annotate is true, an automatic note is generated.
|
|
104
|
-
*
|
|
105
|
-
* If impl is a function, it should have signature (Expr) => ... => Expr
|
|
106
|
-
* (see typedef Partial at top of expr.js)
|
|
107
|
-
*
|
|
108
|
-
* @example ski.add('T', 'S(K(SI))K', 'swap combinator')
|
|
109
|
-
* @example ski.add( ski.parse('T = S(K(SI))K') ) // ditto but one-arg form
|
|
110
|
-
* @example ski.add('T', x => y => y.apply(x), 'swap combinator') // heavy artillery
|
|
111
|
-
* @example ski.add('Y', function (f) { return f.apply(this.apply(f)); }, 'Y combinator')
|
|
112
|
-
*
|
|
113
|
-
* @param {Alias|String} term
|
|
114
|
-
* @param {String|Expr|function(Expr):Partial} [impl]
|
|
115
|
-
* @param {String} [note]
|
|
116
|
-
* @return {SKI} chainable
|
|
117
|
-
*/
|
|
118
|
-
add (term, impl, note ) {
|
|
119
|
-
term = this._named(term, impl);
|
|
120
|
-
|
|
121
|
-
if (this.annotate && note === undefined) {
|
|
122
|
-
const guess = term.infer();
|
|
123
|
-
if (guess.expr)
|
|
124
|
-
note = guess.expr.format({ terse: true, html: true, lambda: ['', ' ↦ ', ''] });
|
|
125
|
-
}
|
|
126
|
-
if (note !== undefined)
|
|
127
|
-
term.note = note;
|
|
128
|
-
|
|
129
|
-
if (this.known[term.name])
|
|
130
|
-
this.known[term.name].outdated = true;
|
|
131
|
-
this.known[term.name] = term;
|
|
132
|
-
this.allow.add(term.name);
|
|
133
|
-
|
|
134
|
-
return this;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
_named (term, impl) {
|
|
138
|
-
if (term instanceof Alias)
|
|
139
|
-
return new Alias(term.name, term.impl, { canonize: true });
|
|
140
|
-
if (typeof term !== 'string')
|
|
141
|
-
throw new Error('add(): term must be an Alias or a string');
|
|
142
|
-
if (impl === undefined)
|
|
143
|
-
throw new Error('add(): impl must be provided when term is a string');
|
|
144
|
-
if (typeof impl === 'string')
|
|
145
|
-
return new Alias(term, this.parse(impl), { canonize: true });
|
|
146
|
-
if (impl instanceof Expr)
|
|
147
|
-
return new Alias(term, impl, { canonize: true });
|
|
148
|
-
if (typeof impl === 'function')
|
|
149
|
-
return new Native(term, impl);
|
|
150
|
-
// idk what this is
|
|
151
|
-
throw new Error('add(): impl must be an Expr, a string, or a function with a signature Expr => ... => Expr');
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
maybeAdd (name, impl) {
|
|
155
|
-
if (this.known[name])
|
|
156
|
-
this.allow.add(name);
|
|
157
|
-
else
|
|
158
|
-
this.add(name, impl);
|
|
159
|
-
return this;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* @desc Declare and remove multiple terms at once
|
|
164
|
-
* term=impl adds term
|
|
165
|
-
* term= removes term
|
|
166
|
-
* @param {string[]]} list
|
|
167
|
-
* @return {SKI} chainable
|
|
168
|
-
*/
|
|
169
|
-
bulkAdd (list) {
|
|
170
|
-
for (const item of list) {
|
|
171
|
-
const m = item.match(/^([A-Z]|[a-z][a-z_0-9]*)\s*=\s*(.*)$/s);
|
|
172
|
-
// TODO check all declarations before applying any (but we might need earlier terms for parsing later ones)
|
|
173
|
-
if (!m)
|
|
174
|
-
throw new Error('bulkAdd: invalid declaration: ' + item);
|
|
175
|
-
if (m[2] === '')
|
|
176
|
-
this.remove(m[1]);
|
|
177
|
-
else
|
|
178
|
-
this.add(m[1], this.parse(m[2]));
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
return this;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Restrict the interpreter to given terms. Terms prepended with '+' will be added
|
|
186
|
-
* and terms preceeded with '-' will be removed.
|
|
187
|
-
* @example ski.restrict('SK') // use the basis
|
|
188
|
-
* @example ski.restrict('+I') // allow I now
|
|
189
|
-
* @example ski.restrict('-SKI +BCKW' ); // switch basis
|
|
190
|
-
* @example ski.restrict('-foo -bar'); // forbid some user functions
|
|
191
|
-
* @param {string} spec
|
|
192
|
-
* @return {SKI} chainable
|
|
193
|
-
*/
|
|
194
|
-
restrict (spec) {
|
|
195
|
-
this.allow = restrict(this.allow, spec);
|
|
196
|
-
return this;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
*
|
|
201
|
-
* @param {string} spec
|
|
202
|
-
* @return {string}
|
|
203
|
-
*/
|
|
204
|
-
showRestrict (spec = '+') {
|
|
205
|
-
const out = [];
|
|
206
|
-
let prevShort = true;
|
|
207
|
-
for (const term of [...restrict(this.allow, spec)].sort()) {
|
|
208
|
-
const nextShort = term.match(/^[A-Z]$/);
|
|
209
|
-
if (out.length && !(prevShort && nextShort))
|
|
210
|
-
out.push(' ');
|
|
211
|
-
out.push(term);
|
|
212
|
-
prevShort = nextShort;
|
|
213
|
-
}
|
|
214
|
-
return out.join('');
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
*
|
|
219
|
-
* @param {String} name
|
|
220
|
-
* @return {SKI}
|
|
221
|
-
*/
|
|
222
|
-
remove (name) {
|
|
223
|
-
this.known[name].outdated = true;
|
|
224
|
-
delete this.known[name];
|
|
225
|
-
this.allow.delete(name);
|
|
226
|
-
return this;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
/**
|
|
230
|
-
*
|
|
231
|
-
* @return {{[key:string]: Native|Alias}}
|
|
232
|
-
*/
|
|
233
|
-
getTerms () {
|
|
234
|
-
const out = {};
|
|
235
|
-
for (const name of Object.keys(this.known)) {
|
|
236
|
-
if (this.allow.has(name))
|
|
237
|
-
out[name] = this.known[name];
|
|
238
|
-
}
|
|
239
|
-
return out;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
/**
|
|
243
|
-
* Export term declarations for use in bulkAdd().
|
|
244
|
-
* @returns {string[]}
|
|
245
|
-
*/
|
|
246
|
-
declare () {
|
|
247
|
-
// TODO accept argument to declare specific terms only
|
|
248
|
-
return declare(this.getTerms());
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
/**
|
|
252
|
-
*
|
|
253
|
-
* @param {string} source
|
|
254
|
-
* @param {Object} [options]
|
|
255
|
-
* @param {{[keys: string]: Expr}} [options.env]
|
|
256
|
-
* @param {any} [options.scope]
|
|
257
|
-
* @param {boolean} [options.numbers]
|
|
258
|
-
* @param {boolean} [options.lambdas]
|
|
259
|
-
* @param {string} [options.allow]
|
|
260
|
-
* @return {Expr}
|
|
261
|
-
*/
|
|
262
|
-
parse (source, options = {}) {
|
|
263
|
-
if (typeof source !== 'string')
|
|
264
|
-
throw new Error('parse: source must be a string, got ' + typeof source);
|
|
265
|
-
|
|
266
|
-
const lines = source.replace(/\/\/[^\n]*$/gm, '')
|
|
267
|
-
.split(/\s*;[\s;]*/).filter( s => s.match(/\S/));
|
|
268
|
-
|
|
269
|
-
const jar = { ...options.env };
|
|
270
|
-
|
|
271
|
-
let expr = new Empty();
|
|
272
|
-
for (const item of lines) {
|
|
273
|
-
const [_, save, str] = item.match(/^(?:\s*([A-Z]|[a-z][a-z_0-9]*)\s*=\s*)?(.*)$/s);
|
|
274
|
-
|
|
275
|
-
if (expr instanceof Alias)
|
|
276
|
-
expr.outdated = true;
|
|
277
|
-
expr = (str === '' && save !== undefined)
|
|
278
|
-
? new FreeVar(save, options.scope)
|
|
279
|
-
: this.parseLine(str, jar, options);
|
|
280
|
-
|
|
281
|
-
if (save !== undefined) {
|
|
282
|
-
if (jar[save] !== undefined)
|
|
283
|
-
throw new Error('Attempt to redefine a known term: ' + save);
|
|
284
|
-
expr = maybeAlias(save, expr);
|
|
285
|
-
jar[save] = expr;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// console.log('parsed line:', item, '; got:', expr,'; jar now: ', jar);
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
expr.context = {
|
|
292
|
-
env: jar, // also contains pre-parsed terms
|
|
293
|
-
scope: options.scope,
|
|
294
|
-
src: source,
|
|
295
|
-
parser: this,
|
|
296
|
-
};
|
|
297
|
-
return expr;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
/**
|
|
301
|
-
*
|
|
302
|
-
* @param {String} source S(KI)I
|
|
303
|
-
* @param {{[keys: string]: Expr}} env
|
|
304
|
-
* @param {Object} [options]
|
|
305
|
-
* @param {{[keys: string]: Expr}} [options.env] - unused, see 'env' argument
|
|
306
|
-
* @param {any} [options.scope]
|
|
307
|
-
* @param {boolean} [options.numbers]
|
|
308
|
-
* @param {boolean} [options.lambdas]
|
|
309
|
-
* @param {string} [options.allow]
|
|
310
|
-
* @return {Expr} parsed expression
|
|
311
|
-
*/
|
|
312
|
-
parseLine (source, env = {}, options = {}) {
|
|
313
|
-
const opt = {
|
|
314
|
-
numbers: options.numbers ?? this.hasNumbers,
|
|
315
|
-
lambdas: options.lambdas ?? this.hasLambdas,
|
|
316
|
-
allow: restrict(this.allow, options.allow),
|
|
317
|
-
};
|
|
318
|
-
// make sure '+' usage is in sync with numerals
|
|
319
|
-
opt.numbers ? opt.allow.add('+') : opt.allow.delete('+');
|
|
320
|
-
|
|
321
|
-
const tokens = combChars.split(source);
|
|
322
|
-
|
|
323
|
-
const empty = new Empty();
|
|
324
|
-
/** @type {Expr[]} */
|
|
325
|
-
const stack = [empty];
|
|
326
|
-
const context = options.scope || SKI; // default is global unbound vars
|
|
327
|
-
|
|
328
|
-
// TODO each token should carry along its position in source
|
|
329
|
-
for (const c of tokens) {
|
|
330
|
-
// console.log("parseLine: found "+c+"; stack =", stack.join(", "));
|
|
331
|
-
if (c === '(')
|
|
332
|
-
stack.push(empty);
|
|
333
|
-
else if (c === ')') {
|
|
334
|
-
if (stack.length < 2)
|
|
335
|
-
throw new Error('unbalanced input: extra closing parenthesis' + source);
|
|
336
|
-
const x = postParse(stack.pop());
|
|
337
|
-
const f = stack.pop();
|
|
338
|
-
stack.push(f.apply(x));
|
|
339
|
-
} else if (c === '->') {
|
|
340
|
-
if (!opt.lambdas)
|
|
341
|
-
throw new Error('Lambdas not supported, allow them explicitly');
|
|
342
|
-
stack.push(new PartialLambda(stack.pop(), env));
|
|
343
|
-
} else if (c.match(/^[0-9]+$/)) {
|
|
344
|
-
if (!opt.numbers)
|
|
345
|
-
throw new Error('Church numbers not supported, allow them explicitly');
|
|
346
|
-
const f = stack.pop();
|
|
347
|
-
stack.push(f.apply(new Church(c)));
|
|
348
|
-
} else {
|
|
349
|
-
const f = stack.pop();
|
|
350
|
-
if (!env[c] && this.known[c] && !opt.allow.has(c)) {
|
|
351
|
-
throw new Error('Term \'' + c + '\' is not in the restricted set '
|
|
352
|
-
+ [...opt.allow].sort().join(' '));
|
|
353
|
-
}
|
|
354
|
-
// look in temp vars first, then in known terms, then fallback to creating free var
|
|
355
|
-
const x = env[c] ?? this.known[c] ?? (env[c] = new FreeVar(c, context));
|
|
356
|
-
stack.push(f.apply(x));
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
if (stack.length !== 1) {
|
|
361
|
-
throw new Error('unbalanced input: missing '
|
|
362
|
-
+ (stack.length - 1) + ' closing parenthesis:' + source);
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
return postParse(stack.pop());
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
toJSON () {
|
|
369
|
-
return {
|
|
370
|
-
version: '1.1.1', // set to incremented package.json version whenever SKI serialization changes
|
|
371
|
-
allow: this.showRestrict('+'),
|
|
372
|
-
numbers: this.hasNumbers,
|
|
373
|
-
lambdas: this.hasLambdas,
|
|
374
|
-
annotate: this.annotate,
|
|
375
|
-
terms: this.declare(),
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
function maybeAlias (name, expr) {
|
|
381
|
-
if (expr instanceof Named && expr.name === name)
|
|
382
|
-
return expr;
|
|
383
|
-
return new Alias(name, expr);
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
// Create shortcuts for common terms
|
|
387
|
-
|
|
388
|
-
SKI.classes = classes;
|
|
389
|
-
|
|
390
|
-
/**
|
|
391
|
-
* @desc Create a proxy object that generates variables on demand,
|
|
392
|
-
* with names corresponding to the property accessed.
|
|
393
|
-
* Different invocations will return distinct variables,
|
|
394
|
-
* even if with the same name.
|
|
395
|
-
*
|
|
396
|
-
*
|
|
397
|
-
* @example const {x, y, z} = SKI.vars();
|
|
398
|
-
* x.name; // 'x'
|
|
399
|
-
* x instanceof FreeVar; // true
|
|
400
|
-
* x.apply(y).apply(z); // x(y)(z)
|
|
401
|
-
*
|
|
402
|
-
* @return {{[key: string]: FreeVar}}
|
|
403
|
-
*/
|
|
404
|
-
|
|
405
|
-
SKI.vars = function (context = {}) {
|
|
406
|
-
const cache = {};
|
|
407
|
-
return new Proxy({}, {
|
|
408
|
-
get: (target, name) => {
|
|
409
|
-
if (!(name in cache))
|
|
410
|
-
cache[name] = new FreeVar(name, context);
|
|
411
|
-
return cache[name];
|
|
412
|
-
}
|
|
413
|
-
});
|
|
414
|
-
};
|
|
415
|
-
|
|
416
|
-
/**
|
|
417
|
-
* Convert a number to Church encoding
|
|
418
|
-
* @param {number} n
|
|
419
|
-
* @return {Church}
|
|
420
|
-
*/
|
|
421
|
-
SKI.church = n => new Church(n);
|
|
422
|
-
|
|
423
|
-
/**
|
|
424
|
-
*
|
|
425
|
-
* @type {{[key: string]: Native}}
|
|
426
|
-
*/
|
|
427
|
-
|
|
428
|
-
for (const name in native)
|
|
429
|
-
SKI[name] = native[name];
|
|
430
|
-
SKI.native = native;
|
|
431
|
-
SKI.declare = declare;
|
|
432
|
-
SKI.control = Expr.control;
|
|
433
|
-
|
|
434
|
-
module.exports = { SKI };
|