@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 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
+
package/index.js ADDED
@@ -0,0 +1,8 @@
1
+ const ski = require('./lib/parser');
2
+ const quest = require('./lib/quest');
3
+
4
+ module.exports = { ...ski, ...quest };
5
+ if (typeof window !== 'undefined') {
6
+ window.SKI = ski.SKI;
7
+ window.SKI.Quest = quest.Quest;
8
+ }