@dallaylaen/ski-interpreter 1.0.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 +21 -0
- package/README.md +138 -0
- package/bin/ski.js +122 -0
- package/index.js +8 -0
- package/lib/expr.js +1009 -0
- package/lib/parser.js +326 -0
- package/lib/quest.js +311 -0
- package/lib/util.js +74 -0
- package/package.json +46 -0
- package/types/index.d.ts +7 -0
- package/types/lib/expr.d.ts +344 -0
- package/types/lib/parser.d.ts +138 -0
- package/types/lib/quest.d.ts +139 -0
- package/types/lib/util.d.ts +13 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Konstantin Uvarin
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# Simple Kombinator Interpreter
|
|
2
|
+
|
|
3
|
+
This package contains a
|
|
4
|
+
[combinatory logic](https://en.wikipedia.org/wiki/Combinatory_logic)
|
|
5
|
+
and [lambda calculus](https://en.wikipedia.org/wiki/Lambda_calculus)
|
|
6
|
+
parser and interpreter focused on traceability and inspectability.
|
|
7
|
+
|
|
8
|
+
It is written in plain JavaScript (with bolted on TypeScript support)
|
|
9
|
+
and can be used in Node.js or in the browser.
|
|
10
|
+
|
|
11
|
+
# Features:
|
|
12
|
+
|
|
13
|
+
* SKI and BCKW combinators
|
|
14
|
+
* Lambda expressions
|
|
15
|
+
* Church numerals
|
|
16
|
+
* Defining new terms
|
|
17
|
+
* λ ⇆ SKI conversion
|
|
18
|
+
* Comparison of expressions
|
|
19
|
+
* Includes a class for building and executing test cases for combinators
|
|
20
|
+
|
|
21
|
+
# Syntax
|
|
22
|
+
|
|
23
|
+
* Uppercase terms are always single characters and may be lumped together;
|
|
24
|
+
* Lowercase alphanumeric terms may have multiple letters and must therefore be separated by spaces;
|
|
25
|
+
* Whole non-negative numbers are interpreted as Church numerals, e.g. `5 x y` evaluates to `x(x(x(x(x y))))`. They must also be space-separated from other terms;
|
|
26
|
+
* `x y z` is the same as `(x y) z` or `x(y)(z)` but **not** `x (y z)`;
|
|
27
|
+
* Unknown terms are assumed to be free variables;
|
|
28
|
+
* Lambda terms are written as `x->y->z->expr`, which is equivalent to
|
|
29
|
+
`x->(y->(z->expr))` (aka right associative). Free variables in a lambda expression ~~stay in Vegas~~ are isolated from terms with the same name outside it;
|
|
30
|
+
* X = y z defines a new term.
|
|
31
|
+
|
|
32
|
+
## Starting combinators:
|
|
33
|
+
|
|
34
|
+
* `I x ↦ x` (identity);
|
|
35
|
+
* `K x y ↦ x` (constant);
|
|
36
|
+
* `S x y z ↦ x z (y z)` (fusion);
|
|
37
|
+
* `B x y z ↦ x (y z)` (composition);
|
|
38
|
+
* `C x y z ↦ y x z` (swapping);
|
|
39
|
+
* `W x y z ↦ x (y z)` (duplication);
|
|
40
|
+
|
|
41
|
+
# Execution strategy
|
|
42
|
+
|
|
43
|
+
Applications and native terms use normal strategy, i.e. the first term in the tree
|
|
44
|
+
that has enough arguments is executed and the step ends there.
|
|
45
|
+
|
|
46
|
+
Lambda terms are lazy, i.e. the body is not touched until
|
|
47
|
+
all free variables are bound.
|
|
48
|
+
|
|
49
|
+
# Installation
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
npm install ski-interpreter
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
# Usage
|
|
56
|
+
|
|
57
|
+
```javascript
|
|
58
|
+
const {SKI} = require('@dallaylaen/ski-interpreter');
|
|
59
|
+
const ski = new SKI(); // the parser
|
|
60
|
+
|
|
61
|
+
// parse an expression
|
|
62
|
+
const expr = ski.parse('S(K(SI))K x y');
|
|
63
|
+
|
|
64
|
+
// run the expression
|
|
65
|
+
const result = expr.run({max: 100, throw: true});
|
|
66
|
+
console.log('reached '+ result.expr + ' after ' + result.steps + ' steps.');
|
|
67
|
+
|
|
68
|
+
// inspect the steps taken to reach the result
|
|
69
|
+
for (const step of expr.walk())
|
|
70
|
+
console.log(step.expr.toString());
|
|
71
|
+
|
|
72
|
+
// convert lambda to SKI
|
|
73
|
+
const lambda = ski.parse('x->y->z->x z y');
|
|
74
|
+
for (const step of lambda.rewriteSKI())
|
|
75
|
+
console.log(step.steps + ': ' + step.expr);
|
|
76
|
+
|
|
77
|
+
// convert combinators to lambda
|
|
78
|
+
const combinator = ski.parse('BSC');
|
|
79
|
+
for (const step of combinator.lambdify())
|
|
80
|
+
console.log(step.steps + ': ' + step.expr);
|
|
81
|
+
|
|
82
|
+
// compare expressions
|
|
83
|
+
ski.parse('a->b->a').equals(ski.parse('x->y->x')); // true!
|
|
84
|
+
|
|
85
|
+
const jar = {}; // share free variables with the same names between parser runs
|
|
86
|
+
ski.parse('a->b->f a').equals(ski.parse('x->y->f x')); // false
|
|
87
|
+
ski.parse('a->b->f a', jar).equals(ski.parse('x->y->f x', jar)); // true
|
|
88
|
+
|
|
89
|
+
// define new terms
|
|
90
|
+
ski.add('T', 'S(K(SI))K');
|
|
91
|
+
console.log(ski.parse('T x y').run().expr); // prints 'x(y)'
|
|
92
|
+
|
|
93
|
+
// define terms with JS implementation
|
|
94
|
+
const jay = new SKI.classes.Native('J', a=>b=>c=>d=>a.apply(b).apply(a.apply(d).apply(c)));
|
|
95
|
+
ski.add('J', jay);
|
|
96
|
+
|
|
97
|
+
// access predefined terms directly
|
|
98
|
+
SKI.C.apply(SKI.S); // a term
|
|
99
|
+
const [x, y] = SKI.free('x', 'y'); // free variables
|
|
100
|
+
SKI.church(5).apply(x, y).run().expr + ''; // 'x(x(x(x(x y))))'
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
# Playground
|
|
104
|
+
|
|
105
|
+
https://dallaylaen.github.io/ski-interpreter/
|
|
106
|
+
|
|
107
|
+
* all of the above features (except comparison and JS-native terms) in your browser
|
|
108
|
+
* expressions have permalinks
|
|
109
|
+
* can configure verbosity & executeion speed
|
|
110
|
+
|
|
111
|
+
# Quests
|
|
112
|
+
|
|
113
|
+
https://dallaylaen.github.io/ski-interpreter/quest.html
|
|
114
|
+
|
|
115
|
+
This page contains small tasks of increasing complexity.
|
|
116
|
+
Each task requires the user to build a combinator with specific properties.
|
|
117
|
+
|
|
118
|
+
# CLI
|
|
119
|
+
|
|
120
|
+
REPL comes with the package as [bin/ski.js](bin/ski.js).
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# Thanks
|
|
125
|
+
|
|
126
|
+
* [@ivanaxe](https://github.com/ivanaxe) for luring me into [icfpc 2011](http://icfpc2011.blogspot.com/2011/06/task-description-contest-starts-now.html) where I was introduced to combinators.
|
|
127
|
+
* [@akuklev](https://github.com/akuklev) for explaining functional programming to me so many times that I actually got some idea.
|
|
128
|
+
|
|
129
|
+
# Prior art and inspiration
|
|
130
|
+
|
|
131
|
+
* "To Mock The Mockingbird" by Raymond Smulian.
|
|
132
|
+
* [combinator birds](https://www.angelfire.com/tx4/cus/combinator/birds.html) by [Chris Rathman](https://www.angelfire.com/tx4/cus/index.html)
|
|
133
|
+
|
|
134
|
+
# License and copyright
|
|
135
|
+
|
|
136
|
+
This software is free and available under the MIT license.
|
|
137
|
+
|
|
138
|
+
© Konstantin Uvarin 2024-2025
|
package/bin/ski.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
#!/usr/bin/env -S node --stack-size=20600
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs/promises');
|
|
4
|
+
|
|
5
|
+
const {SKI} = require('../index');
|
|
6
|
+
|
|
7
|
+
const [myname, options, positional] = parseArgs(process.argv);
|
|
8
|
+
|
|
9
|
+
if (options.help) {
|
|
10
|
+
console.error(myname + ': usage: ' + myname + '[-q | -v ] -e <expression>');
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (typeof options.e === 'string' && positional.length > 0 || positional.length > 1) {
|
|
15
|
+
console.error(myname + ': either -e <expr> or exactly one filename must be given');
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const ski = new SKI();
|
|
20
|
+
|
|
21
|
+
if (options.e === undefined && !positional.length) {
|
|
22
|
+
// interactive console
|
|
23
|
+
const readline = require('readline');
|
|
24
|
+
const rl = readline.createInterface({
|
|
25
|
+
input: process.stdin,
|
|
26
|
+
output: process.stdout,
|
|
27
|
+
prompt: '> ',
|
|
28
|
+
terminal: true,
|
|
29
|
+
});
|
|
30
|
+
if (!options.q)
|
|
31
|
+
console.log('Welcome to SKI interactive shell. Known combinators: '+ski.showRestrict());
|
|
32
|
+
rl.on('line', str => {
|
|
33
|
+
const flag = str.match(/^\s*([-+])([qvt])\s*$/);
|
|
34
|
+
if (flag) {
|
|
35
|
+
options[flag[2]] = flag[1] === '+';
|
|
36
|
+
} else {
|
|
37
|
+
runLine(err => {
|
|
38
|
+
console.log('' + err)
|
|
39
|
+
})(str);
|
|
40
|
+
}
|
|
41
|
+
rl.prompt();
|
|
42
|
+
});
|
|
43
|
+
rl.once('close', () => {
|
|
44
|
+
if (!options.q)
|
|
45
|
+
console.log('Bye, and may your bird fly high!');
|
|
46
|
+
process.exit(0)
|
|
47
|
+
});
|
|
48
|
+
rl.prompt();
|
|
49
|
+
} else {
|
|
50
|
+
const prom = positional.length > 0
|
|
51
|
+
? fs.readFile(positional[0], 'utf8')
|
|
52
|
+
: Promise.resolve(options.e);
|
|
53
|
+
|
|
54
|
+
prom.then(runLine(err => { console.error(''+err); process.exit(3)})).catch(err => {
|
|
55
|
+
console.error(myname + ': ' + err);
|
|
56
|
+
process.exit(2);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function runLine(onErr) {
|
|
61
|
+
return function(source) {
|
|
62
|
+
if (!source.match(/\S/))
|
|
63
|
+
return 0; // nothing to see here
|
|
64
|
+
try {
|
|
65
|
+
const expr = ski.parse(source);
|
|
66
|
+
|
|
67
|
+
const t0 = new Date();
|
|
68
|
+
for (let state of expr.walk()) {
|
|
69
|
+
if (state.final && !options.q)
|
|
70
|
+
console.log(`// ${state.steps} step(s) in ${new Date() - t0}ms`);
|
|
71
|
+
if (options.v || state.final)
|
|
72
|
+
console.log('' + state.expr.toString({terse: options.t}));
|
|
73
|
+
if (state.final && expr instanceof SKI.classes.Alias)
|
|
74
|
+
ski.add(expr.name, state.expr);
|
|
75
|
+
}
|
|
76
|
+
return 0;
|
|
77
|
+
} catch (err) {
|
|
78
|
+
onErr(err);
|
|
79
|
+
return 1;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function parseArgs(argv) {
|
|
85
|
+
const [_, script, ...list] = argv;
|
|
86
|
+
|
|
87
|
+
const todo = {
|
|
88
|
+
'--': () => { pos.push(...list) },
|
|
89
|
+
'--help': () => { opt.help = true },
|
|
90
|
+
'-q': () => { opt.q = true },
|
|
91
|
+
'-v': () => { opt.v = true },
|
|
92
|
+
'-c': () => { opt.t = false },
|
|
93
|
+
'-t': () => { opt.t = true },
|
|
94
|
+
'-e': () => {
|
|
95
|
+
if (list.length < 1)
|
|
96
|
+
throw new Error('option ' + next + 'requires and argument');
|
|
97
|
+
opt.e = list.shift();
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// TODO replace with a relevant dependency
|
|
102
|
+
const pos = [];
|
|
103
|
+
const opt = {};
|
|
104
|
+
|
|
105
|
+
while (list.length > 0) {
|
|
106
|
+
const next = list.shift();
|
|
107
|
+
|
|
108
|
+
if (!next.match(/^-/)) {
|
|
109
|
+
pos.push(next);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const action = todo[next];
|
|
114
|
+
if (typeof action !== "function")
|
|
115
|
+
throw new Error('Unknown option ' + next + '; see ski.js --help');
|
|
116
|
+
|
|
117
|
+
action();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return [script, opt, pos];
|
|
121
|
+
}
|
|
122
|
+
|