├── .babelrc
├── .gitignore
├── .mocha.opts
├── .mocharc.json
├── .travis.yml
├── LICENSE
├── README.md
├── index.js
├── package-lock.json
├── package.json
├── public
├── index.html
├── lambda.png
└── style.css
├── src
├── components
│ ├── App
│ │ ├── ProblemPrompter.jsx
│ │ └── index.jsx
│ ├── LambdaInput
│ │ └── index.jsx
│ └── Repl
│ │ ├── Assignment.jsx
│ │ ├── Computation.jsx
│ │ ├── Error.jsx
│ │ ├── Info.jsx
│ │ ├── LambdaMetadata.jsx
│ │ └── index.jsx
├── game
│ ├── inlineDefinitions
│ │ ├── InlineDefinition.jsx
│ │ └── definitions.js
│ └── problems
│ │ ├── index.js
│ │ └── readme.md
├── index.jsx
├── lambdaActor
│ ├── actor.js
│ ├── astToMetadata.js
│ ├── errors.js
│ ├── executionContext.js
│ ├── timeoutWatcher.js
│ └── worker.js
├── lib
│ └── lambda
│ │ ├── __tests__
│ │ ├── bReduction.spec.js
│ │ ├── cannonize.spec.js
│ │ ├── generated_suite.data.js
│ │ ├── generated_suite.spec.js
│ │ ├── lexer.spec.js
│ │ ├── normalize.spec.js
│ │ ├── parser.spec.js
│ │ └── util.spec.js
│ │ ├── cannonize.ts
│ │ ├── churchPrimitives.ts
│ │ ├── equality.ts
│ │ ├── errors.ts
│ │ ├── index.ts
│ │ ├── lexer.ts
│ │ ├── normalize.ts
│ │ ├── operations.ts
│ │ ├── parser.ts
│ │ ├── renderer.ts
│ │ ├── types.ts
│ │ └── util.ts
└── util
│ ├── generateGoldens.js
│ └── persist.js
├── tsconfig.json
└── webpack.config.cjs
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["@babel/env", "@babel/preset-react"]
3 | }
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | npm-debug.log
3 | build
4 | public/build
5 |
--------------------------------------------------------------------------------
/.mocha.opts:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evinism/lambda-explorer/7c69ebcf4907409e48773e602b31a45995e84572/.mocha.opts
--------------------------------------------------------------------------------
/.mocharc.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/mocharc.json",
3 | "require": "tsx",
4 | "spec": "src/**/*.spec.js"
5 | }
6 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "node"
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Evin Sellin
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.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Lambda Explorer!
2 |
3 | [](https://travis-ci.org/evinism/lambda-explorer)
4 |
5 | To get running:
6 | ```
7 | npm install
8 | npm run dev
9 | ```
10 | then navigate to localhost:8082
11 |
12 | To not persist or destroy local copy of the game, visit `/?nopersist`
13 |
14 | To view the in progress inline definitions, visit `/?inlinedefs`
15 |
16 | ### Contributing
17 |
18 | [Please contribute!](https://github.com/evinism/lambda-explorer/issues)
19 |
20 | Disclaimer: This isn't clean code -- this is MVP code. Not absolutely horrible to work with but still don't expect to find supreme design beauty in here.
21 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evinism/lambda-explorer/7c69ebcf4907409e48773e602b31a45995e84572/index.js
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "lambda-explorer",
3 | "version": "0.0.1",
4 | "description": "exploration in webdev",
5 | "repository": "https://github.com/evinism/lambda-explorer",
6 | "main": "index.js",
7 | "type": "module",
8 | "scripts": {
9 | "watch": "webpack --watch",
10 | "dev": "webpack-dev-server --port 8082 --static public --mode=development",
11 | "webpack": "webpack",
12 | "test": "mocha"
13 | },
14 | "author": "evin",
15 | "license": "MIT",
16 | "devDependencies": {
17 | "@babel/core": "^7.23.7",
18 | "@babel/preset-env": "^7.23.8",
19 | "@babel/preset-react": "^7.23.3",
20 | "@babel/register": "^7.23.7",
21 | "babel-loader": "^9.1.3",
22 | "chai": "^5.0.0",
23 | "mocha": "^10.2.0",
24 | "ts-loader": "^9.5.1",
25 | "tsx": "^4.19.1",
26 | "typescript": "^5.3.3",
27 | "webpack": "^5.89.0",
28 | "webpack-cli": "^5.1.4",
29 | "webpack-dev-server": "^4.15.1"
30 | },
31 | "dependencies": {
32 | "@babel/polyfill": "^7.10.4",
33 | "@types/ramda": "^0.29.9",
34 | "classnames": "^2.5.1",
35 | "ramda": "^0.29.1",
36 | "react": "^18.2.0",
37 | "react-dom": "^18.2.0",
38 | "reactjs-popup": "^2.0.6"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Lambda Explorer
6 |
7 |
8 |
9 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/public/lambda.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evinism/lambda-explorer/7c69ebcf4907409e48773e602b31a45995e84572/public/lambda.png
--------------------------------------------------------------------------------
/public/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0px;
3 | font-family: Roboto;
4 | }
5 |
6 | html, body, #react-mount {
7 | height: 100%;
8 | }
9 |
10 | .app-wrapper {
11 | display: flex;
12 | flex-direction: column;
13 | height: 100%;
14 | }
15 |
16 | header {
17 | padding-left: 10%;
18 | background-color: aliceblue;
19 | box-shadow: 0px -5px 10px black;
20 | }
21 |
22 | footer {
23 | padding-top: 20px;
24 | padding-bottom: 20px;
25 | padding-left: 10%;
26 | background-color: aliceblue;
27 | font-size: 12px;
28 | color: darkslateblue;
29 | box-shadow: 0px 5px 10px black;
30 | }
31 |
32 | footer > a {
33 | margin-right: 20px;
34 | color: inherit;
35 | }
36 |
37 | h1 {
38 | margin: 10px 0;
39 | font-weight: 400;
40 | }
41 |
42 | .app-content {
43 | width: 90vw;
44 | margin-left: auto;
45 | display: flex;
46 | font-weight: 300;
47 | overflow: hidden;
48 | flex-grow: 1;
49 | }
50 |
51 | .highlighted {
52 | font-weight: bold;
53 | }
54 |
55 | article {
56 | flex-grow: 1;
57 | position: relative;
58 | }
59 |
60 | aside {
61 | width: calc(300px + 10vw);
62 | min-width: 300px;
63 | border-left: 3px dashed #f0f0f0;
64 | margin-left: 5px;
65 | padding: 10px 27px 15px 27px;
66 | overflow: auto;
67 | }
68 |
69 | aside p,
70 | aside h3 {
71 | max-width: 300px;
72 | }
73 |
74 |
75 |
76 | table {
77 | width: 100%;
78 | margin-left: -8px;
79 | padding-left: 4px;
80 | border-collapse: collapse;
81 | }
82 |
83 | td, th {
84 | padding: 5px;
85 | }
86 |
87 | td {
88 | border-top: 1px solid lightgray;
89 | border-bottom: 1px solid lightgray;
90 | }
91 |
92 | .repl {
93 | font-size: 15pt;
94 | font-weight: 300;
95 | padding-left: 5px;
96 | height: 100%;
97 | width: 100%;
98 | overflow: auto;
99 | position: absolute;
100 | cursor: default;
101 | border-left: 1px solid lightgray;
102 | }
103 |
104 | .repl .info {
105 | color: #bbbbbb;
106 | padding-top: 7px;
107 | }
108 |
109 | .repl .result {
110 | color: black;
111 | }
112 |
113 | .repl .command {
114 | color: #777777;
115 | white-space: nowrap;
116 | overflow: hidden;
117 | text-overflow: ellipsis;
118 | max-width: 100%;
119 | }
120 |
121 | .repl .error {
122 | color: #ff3333;
123 | }
124 |
125 | .repl .caret {
126 | color: #dddddd;
127 | }
128 |
129 | .repl .prompt-caret {
130 | color: #777777;
131 | padding: 2px 4px 2px 0;
132 | }
133 |
134 | .repl .prompt.error .prompt-caret {
135 | color: #ff1111;
136 | }
137 |
138 | .prompt {
139 | padding-bottom: 7px;
140 | display: flex;
141 | }
142 |
143 | .output > div {
144 | padding: 2px 0px;
145 | }
146 |
147 | .lambda-input {
148 | flex-grow: 1;
149 | border: 0px;
150 | font-family: inherit;
151 | font-size: inherit;
152 | font-weight: inherit;
153 | background-color: transparent;
154 | }
155 |
156 | .lambda-input:focus {
157 | outline: none;
158 | }
159 |
160 | .expand-collapse-button {
161 | visibility: hidden;
162 | font-size: 10px;
163 | vertical-align: middle;
164 | line-height: 24px;
165 | cursor: pointer;
166 | margin-left: 5px;
167 | width: 7px;
168 | display: inline-block;
169 | }
170 |
171 | .result:hover .expand-collapse-button, .error:hover .expand-collapse-button {
172 | visibility: visible;
173 | }
174 |
175 |
176 | .result-inner {
177 | display: flex;
178 | max-width: calc(100% - 10px);
179 | white-space: nowrap;
180 | }
181 |
182 | .result-inner > span {
183 | overflow: hidden;
184 | text-overflow: ellipsis;
185 | }
186 |
187 | .result-inner > div {
188 | flex-grow: 1;
189 | padding-left: 3px;
190 | }
191 |
192 | .metadata {
193 | color: black;
194 | margin-left: -5px;
195 | padding: 10px;
196 | font-size: 10pt;
197 | border-left: 5px solid lightgrey;
198 | overflow: auto;
199 | white-space: nowrap;
200 | }
201 |
202 | .error-metadata-header {
203 | margin-top: 0px;
204 | }
205 |
206 | .result i {
207 | color: #777777;
208 | }
209 |
210 | aside button {
211 | vertical-align: bottom;
212 | }
213 |
214 | .secret.secret {
215 | color: transparent;
216 | background: #555555;
217 | }
218 |
219 | .secret.secret:hover {
220 | background-color: aliceblue;
221 | color: black;
222 | }
223 |
224 | .code, .secret {
225 | font-family: 'Roboto Mono';
226 | font-size: 11pt;
227 | padding: 0px 4px;
228 | background: aliceblue;
229 | white-space: nowrap;
230 | }
231 |
232 | .problem-text {
233 | padding-bottom: 10px;
234 | }
235 |
236 | .solved-badge {
237 | margin-top: -14px;
238 | font-size: 10pt;
239 | color: #888888;
240 | }
241 |
242 | .problem-navigator {
243 | position: absolute;
244 | bottom: 50px;
245 | width: 120px;
246 | padding: 4px;
247 | display: flex;
248 | background: aliceblue;
249 | align-items: center;
250 | justify-content: space-between;
251 | }
252 |
253 | .problem-navigator > button {
254 | font-size: 10pt;
255 | margin: 4px;
256 | }
257 |
258 | .problem-navigator > button.hidden {
259 | visibility: hidden;
260 | }
261 |
262 | .problem-navigator > .next-problem {
263 | float: right; /* don't hate me- this is like one line */
264 | }
265 |
266 | .repl::-webkit-scrollbar {
267 | width: 0px; /* remove scrollbar space */
268 | background: transparent; /* optional: just make scrollbar invisible */
269 | }
270 |
271 | .inline-definition {
272 | border-bottom: 1px dotted #888888;
273 | }
274 |
275 | /* Dark Mode */
276 |
277 | .dark-mode.app-wrapper {
278 | color: #eeeeee;
279 | background-color: #12141c;
280 | }
281 |
282 | .dark-mode header {
283 | background-color: #333344;
284 | }
285 |
286 | .dark-mode .code {
287 | background: #333333;
288 | }
289 |
290 | .dark-mode .lambda-input {
291 | color: whitesmoke;
292 | }
293 |
294 | /* Dark mode REPL stuff */
295 | .dark-mode .repl .info {
296 | color: whitesmoke;
297 | }
298 |
299 | .dark-mode .repl .result {
300 | color: whitesmoke;
301 | }
302 |
303 | .dark-mode .repl .command {
304 | color: #999999;
305 | }
306 |
307 | .dark-mode .repl .error {
308 | color: #ff3333;
309 | }
310 |
311 | .dark-mode .repl .caret {
312 | color: #676767;
313 | }
314 |
315 | .dark-mode .repl .prompt-caret {
316 | color: #eeeeee;
317 | }
318 |
319 | .dark-mode .result i {
320 | color: #aaaadd;
321 | }
322 |
323 | .dark-mode .repl {
324 | border-left: 1px solid #33333a;
325 | }
326 |
327 | .dark-mode aside {
328 | border-left: 3px dashed #33333a;
329 | }
330 |
331 | .dark-mode footer {
332 | background-color: #333344;
333 | color: #eeeeee;
334 | }
335 |
336 | .dark-mode .metadata {
337 | color: whitesmoke;
338 | border-left: 5px solid #33333a;
339 | }
340 |
341 |
342 | .dark-mode .secret.secret {
343 | background: whitesmoke;
344 | color: transparent;
345 | }
346 |
347 | .dark-mode .secret.secret:hover {
348 | color: whitesmoke;
349 | background-color: #333333;
350 | }
351 |
352 | .dark-mode h1 {
353 | font-weight: 300;
354 | }
355 |
356 | .dark-mode aside a {
357 | color: whitesmoke;
358 | }
359 |
360 | .dark-mode .problem-navigator {
361 | background: #333344;
362 | }
363 |
--------------------------------------------------------------------------------
/src/components/App/ProblemPrompter.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default ({problems, current, shown, handlePrevClick, handleNextClick}) => {
4 | const problem = problems[shown];
5 |
6 | const formatted = (num, item) => `${num}. ${item.title}`
7 |
8 | return (
9 |
10 |
11 |
Problem {shown + 1}: {problem.title}
12 | {current !== shown && (
[solved]
)}
13 | {problem.prompt}
14 |
15 |
16 | 0 ? 'prev-problem' : 'prev-problem hidden'}
18 | onClick={handlePrevClick}
19 | >
20 | ‹
21 |
22 | {shown + 1} / {problems.length}
23 |
27 | ›
28 |
29 |
30 |
31 | );
32 | };
33 |
--------------------------------------------------------------------------------
/src/components/App/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import ProblemPrompter from "./ProblemPrompter.jsx";
4 | import Repl from "../Repl/index.jsx";
5 | import persistComponent from "../../util/persist.js";
6 | import problems from "../../game/problems/index.js";
7 |
8 | const StartPrompt = ({start}) => (
9 |
10 |
11 | Interactive REPL and tutorial for the untyped Lambda Calculus
12 |
13 |
14 | Click here to begin the tutorial
15 |
16 |
17 | );
18 |
19 | const defaultState = {
20 | currentProblem: 0,
21 | gameStarted: false,
22 | shownProblem: 0,
23 | darkMode: false,
24 | };
25 |
26 | class App extends React.Component {
27 | state = defaultState;
28 |
29 | _handleOnCompute = (computation) => {
30 | let {
31 | currentProblem,
32 | shownProblem,
33 | } = this.state;
34 | if (problems[shownProblem].winCondition(computation)) {
35 | if (shownProblem < problems.length - 1){
36 | this.setState({
37 | currentProblem: Math.max(shownProblem + 1, currentProblem),
38 | shownProblem: shownProblem + 1,
39 | });
40 | } else {
41 | this.setState({gameStarted: false});
42 | }
43 | }
44 | }
45 |
46 | startGame = () => {
47 | this.setState({
48 | gameStarted: true,
49 | currentProblem: 0,
50 | shownProblem: 0,
51 | });
52 | }
53 |
54 | _handleNext = () => {
55 | this.setState({
56 | shownProblem: Math.min(
57 | this.state.shownProblem + 1,
58 | this.state.currentProblem
59 | ),
60 | });
61 | }
62 |
63 | _handlePrev = () => {
64 | this.setState({
65 | shownProblem: Math.max(
66 | this.state.shownProblem - 1,
67 | 0
68 | )
69 | });
70 | }
71 |
72 | componentWillMount() {
73 | persistComponent (
74 | 'component/App',
75 | () => this.state,
76 | newState => this.setState(newState || {})
77 | );
78 | }
79 |
80 | _toggleDarkLight = () => {
81 | this.setState({
82 | darkMode: !this.state.darkMode,
83 | });
84 | }
85 |
86 | render() {
87 | const {
88 | gameStarted,
89 | shownProblem,
90 | currentProblem,
91 | darkMode,
92 | } = this.state;
93 |
94 | return (
95 |
96 |
97 | Lambda Explorer
98 |
99 |
100 |
101 |
102 |
103 |
104 | {!gameStarted && (
105 |
106 | )}
107 | {gameStarted && (
108 |
115 | )}
116 |
117 |
118 |
127 |
128 | );
129 | }
130 | }
131 |
132 | export default App;
133 |
--------------------------------------------------------------------------------
/src/components/LambdaInput/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {mapObjIndexed as rMap, compose as rCompose, values as rValues} from 'ramda';
3 |
4 | const replacementMapping = {
5 | 0: '₀',
6 | 1: '₁',
7 | 2: '₂',
8 | 3: '₃',
9 | 4: '₄',
10 | 5: '₅',
11 | 6: '₆',
12 | 7: '₇',
13 | 8: '₈',
14 | 9: '₉',
15 | '\\': 'λ',
16 | };
17 |
18 | const replaceAll = str => str.split('').map(
19 | letter => (replacementMapping[letter] || letter)
20 | ).join('');
21 |
22 | export default class LambdaInput extends React.Component {
23 | state = {text: ''};
24 |
25 | _handleChange = (e) => {
26 | const text = replaceAll(e.target.value);
27 | this.setState({
28 | text,
29 | selStart: e.target.selectionStart,
30 | selEnd: e.target.selectionEnd,
31 | });
32 | if (this.props.onChange) {
33 | this.props.onChange(text);
34 | }
35 | };
36 |
37 | _handleKeyPress = (e) => {
38 | if(e.key == 'Enter'){
39 | this.props.submit && this.props.submit();
40 | }
41 | }
42 |
43 | componentDidUpdate(){
44 | this.refs.input.selectionStart = this.state.selStart;
45 | this.refs.input.selectionEnd = this.state.selEnd;
46 | }
47 |
48 | render(){
49 | // is very controllable yes!!!
50 | const value = this.props.value !== undefined
51 | ? this.props.value
52 | : this.state.text;
53 | return (
54 |
62 | )
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/components/Repl/Assignment.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default class Error extends React.Component {
4 | state = { expanded: false };
5 |
6 | handleButtonClick = (e) => {
7 | this.setState({
8 | expanded: !this.state.expanded,
9 | });
10 | e.preventDefault();
11 | e.stopPropagation();
12 | }
13 |
14 | render(){
15 | const {ast} = this.props;
16 | return (
17 |
18 |
19 |
{this.props.children}
20 |
21 |
22 |
23 | );
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/components/Repl/Computation.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Metadata from "./LambdaMetadata.jsx";
3 |
4 | export default class Computation extends React.Component {
5 | state = { expanded: false };
6 |
7 | handleButtonClick = (e) => {
8 | this.setState({
9 | expanded: !this.state.expanded,
10 | });
11 | e.preventDefault();
12 | e.stopPropagation();
13 | }
14 |
15 | render(){
16 | const {
17 | normAsNumeral,
18 | normAsBoolean,
19 | normalForm,
20 | } = this.props.computation;
21 |
22 | let addlInfo = [];
23 | addlInfo.push(normalForm.type);
24 | (normAsNumeral !== undefined) && addlInfo.push(`church numeral ${normAsNumeral}`);
25 | (normAsBoolean !== undefined) && addlInfo.push(`church boolean ${normAsBoolean}`);
26 | const addlInfoString = addlInfo.join(', ');
27 |
28 | const renderedAddlInfo = addlInfoString && ` <${addlInfoString}>`;
29 |
30 | return (
31 |
32 |
33 |
{this.props.children}
34 |
35 | {renderedAddlInfo}
36 |
37 | {this.state.expanded ? '(-)' : '(+)'}
38 |
39 |
40 |
41 | {this.state.expanded &&
}
42 |
43 | );
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/components/Repl/Error.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | leftmostOutermostRedex,
4 | renderExpression,
5 | } from "../../lib/lambda/index.ts";
6 |
7 |
8 | const shownSteps = 25;
9 |
10 | function showFirstNSteps(ast){
11 | const firstSteps = [ ast ];
12 | for (let i = 0; i < shownSteps; i++){
13 | firstSteps.push(ast && leftmostOutermostRedex(firstSteps[firstSteps.length - 1]));
14 | }
15 |
16 | const firstStepsRendered = (
17 | {
18 | firstSteps
19 | .filter(item => Boolean(item))
20 | .map((step, idx) => (
21 | {renderExpression(step)}
22 | ))
23 | }
24 | );
25 |
26 | return (
27 |
28 |
First {shownSteps} redexes: {firstStepsRendered}
29 |
30 | );
31 | }
32 |
33 | export default class Error extends React.Component {
34 | state = { expanded: false };
35 |
36 | handleButtonClick = (e) => {
37 | this.setState({
38 | expanded: !this.state.expanded,
39 | });
40 | e.preventDefault();
41 | e.stopPropagation();
42 | }
43 |
44 | render(){
45 | const {ast, error} = this.props;
46 |
47 | let buildErrorMetadata;
48 | // would prefer instanceof, but i think babel has a bug
49 | if (error.name === 'LambdaExecutionTimeoutError') {
50 | buildErrorMetadata = showFirstNSteps;
51 | }
52 |
53 | return (
54 |
55 |
56 |
{this.props.children}
57 | {buildErrorMetadata && ( // implicitly selects runtime errors, kinda shitty
58 |
59 |
60 | {this.state.expanded ? '(-)' : '(+)'}
61 |
62 |
63 | )}
64 |
65 | {this.state.expanded && buildErrorMetadata && buildErrorMetadata(ast)}
66 |
67 | );
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/components/Repl/Info.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default props => ({props.children}
);
4 |
--------------------------------------------------------------------------------
/src/components/Repl/LambdaMetadata.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import astToMetadata from '../../lambdaActor/astToMetadata';
3 | import {
4 | parseTerm,
5 | renderAsChurchNumeral,
6 | renderAsChurchBoolean,
7 | renderExpression,
8 | } from "../../lib/lambda/index.ts";
9 |
10 | const LambdaMetadata = ({ast, err}) => {
11 | if(!ast){
12 | return ({err}
);
13 | }
14 |
15 | const metadata = astToMetadata(ast);
16 | const renderedFreeVars = metadata.freeVars.map(token => (
17 | {token.name}
18 | ));
19 |
20 | const renderedFromAst = renderExpression(ast);
21 | let renderedBetaReduced;
22 | if (metadata.betaReduced) {
23 | renderedBetaReduced = renderExpression(metadata.betaReduced);
24 | } else {
25 | renderedBetaReduced = '[beta irreducible]';
26 | }
27 |
28 | // -- eta reduction
29 | let renderedEtaReduced;
30 | if (metadata.etaReduced) {
31 | renderedEtaReduced = renderExpression(metadata.etaReduced);
32 | } else {
33 | renderedEtaReduced = '[eta irreducible]';
34 | }
35 |
36 | let renderedNumeral;
37 | if (metadata.asNumeral !== undefined) {
38 | renderedNumeral = metadata.asNumeral;
39 | } else {
40 | renderedNumeral = '[not a church numeral]'
41 | }
42 |
43 | // -- church booleans
44 | let renderedBoolean;
45 | if (metadata.asBoolean !== undefined) {
46 | renderedBoolean = String(metadata.asBoolean);
47 | } else {
48 | renderedBoolean = '[not a church boolean]'
49 | }
50 |
51 | // -- normal form
52 | let renderedNormalForm;
53 | if (metadata.normalForm) {
54 | renderedNormalForm = renderExpression(metadata.normalForm);
55 | } else {
56 | renderedNormalForm = renderExpression(metadata.ast);
57 | }
58 | // -- normal form church numerals
59 | let renderedNormNumeral;
60 | if (metadata.normAsNumeral !== undefined) {
61 | renderedNormNumeral = metadata.normAsNumeral;
62 | } else {
63 | renderedNormNumeral = '[not a church numeral]'
64 | }
65 |
66 | // -- normal form church booleans
67 | let renderedNormBoolean;
68 | if (metadata.normAsBoolean !== undefined) {
69 | renderedNormBoolean = String(metadata.normAsBoolean);
70 | } else {
71 | renderedNormBoolean = '[not a church boolean]'
72 | }
73 |
74 | const renderedStepsToNormal = (
75 |
76 | {metadata.stepsToNormal.map((step, idx) => (
77 | {renderExpression(step)}
78 | ))}
79 |
80 | );
81 |
82 | return (
83 |
84 |
Free Variables: {renderedFreeVars}
85 |
Rendered from AST: {renderedFromAst}
86 |
Beta-reduced: {renderedBetaReduced}
87 |
Eta-reduced: {renderedEtaReduced}
88 |
Normal Form: {renderedNormalForm}
89 |
Normal As Church Numeral: {renderedNormNumeral}
90 |
Normal As Church Boolean: {renderedNormBoolean}
91 |
steps to normal form: {renderedStepsToNormal}
92 |
93 | );
94 | };
95 |
96 | export default LambdaMetadata;
97 |
--------------------------------------------------------------------------------
/src/components/Repl/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import cx from 'classnames';
3 |
4 | import persistComponent from "../../util/persist.js";
5 |
6 | import LambdaInput from '../LambdaInput';
7 | import LambdaActor from '../../lambdaActor/actor.js';
8 |
9 | import Assignment from './Assignment';
10 | import Computation from './Computation';
11 | import Error from './Error'
12 | import Info from './Info';
13 |
14 | import {
15 | renderExpression,
16 | parseExtendedSyntax,
17 | } from "../../lib/lambda/index.ts";
18 |
19 | const initialOutput = (
20 |
21 | lambda runtime v0.1
22 | \ to type λ, [0-9] to type subscripts, := for assignment, upper-case for multi-letter variables
23 |
24 | );
25 |
26 | const renderEvaluation = (evaluation) => {
27 | switch (evaluation.type) {
28 | case 'assignment': {
29 | const { lhs, ast } = evaluation;
30 | const outputText = `${lhs}: ${renderExpression(ast)}`
31 | return ( {outputText} );
32 | }
33 | case 'computation': {
34 | const { normalForm } = evaluation;
35 | const renderedNF = normalForm && renderExpression(normalForm);
36 | return ( {renderedNF} );
37 | }
38 | case 'error': {
39 | const { error, ast } = evaluation;
40 | return ( {error.message} );
41 | }
42 | case 'info': {
43 | const { message } = evaluation;
44 | return ( {message} );
45 | }
46 | }
47 | };
48 |
49 | class Repl extends React.Component {
50 | state = {
51 | text: '',
52 | commandHistory: [],
53 | mutableHistory: [''],
54 | currentPos: 0,
55 | output: [initialOutput],
56 | }
57 |
58 | _onChange = (text) => {
59 | let error = false;
60 | const newArr = [].concat(this.state.mutableHistory);
61 | newArr[this.state.currentPos] = text
62 | this.setError(text);
63 | this.setState({mutableHistory: newArr});
64 | }
65 |
66 | setError = (text) => {
67 | // should probably be done in the render function anyways..
68 | let error = false;
69 | if (text === '') {
70 | this.setState({error: false});
71 | return;
72 | }
73 | try {
74 | parseExtendedSyntax(text);
75 | } catch (e) {
76 | error = true;
77 | }
78 | this.setState({error: error});
79 | }
80 |
81 | _scrollToBottom = () => {
82 | const repl = this.refs.repl;
83 | repl.scrollTop = repl.scrollHeight;
84 | }
85 |
86 | _handleClick = () => {
87 | if(window.getSelection().isCollapsed){
88 | this.refs.prompt.querySelector('.lambda-input').focus();
89 | }
90 | }
91 |
92 | _receiveEvaluation = (evaluation) => {
93 | evaluation = {
94 | ...evaluation,
95 | executionContext: this.lambdaActor.executionContext, //this is a garbage hack to allow win conditions
96 | };
97 |
98 | const result = renderEvaluation(evaluation);
99 | const text = evaluation.text;
100 |
101 | let nextOutput = [
102 | ...this.state.output,
103 | (
104 |
105 | >
106 | {text}
107 |
108 | ),
109 | result,
110 | ];
111 |
112 | const nextHistory = [...this.state.commandHistory, text];
113 |
114 | this.props.onCompute && this.props.onCompute(evaluation);
115 |
116 | this.setError('');
117 | this.setState({
118 | commandHistory: nextHistory,
119 | mutableHistory: [...nextHistory, ''],
120 | currentPos: nextHistory.length,
121 | output: nextOutput,
122 | });
123 | }
124 |
125 | _submit = async () => {
126 | const text = this.state.mutableHistory[this.state.currentPos];
127 | if (text === '') {
128 | this.setState({
129 | output: [
130 | ...this.state.output,
131 | (> )
132 | ],
133 | });
134 | return;
135 | }
136 |
137 | this.lambdaActor.send(text);
138 | }
139 |
140 | _nextInHistory = () => {
141 | const nextPos = Math.min(
142 | this.state.currentPos + 1,
143 | this.state.mutableHistory.length - 1
144 | );
145 | this.setError(this.state.mutableHistory[nextPos]);
146 | if(nextPos !== this.state.currentPos) {
147 | this.setState({
148 | currentPos: nextPos
149 | }, this._putCursorAtEnd);
150 | }
151 | }
152 |
153 | _prevInHistory = () => {
154 | const nextPos = Math.max(this.state.currentPos - 1, 0);
155 | this.setError(this.state.mutableHistory[nextPos]);
156 | if(nextPos !== this.state.currentPos) {
157 | this.setState({
158 | currentPos: nextPos
159 | }, this._putCursorAtEnd);
160 | }
161 | }
162 |
163 | _putCursorAtEnd = () => {
164 | // this should be added into the lambda input tbh
165 | // this is garbage
166 | window.setTimeout(() => {
167 | const input = this.refs.prompt.querySelector('input');
168 | const selectionPos = input.value.length;
169 | input.selectionStart = selectionPos;
170 | input.selectionEnd = selectionPos;
171 | }, 0);
172 | }
173 |
174 | _captureUpDown = (e) => {
175 | if(e.key === 'ArrowDown') {
176 | this._nextInHistory();
177 | } else if(e.key === 'ArrowUp') {
178 | this._prevInHistory();
179 | }
180 | }
181 |
182 | componentDidUpdate(){
183 | this._scrollToBottom();
184 | }
185 |
186 | componentWillMount(){
187 | this.lambdaActor = new LambdaActor();
188 | this.lambdaActor.receive = this._receiveEvaluation;
189 | }
190 |
191 | render(){
192 | const renderedOutputs = this.state.output.map((elem, idx) => (
193 |
194 | {elem}
195 |
196 | ));
197 | return (
198 |
199 |
200 | {this.state.output.map((elem, idx) => (
201 |
202 | {elem}
203 |
204 | ))}
205 |
206 |
211 | >
212 |
220 |
221 |
222 | );
223 | }
224 | }
225 |
226 | export default Repl;
227 |
--------------------------------------------------------------------------------
/src/game/inlineDefinitions/InlineDefinition.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactjsPopup from 'reactjs-popup';
3 | import definitions from "./definitions.js";
4 |
5 |
6 | export default ({entry, children}) => {
7 | if (window.location.search !== '?inlinedefs') {
8 | return children;
9 | }
10 | return (
11 | {children}}
13 | position="left center"
14 | on="hover"
15 | >
16 | {console.log(definitions, entry)}
17 | {definitions[entry] || (No definition found!!! )}
18 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/src/game/inlineDefinitions/definitions.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default {
4 | redex: (
5 | Reducible Expression haha lol
6 | ),
7 | expression: (
8 |
9 |
An expression is any valid combination of lambda abstractions, applications, and variables. You're typing expressions into the interpreter!
10 |
11 | ),
12 | application: (
13 |
14 |
An application is a term in the lambda calculus where two expressions are "applied" to each other.
15 |
This is akin to calling A with B as an argument
16 |
17 | ),
18 | lambda_abstraction: (
19 |
20 |
A lambda abstraction term of the form λ [head] . [body]
21 |
Lambda abstractions represent functions in the lambda calculus.
22 |
23 | ),
24 | parameter: (
25 |
26 |
The parameter is the variable that goes in the head of the lambda abstraction
27 |
When beta-reducing, all instances of the parameter get replaced with the
28 |
29 | ),
30 | head: (
31 | TK
32 | ),
33 | body: (
34 |
35 |
The body of a lambda abstraction is section that follows the dot
36 |
This represents what the function returns.
37 |
38 | ),
39 | argument: (
40 |
41 |
When your expression is an application, the argument is the right side of the application.
42 |
For example, in the expression ab, the argument is b
43 |
44 | ),
45 | // Problem specific definitions
46 | beta_reducible_intro: (
47 |
48 |
An expression is beta reducible if the expression is an application where the left side is a lambda abstraction.
49 |
In this case, the left side of the application is λa.aba and the right side is c
50 |
51 | )
52 | };
53 |
--------------------------------------------------------------------------------
/src/game/problems/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import InlineDefinition from "../inlineDefinitions/InlineDefinition.jsx";
3 | import {
4 | equal,
5 | parseTerm as parse,
6 | leftmostOutermostRedex,
7 | toNormalForm,
8 | bReduce,
9 | getFreeVars,
10 | tokenize,
11 | renderExpression,
12 | renderAsChurchNumeral,
13 | renderAsChurchBoolean,
14 | } from "../../lib/lambda/index.ts";
15 | // interface for each should be roughly:
16 | /*
17 | {
18 | title: 'string',
19 | prompt: ReactElement,
20 | winCondition: computationData => bool
21 | }
22 | */
23 |
24 | const safeEqual = (a, b) => (a && b) ? equal(a, b) : false;
25 |
26 | const t = parse('λab.a');
27 | const f = parse('λab.b');
28 |
29 | // (ast, [[arg, arg, result]]) => bool
30 | // should be able to handle non-boolean arguments too...
31 | function satisfiesTruthTable(ast, rules){
32 | return rules.map(
33 | rule => {
34 | const mutable = [].concat(rule);
35 | const target = mutable.pop();
36 | const ruleArgs = mutable;
37 |
38 | const testAst = ruleArgs.reduce((acc, cur) => ({
39 | type: 'application',
40 | left: acc,
41 | right: cur,
42 | }), ast);
43 |
44 | try {
45 | const res = equal(target, toNormalForm(testAst));
46 | return res;
47 | } catch (e) {
48 | console.log("Error in test: " + e);
49 | return false;
50 | }
51 | }
52 | ).reduce((a, b) => a && b, true);
53 | };
54 |
55 | const Code = props => ({props.children} );
56 |
57 | // just a dumb alias
58 | const Def = ({e, children}) => ({children} );
59 |
60 | export default [
61 | {
62 | title: 'Simple Variable',
63 | prompt: (
64 |
65 |
Let's get acquainted with some basic syntax. First, type a₁
. Letters followed optionally by numbers represent variables in this REPL.
66 |
In the actual lambda calculus, it's a bit broader, but we'll keep it simple right now.
67 |
68 | ),
69 | winCondition: ({ast}) => safeEqual(ast, parse('a₁')),
70 | },
71 | // okay the first problem I actually care about
72 | {
73 | title: 'Application',
74 | prompt: (
75 |
76 |
You just wrote an expression which contains only the variable a₁
, which is just a symbol, and not currently bound to anything. In the lambda calculus, variables can be bound to functions, and variables can be applied to one another.
77 |
To apply the variable a₁
to the variable b₁
, type in a₁b₁
. This represents calling the function a₁
with b₁
as an argument.
78 |
Remember, the variable or function you're applying always goes first
79 |
Try applying one variable to another.
80 |
81 | ),
82 | winCondition: ({ast}) => {
83 | return safeEqual(ast, parse('a₁b₁'));
84 | },
85 | },
86 | {
87 | title: 'Upper Case Variables',
88 | prompt: (
89 |
90 |
Since lots of variables in the Lambda Calculus are single letters, there's often a semantic ambiguity when written down. For example, if I type in hi
, do I mean one variable hi
, or the variable h
applied to variable i
?
91 |
For ease of use in this REPL, we'll make a small comprimise: upper case letters are interpreted as multi-letter variables, and lower case letters are interpreted as single-letter variables.
92 |
Try typing MULT
, and observe that it's interpreted as one variable, and NOT an application.
93 |
94 | ),
95 | winCondition: ({ast}) => safeEqual(ast, parse('MULT')),
96 | },
97 | {
98 | title: 'Identity',
99 | prompt: (
100 |
101 |
Now we'll get into lambda abstractions. Lambda abstractions represent functions in the lambda calculus. A lambda abstraction takes the form λ [head] . [body]
where [head]
is the parameter of the function, and [body]
is what the function resolves to.
102 |
Let's write the identity function; a function which takes its argument, does nothing to it, and spits it back out. In the lambda calculus, that looks something like λa.a
103 |
as a reminder, you can type backslash (\
) for λ
104 |
105 | ),
106 | winCondition: ({ast}) => {
107 | return safeEqual(ast, parse('λa.a'));
108 | },
109 | },
110 | {
111 | title: "Parentheses",
112 | prompt: (
113 |
114 |
Schweet! This takes one argument a
and outputs that same argument! Now go ahead and wrap the whole thing in parentheses
115 |
116 | ),
117 | winCondition: ({text, ast}) => {
118 | return (
119 | /^\s*\(.*\)\s*$/.test(text) &&
120 | safeEqual(ast, parse('λa.a'))
121 | );
122 | },
123 | },
124 | {
125 | title: "Baby's first β-reduction",
126 | prompt: (
127 |
128 |
Perfect! In the lambda calculus, you can always wrap expressions in parentheses.
129 |
Now in the same way that we can apply variables to other variables, we can apply lambda expressions to variables. Try applying your identity function to the variable b
, by writing (λa.a)b
.
130 |
Don't worry if this doesn't make sense yet, we'll go a bit more in depth in the future.
131 |
132 | ),
133 | winCondition: ({ast}) => safeEqual(ast, parse('(λa.a)b')),
134 | },
135 | {
136 | title: 'β-reduction function',
137 | prompt: (
138 |
139 |
Nice! What happened here is your identity function took b
as the input and spit it right back out. The process of evaluating a function like this is called beta reduction .
140 |
The result you're seeing here is in what's called normal form , which we'll also go through a little later.
141 |
Just like we can evaluate functions with variables, we can also evaluate them with other functions! Try typing (λa.a)λb.b
142 |
143 | ),
144 | winCondition: ({ast}) => safeEqual(ast, parse('(λa.a)λb.b')),
145 | },
146 | {
147 | title: 'A primer on parsing',
148 | prompt: (
149 |
150 |
So we can perform beta reductions with other functions as the argument!
151 |
With that, we've just introduced the main elements of the syntax of the lambda calculus:
152 |
153 | Variables a₁
154 | Applying one expression to another a₁b₁
155 | A lambda abstraction λx.y
156 | Parentheses (λx.y)
157 |
158 |
We've also introduced a few ways in which these can be combined.
159 |
160 | Applying one lambda expression to a variable (λx.x)b₁
161 | Applying one lambda expression to another (λa.a)λb.b
162 |
163 |
It's time to solidify our understanding of how these combine syntactically. Write any expression to continue.
164 |
165 | ),
166 | winCondition: () => true,
167 | },
168 | {
169 | title: 'Left-associativity',
170 | prompt: (
171 |
172 |
Repeated applications in the lambda calculus are what is called left-associative . This means that repeated applications are evaluated from left to right.
173 |
To make this clearer, if we were to explicity write out the parentheses for the expression abcd
, we'd end up with ((ab)c)d
. That is, in the expression abcd
, a
will first be applied to b
, then the result of ab
will be applied to c
, so on and so forth.
174 |
Write out the parentheses explicitly for ijkmn
175 |
176 | ),
177 | winCondition: ({text}) => {
178 | // Any of these are valid interpretations and we should be permissive rather
179 | // than enforcing dumb bullshit.
180 | return [
181 | '(((ij)k)m)n',
182 | '((((ij)k)m)n)',
183 | '((((i)j)k)m)n',
184 | '(((((i)j)k)m)n)',
185 | ].includes(text.replace(/\s/g, ''));
186 | },
187 | },
188 | {
189 | title: 'Tightly Binding Lambdas',
190 | prompt: (
191 |
192 |
Lambda abstractions have higher prescedence than applications .
193 |
This means that if we write the expression λx.yz
, it would be parenthesized as λx.(yz)
and NOT (λx.y)z
.
194 |
As a rule of thumb, the body of a lambda abstraction (i.e. the part of the lambda expression after the dot) extends all the way to the end of the expression unless parentheses tell it not to.
195 |
Explicitly write the parentheses around λw.xyz
, combining this new knowledge with what you learned in the last question around how applications are parenthesized.
196 |
Solution: λw.((xy)z)
197 |
198 | ),
199 | winCondition: ({text}) => {
200 | return [
201 | 'λw.((xy)z)',
202 | '(λw.((xy)z))',
203 | 'λw.(((x)y)z)',
204 | '(λw.(((x)y)z))',
205 | ].includes(text.replace(/\s/g, ''));
206 | },
207 | },
208 | {
209 | title: 'Applying Lambdas to Variables',
210 | prompt: (
211 |
212 |
So what if we DID want to apply a lambda abstraction to a variable? We'd have to write it out a little more explicity, like we did back in problem 6.
213 |
For example, if we wanted to apply the lambda abstraction λx.y
to variable z
, we'd write it out as (λx.y)z
214 |
Write an expression that applies the lambda abstraction λa.bc
to the variable d
.
215 |
216 | ),
217 | winCondition: ({ast}) => safeEqual(ast, parse('(λa.bc)d')),
218 | },
219 | {
220 | title: 'Applying Variables to Lambdas',
221 | prompt: (
222 |
223 |
Fortunately, the other direction requires fewer parentheses. If we wanted to apply a variable to a lambda abstraction instead of the other way around, we'd just write them right next to each other, like any other application.
224 |
Concretely, applying a
to lambda abstraction λb.c
is written as aλb.c
225 |
Try applying w
to λx.yz
!
226 |
227 | ),
228 | winCondition: ({ast}) => safeEqual(ast, parse('wλx.yz')),
229 | },
230 | {
231 | title: 'Curry',
232 | prompt: (
233 |
234 |
As you may have noticed before, functions can only take one argument, which is kind of annoying.
235 |
Let's say we quite reasonably want to write a function which takes more than one argument. Fortunately, we can sort of get around the single argument restriction by making it so that a function returns another function, which when evaluated subsequently gives you the result. Make sense?
236 |
In practice, this looks like λa.λb. [some expression]
. Go ahead and write any 'multi-argument' function!
237 |
238 | ),
239 | winCondition: ({ast}) => (
240 | ast &&
241 | ast.type === 'function' &&
242 | ast.body.type === 'function'
243 | ),
244 | },
245 | {
246 | title: 'And a Dash of Sugar',
247 | prompt: (
248 |
249 |
Getting the hang of it!
250 |
Representing functions with multiple arguments like this is so convenient, we're going to introduce a special syntax. We'll write λab. [some expression]
as shorthand for λa.λb. [some expression]
. Try writing a function using that syntax!
251 |
252 | ),
253 | winCondition: ({text, ast}) => {
254 | // wow this is a garbage win condition
255 | const isMultiargumentFn = ast &&
256 | ast.type === 'function' &&
257 | ast.body.type === 'function';
258 | if (!isMultiargumentFn) {
259 | return false;
260 | }
261 | // has special syntax.. better way than pulling the lexer??
262 | // this shouldn't throw because by here we're guaranteed ast exists.
263 | const tokenStream = tokenize(text).filter(
264 | // only try to match '(((Lab' and don't care about the rest of the string.
265 | token => token.type !== 'openParen'
266 | );
267 | return tokenStream.length >= 3 &&
268 | tokenStream[0].type === 'lambda' &&
269 | tokenStream[1].type === 'identifier' &&
270 | tokenStream[2].type === 'identifier';
271 | },
272 | },
273 | {
274 | title: 'Summing up Syntax',
275 | prompt: (
276 |
277 |
We've just gone through a whirlwind of syntax in the Lambda Calculus, but fortunately, it's almost everything you need to know.
278 |
As a final challenge for this section on syntax, try writing out the expression that applies the expression aλb.c
to variable d
279 |
280 | ),
281 | winCondition: ({ast}) => safeEqual(ast, parse('(aλb.c)d')),
282 | },
283 | {
284 | title: 'β-reducibility revisited',
285 | prompt: (
286 |
287 |
Let's take a deeper look at Beta Reductions.
288 |
When an expression is an application where the left side is a lambda abstraction , we say that the expression is beta reducible .
289 |
Here are a few examples of beta reducible expressions:
290 |
291 |
292 |
293 | Expression
294 | Explanation
295 |
296 |
297 |
298 | (λx.y)z
Lambda abstraction λx.y
applied to z
299 | (λa.b)λc.d
Lambda abstraction λa.b
applied to λc.d
300 | (λzz.top)λy.ee
Lambda abstraction λz.λz.top
applied to λy.ee
301 |
302 |
303 |
And here are a few examples of expressions that are NOT beta reducible:
304 |
305 |
306 |
307 | Expression
308 | Explanation
309 |
310 |
311 |
312 | zλx.y
Variable z
applied to λx.y
313 | λa.bcd
Lambda abstraction λa.bcd
, but not applied to anything
314 | bee
Application be
applied to e
315 | f(λg.h)i
Application f(λg.h)
applied to i
(This one's tricky! Remember that applications are left-associative).
316 |
317 |
318 |
Write any beta reducible expression that does not appear in the above table.
319 |
320 | ),
321 | winCondition: ({ast}) => {
322 | const rejectList = [
323 | '(λx.y)z',
324 | '(λa.b)λc.d',
325 | '(λz.λz.top)λy.ee',
326 | ];
327 | const isInList = !!rejectList.find(
328 | rejectItem => safeEqual(ast, parse(rejectItem)));
329 | return !isInList && ast && bReduce(ast);
330 | }
331 | },
332 | {
333 | title: 'A more precise look at β-reductions',
334 | prompt: (
335 |
336 |
As you might guess, if something is beta reducible, that means we can perform an operation called beta reduction on the expression.
337 |
Beta reduction works as follows:
338 |
339 |
340 |
341 | Expression
342 | Step
343 |
344 |
345 |
346 | (λa.aba)c
Start with a beta reducible expression.
347 | (λa.cbc)c
In the body of the lambda abstraction, replace every occurrence of the parameter with the argument .
348 | λa.cbc
Erase the argument.
349 | cbc
Erase the head of the lambda expression.
350 |
351 |
352 |
That's all there is to it!
353 |
Write any expression that beta reduces to pp
.
354 |
355 | ),
356 | winCondition: ({ast}) => {
357 | return ast && safeEqual(bReduce(ast), parse('pp'));
358 | },
359 | },
360 | {
361 | title: 'β-reduction function reprise',
362 | prompt: (
363 |
364 |
As we showed in the beginning, this works on functions as well!
365 |
Let's work through an example for a function:
366 |
367 |
368 |
369 | Expression
370 | Step
371 |
372 |
373 |
374 | (λx.yx)λa.a
Start with a beta reducible expression.
375 | (λx.y(λa.a))λa.a
In the body of the lambda abstraction, replace every occurrence of the parameter with the argument .
376 | λx.y(λa.a)
Erase the argument.
377 | y(λa.a)
Erase the head of the lambda expression.
378 |
379 |
380 |
Write any expression that beta reduces to iλj.k
.
381 |
382 | ),
383 | winCondition: ({ast}) => {
384 | return ast && safeEqual(bReduce(ast), parse('i(λj.k)'));
385 | },
386 | },
387 | {
388 | title: 'Bound and Free Variables',
389 | prompt: (
390 |
391 |
It's prudent to make a distinction between bound and free variables. When a function takes an argument, every occurrence of the variable in the body of the function is bound to that parameter.
392 |
For quick example, if you've got the expression λx.xy
, the variable x
is bound in the lambda expression, whereas the variable y
is currently unbound. We call unbound variables like y
free variables .
393 |
Write a lambda expression with a free variable c
(hint: this can be extremely simple).
394 |
395 | ),
396 | winCondition: ({ast}) => ast && getFreeVars(ast).map(item => item.name).includes('c'),
397 | },
398 | {
399 | title: 'α conversions',
400 | prompt: (
401 |
402 |
Easy enough. In this REPL you can see what free variables are in an expression (as well as a lot of other information) by clicking the (+) that appears next to results.
403 |
404 |
It might be obvious that there are multiple ways to write a single lambda abstraction. For example, let's take that identity function we wrote all the way in the beginning, λa.a
. We could have just as easily used x
as the parameter, yielding λx.x
.
405 |
The lambda calculus's word for "renaming a parameter" is alpha-conversion.
406 |
Manually perform an alpha conversion for the expression λz.yz
, by replacing z
with t
407 |
408 | ),
409 | winCondition: ({ast}) => {
410 | return ast && safeEqual(ast, parse('λt.yt'));
411 | },
412 | },
413 | // --- Computation ---
414 | {
415 | title: 'β reductions + α conversions',
416 | prompt: (
417 |
418 |
Occasionally, we'll get into a situation where a variable that previously was unbound is suddenly bound to a parameter that it shouldn't be. For example, if we tried beta-reducing (λab.ab)b
without renaming to resolve the conflict, we'd get λb.bb
. What originally was a free variable b
is now (accidentally) bound to the parameter of the lambda expression!
419 |
To eliminate this conflict, we have to do an alpha-conversion prior to doing the beta reduction.
420 |
Try inputting an expression (like (λab.ab)b
) that requires an alpha conversion to see how the REPL handles this situation.
421 |
422 | ),
423 | // lol this win condition.
424 | winCondition: ({normalForm}) => (
425 | normalForm && renderExpression(normalForm).includes('ε')
426 | ),
427 | },
428 | {
429 | title: "Nested Redexes",
430 | prompt: (
431 |
432 |
Notice that epsilon that pops up? That's this REPL's placeholder variable for when it needs to rename a variable due to a conflict.
433 |
Often, an expression is not beta reducible itself, but contains one or more beta reducible expressions (redexes) nested within. We can still evaluate the expression!
434 |
Try writing a function with a nested redex!
435 |
Possible solution: λa.(λb.b)c
436 |
437 | ),
438 | winCondition: ({ast}) => (
439 | ast && !bReduce(ast) && leftmostOutermostRedex(ast)
440 | ),
441 | },
442 | {
443 | title: "Leftmost Outermost Redex",
444 | prompt: (
445 |
446 |
"But wait," I hear you shout. "What if I have more than one reducible subexpression in my expression? Which do I evaluate first?"
447 |
Let's traverse the expression, left to right, outer scope to inner scope, find the leftmost outermost redex , and evaluate that one. This is called the normal order .
448 |
Try typing and expanding ((λb.b)c)((λd.d)e)
to see what I mean.
449 |
450 | ),
451 | // no need to be super restrictive in what they paste in here
452 | winCondition: ({ast}) => ast && equal(ast, parse('((λb.b)c)((λd.d)e)')),
453 | },
454 | {
455 | title: 'Normal Form',
456 | prompt: (
457 |
458 |
If we do this repeatedly until there's nothing more to reduce, we get to what's called the "normal form". Finding the normal form is analogous to executing the lambda expression, and is in fact exactly what this REPL does when you enter an expression.
459 |
In this REPL you can see the steps it took to get to normal form by pressing the (+) button beside the evaluated expression.
460 |
Type in any expression to continue.
461 |
462 | ),
463 | winCondition: () => true,
464 | },
465 | {
466 | title: 'Or Not',
467 | prompt: (
468 |
469 |
It's possible that this process never halts, meaning that a normal form for that expression doesn't exist.
470 |
See if you can find an expression whose normal form doesn't exist!
471 |
Possible answer: (λa.aa)λa.aa
472 |
473 | ),
474 | winCondition: ({error}) => (
475 | // TODO: make it so errors aren't compared by user string, that's dumb
476 | error && error.message === 'Normal form execution exceeded. This expression may not have a normal form.'
477 | )
478 | },
479 | {
480 | title: 'The Y-Combinator',
481 | prompt: (
482 |
483 |
You can expand that error that pops up to see the first few iterations. If you went with (λa.aa)λa.aa
, you can see that performing a beta reduction gives you the exact same expression back!
484 |
The famed Y-Combinator is one of these expressions without a normal form. Try inputting the Y-Combinator, and see what happens:
485 |
Y: λg.(λx.g(xx))(λx.g(xx))
486 |
487 | ),
488 | winCondition: ({ast}) => equal(ast, parse('λg.(λx.g(xx))(λx.g(xx))')),
489 | },
490 | {
491 | title: "Assigning variables",
492 | prompt: (
493 |
494 |
In the lambda calculus, there's no formal notion of assigning variables, but it's far easier for us to refer to functions by name than just copy/paste the expression every time we want to use it.
495 |
In this REPL, we've added a basic syntax around assign variables. (Note: You can't assign an expression with free variables.)
496 |
This kind of lexical environment around the lambda calculus comes very close to the original sense of a closure , as presented in The mechanical evaluation of expressions .
497 |
Try assigning ID
to your identity function by typing ID := λa.a
498 |
499 | ),
500 | winCondition: ({ast, lhs}) => {
501 | return (
502 | // could probably be simplified by including execution context in winCondition.
503 | ast &&
504 | lhs === 'ID' &&
505 | safeEqual(ast, parse('λa.a'))
506 | );
507 | }
508 | },
509 | {
510 | title: 'Using assigned variables',
511 | prompt: (
512 |
513 |
Now that ID
is defined in the lexical environment , we can use it as if it's a previously bound variable
514 |
Try writing ID b
in order to apply your newly defined identity function to b
, with predictable results.
515 |
516 | ),
517 | winCondition: ({ast}) => (
518 | // we don't really have a good way of testing whether or not
519 | // a certain variable was used, because execution context does var replacement,
520 | // which is kinda bad. whatever. just check if left is identical to ID.
521 | ast &&
522 | ast.type === 'application' &&
523 | safeEqual(ast.left, parse('λa.a'))
524 | ),
525 | },
526 | {
527 | title: "Church Booleans",
528 | prompt: (
529 |
530 |
Now we're well equipped enough to start working with actual, meaningful values.
531 |
Let's start off by introducing the booleans! The two booleans are:
532 |
true: λab.a
533 |
false: λab.b
534 |
You'll notice that these values themselves are just functions. That's true of any value in the lambda calculus -- all values are just functions that take a certain form. They're called the Church booleans, after Alonzo Church, the mathematician who came up with the lambda calculus, as well as these specific encodings.
535 |
It'll be helpful to assign them to TRUE
and FALSE
respectively. Do that.
536 |
537 | ),
538 | winCondition: ({executionContext}) => {
539 | const t = executionContext.definedVariables.TRUE;
540 | const f = executionContext.definedVariables.FALSE;
541 | if (!t || !f) {
542 | return false;
543 | }
544 | return renderAsChurchBoolean(t) === true && renderAsChurchBoolean(f) === false;
545 | },
546 | },
547 | {
548 | title: 'The Not Function',
549 | prompt: (
550 |
551 |
We're gonna work our way to defining the XOR (exclusive or) function on booleans.
552 |
Our first step along the way is to define the NOT function. To do this, let's look at the structure of what a boolean looks like.
553 |
True is just a two parameter function that selects the first, whereas false is just a two parameter function that selects the second argument. We can therefore call a potential true or false value like a function to select either the first or second parameter!
554 |
For example, take the application mxy
. If m
is Church Boolean true, then mxy
beta reduces to x
. However, if m
is Church Boolean false, mxy
beta reduces to y
555 |
Try writing the NOT function, and assign that to NOT
.
556 |
Answer: NOT := λm.m FALSE TRUE
557 |
558 | ),
559 | winCondition: ({ast, lhs}) => (
560 | // should probably be a broader condition-- test for true and false respectively using N.
561 | lhs === 'NOT' && ast && satisfiesTruthTable(
562 | ast,
563 | [
564 | [t, f],
565 | [f, t]
566 | ]
567 | )// safeEqual(ast, parse('λm.m(λa.λb.b)(λa.λb.a)'))
568 | ),
569 | },
570 | {
571 | title: 'The Or Function',
572 | prompt: (
573 |
574 |
Nice! We've now done the heavy mental lifting of how to use the structure of the value to our advantage.
575 |
You should be well equipped enough to come up with the OR function, a function which takes two booleans and outputs true if either of parameters are true, otherwise false.
576 |
Give it a shot, and assign it to OR
577 |
Answer: OR := λmn.m TRUE n
578 |
579 | ),
580 | winCondition: ({ast, lhs}) => (
581 | // same here
582 | lhs === 'OR' && ast && satisfiesTruthTable(
583 | ast,
584 | [
585 | [t, t, t],
586 | [t, f, t],
587 | [f, t, t],
588 | [f, f, f]
589 | ]
590 | )
591 | //safeEqual(ast, parse('λm.λn.m(λa.λb.a)n'))
592 | ),
593 | },
594 | {
595 | title: 'The And Function',
596 | prompt: (
597 |
598 |
Closer and closer.
599 |
This one's very similar to the previous one. See if you can define the AND function, a function which takes two booleans and outputs true if both parameters are true, otherwise false.
600 |
Assign your answer to AND
601 |
Answer: AND := λmn.m n FALSE
602 |
603 | ),
604 | winCondition: ({ast, lhs}) => (
605 | // same here
606 | lhs === 'AND' && ast && satisfiesTruthTable(
607 | ast,
608 | [
609 | [t, t, t],
610 | [t, f, f],
611 | [f, t, f],
612 | [f, f, f]
613 | ]
614 | ) //&& safeEqual(ast, parse('λm.λn.mn(λa.λb.b)'))
615 | ),
616 | },
617 | {
618 | title: 'NAND and NOR',
619 | prompt: (
620 |
621 |
The NOR and NAND functions are the opposite of OR and AND. For example, if AND returns true, NAND returns false, and vice versa. The same follows for OR and NOR
622 |
Since we've already defined the NOT
, AND
, and OR
functions, we can just compose those together to get NAND
and NOR
623 |
Define NAND and NOR, and assign them to NAND
and NOR
.
624 |
Answers:
625 |
NOR := λab. NOT (OR a b)
626 |
NAND := λab. NOT (AND a b)
627 |
628 | ),
629 | winCondition: ({executionContext}) => {
630 | const nor = executionContext.definedVariables.NOR;
631 | const nand = executionContext.definedVariables.NAND;
632 | if (!nor || !nand) {
633 | return false;
634 | }
635 | return satisfiesTruthTable(
636 | nor,
637 | [
638 | [t, t, f],
639 | [t, f, f],
640 | [f, t, f],
641 | [f, f, t],
642 | ]
643 | ) && satisfiesTruthTable(
644 | nand,
645 | [
646 | [t, t, f],
647 | [t, f, t],
648 | [f, t, t],
649 | [f, f, t],
650 | ]
651 | );
652 | },
653 | },
654 | {
655 | title: 'Composing them all together',
656 | prompt: (
657 |
658 |
One last step!
659 |
For reference, the XOR operation is true iff one parameter or the other is true, but not both. So XOR(true, false)
would be true, but XOR(true, true)
would be false.
660 |
Let's see if you can translate that into a composition of the functions you've defined so far. Assign your answer to XOR
661 |
(There is, of course, a simpler way of defining XOR
without composing functions, and that will work here too)
662 |
Answer: XOR := λmn. AND (OR m n) (NAND m n)
663 |
664 | ),
665 | winCondition: ({ast, lhs}) => (
666 | // The likelihood that they got this exact one is pretty small... we really need to define truth tables.
667 | lhs === 'XOR' && ast && satisfiesTruthTable(
668 | ast,
669 | [
670 | [t, t, f],
671 | [t, f, t],
672 | [f, t, t],
673 | [f, f, f]
674 | ]
675 | )
676 | ),
677 | },
678 | {
679 | title: 'Defining numbers',
680 | prompt: (
681 |
682 |
Well, that was a marathon. Take a little break, you've earned it.
683 |
Now we're getting into the meat of it. We can encode numbers in the lambda calculus. Church numerals are 2 parameter functions in the following format:
684 |
685 |
686 | {`
687 | 0: λfn.n
688 | 1: λfn.f(n)
689 | 2: λfn.f(f(n))
690 | 3: λfn.f(f(f(n)))
691 | `}
692 |
693 |
694 |
Write Church Numeral 5
695 |
Answer: λfn.f(f(f(f(fn))))
696 |
697 | ),
698 | winCondition: ({ast}) => ast && (renderAsChurchNumeral(ast) === 5),
699 | },
700 | {
701 | title: 'The Successor Function',
702 | prompt: (
703 |
704 |
We can write functions for these numbers. For example, let's look at the successor function , a function which simply adds 1 to its argument.
705 |
If you're feeling brave, you can attempt to write the successor function yourself. It's a pretty interesting exercise. Otherwise, just copy/paste from the answer key, but feel a little defeated while doing so.
706 |
Answer: λn.λf.λx.f(nfx)
707 |
708 | ),
709 | winCondition: ({ast}) => ast && satisfiesTruthTable(
710 | ast,
711 | [
712 | [parse('λfn.n'), parse('λfn.fn')],
713 | [parse('λfn.fn'), parse('λfn.f(f(n))')],
714 | [parse('λfn.f(f(n))'), parse('λfn.f(f(f(n)))')],
715 | [parse('λfn.f(f(f(n)))'), parse('λfn.f(f(f(f(n))))')],
716 | ]
717 | ),
718 | },
719 | {
720 | title: "The Successor Function(cot'd)",
721 | prompt: (
722 |
723 |
So here's what we just did: Let's say we were adding 1 to λfn.f(f(f(f(n))))
. We just wrote a function that replaced all the f
's with f
's again, and then replaced the n
with a f(n)
, thus creating a stack one higher than we had before! Magic!
724 |
Assign the successor function to SUCC
, we'll need it later
725 |
726 | ),
727 | winCondition: ({executionContext}) => (
728 | executionContext.definedVariables.SUCC && satisfiesTruthTable(
729 | executionContext.definedVariables.SUCC,
730 | [
731 | [parse('λfn.n'), parse('λfn.fn')],
732 | [parse('λfn.fn'), parse('λfn.f(f(n))')],
733 | [parse('λfn.f(f(n))'), parse('λfn.f(f(f(n)))')],
734 | [parse('λfn.f(f(f(n)))'), parse('λfn.f(f(f(f(n))))')],
735 | ]
736 | )
737 | ),
738 | },
739 | {
740 | title: "Adding Numbers bigger than 1",
741 | prompt: (
742 |
743 |
The nice thing about Church numerals as we've defined them is they encode "compose this function n times", so in order to compose a function 3 times, just apply the target function to the Church numeral 3.
744 |
For example, let's say we had the function APPLY_C := λa.a c
that applied free variable c
to whatever function was passed in. If we wanted to write a function that applied c 3 times, we would write (λfn.f(f(fn))) APPLY_C
745 |
Write the "add 4" function by composing the successor function 4 times.
746 |
747 | ),
748 | winCondition: ({ast}) => (
749 | ast && satisfiesTruthTable(
750 | ast,
751 | [
752 | [parse('λfn.n'), parse('λfn.f(f(f(f(n))))')],
753 | [parse('λfn.fn'), parse('λfn.f(f(f(f(f(n)))))')],
754 | [parse('λfn.f(fn)'), parse('λfn.f(f(f(f(f(f(n))))))')],
755 | ]
756 | )
757 | ),
758 | },
759 | {
760 | title: "Defining the Addition Function",
761 | prompt: (
762 |
763 |
What's convenient about this is in order to add the numbers a
and b
, we just create the (add a)
function and apply it to b
764 |
You can take this structure and abstract it out a little, turning it into a function.
765 |
Go ahead and define ADD
to be your newly crafted addition function.
766 |
Answer: ADD := λab.a SUCC b
767 |
768 | ),
769 | winCondition: ({lhs, ast}) => (
770 | lhs === 'ADD' && ast && satisfiesTruthTable(
771 | ast,
772 | [
773 | [parse('λfn.n'), parse('λfn.n'), parse('λfn.n')],
774 | [parse('λfn.f(n)'), parse('λfn.f(n)'), parse('λfn.f(fn)')],
775 | [parse('λfn.f(f(n))'), parse('λfn.f(f(f(n)))'), parse('λfn.f(f(f(f(f(n)))))')],
776 | ]
777 | )
778 | ),
779 | },
780 | {
781 | title: "Defining the Multiplication Function",
782 | prompt: (
783 |
784 |
Let's go ahead write the Multiply function by composing adds together. One possible way to think about a multiply function that takes x
and y
"Compose the Add x
function y
times, and evaluate that at zero".
785 |
Go ahead and assign that to MULT
786 |
Answer: MULT := λab.b(ADD a)λfn.n
787 |
788 | ),
789 | winCondition: ({lhs, ast}) => (
790 | lhs === 'MULT' && ast && satisfiesTruthTable(
791 | ast,
792 | [
793 | [parse('λfn.n'), parse('λfn.n'), parse('λfn.n')],
794 | [parse('λfn.f(n)'), parse('λfn.f(n)'), parse('λfn.f(n)')],
795 | [parse('λfn.f(f(n))'), parse('λfn.f(f(f(n)))'), parse('λfn.f(f(f(f(f(fn)))))')],
796 | ]
797 | )
798 | )
799 | },
800 | {
801 | title: "To Exponentiation!",
802 | prompt: (
803 |
804 |
This shouldn't be too difficult, as it's very similar to the previous problem.
805 |
Compose together a bunch of multiplications, for some starting position to get the exponentiation function. What's cool is that constructing the exponentiation this way means the function behaves correctly for the number 0 straight out of the box, without eta-reduction
806 |
Assign your exponentiation function to EXP to win, and complete the tutorial.
807 |
Answer is: EXP := λab.b (MULT a) λfn.fn
808 |
809 | ),
810 | winCondition: ({lhs, ast}) => (
811 | lhs === 'EXP' && ast && satisfiesTruthTable(
812 | ast,
813 | [
814 | [parse('λfn.n'), parse('λfn.fn'), parse('λfn.n')],
815 | [parse('λfn.f(n)'), parse('λfn.f(n)'), parse('λfn.f(n)')],
816 | [parse('λfn.f(f(n))'), parse('λfn.n'), parse('λfn.f(n)')],
817 | [parse('λfn.f(f(n))'), parse('λfn.f(f(f(n)))'), parse('λfn.f(f(f(f(f(f(f(fn)))))))')],
818 | ]
819 | )
820 | )
821 | },
822 | {
823 | title: "Challenges",
824 | prompt: (
825 |
826 |
You made it through! Not bad at all!
827 |
Miscellaneous Challenges:
828 |
(full disclosure: I haven't attempted these)
829 |
1: Write the Subtract 1 function. (there are a number of tutorials you can find on this on the internet)
830 |
2: Write the Max(a, b)
function, a function that takes two numbers and outputs the larger of the two.
831 |
3: Write a function that computes the decimal equivalent of its input in Gray code . In other words, compute A003188
832 |
833 | ),
834 | winCondition: () => false,
835 | },
836 | ];
837 |
--------------------------------------------------------------------------------
/src/game/problems/readme.md:
--------------------------------------------------------------------------------
1 | Problems for the game
2 |
3 | The interface I want to go for is where each of these problems is an object containing a `prompt` string and a `winCondition` function. the `winCondition` function should take an object containing... uhh, maybe {inputString, betaReduced, ...}, maybe a computation, and return whether or not the problem is in a win state. There's not too much of a downside to having the win condition possibly depend on lots of stuff.
4 |
5 | This interface is probably insufficient, but will be good enough to get a basic version.
6 |
7 | Each one of these should be approximately a problem
8 |
9 | ## Basics
10 | - Identifiers
11 | - Parentheses
12 | - Lambda expression syntax.
13 | - Free Variables.
14 | - Multiple argument functions / currying
15 | - The "apply to itself" function, to illustrate the copy paste aspect of it.
16 | - Beta reductions with functions as the input.
17 | - Replacing variable names when we need to, via alpha conversion.
18 | - Leftmost Outermost Redex (normal order)
19 | - Normal form (no redexes exist);
20 | - Functions with no normal form, i.e. (λa.aa)λb.bb
21 | - The Y-Combinator, where there is no normal form!
22 |
23 | - Binding variables in the global scope (an extension of the lambda calculus)
24 | - Reusing variables
25 | -- (maybe say what variables we've defined???)
26 |
27 | ## Boolean expressions: building the XOR operator.
28 | - How do we define true and false? Let's put them both into
29 | - First notice that true/false is essentially a (first argument) vs (second argument) choose operator.
30 | - In a sense, using a 'true/false' value is kind of like a ternary operator. should result in
31 | - Use this knowledge to build the not operator -- should get N: (λe.eft)
32 | - Build And -- should get A: (λa.λb.abf)
33 | - Build Or -- should get O: (λa.λb.atb)
34 | - We can build NAND or NOR out of this. (λa.λb.O(A(Na)b)(A(Nb)a))
35 |
36 | ## Numbers: Building the Exponentiation function
37 | - Defining numbers
38 | - Summation function. This is hard... S: (λi.(λf.λx.if(fx)))
39 | [...]
40 | - Compose N function (comes out nice and simple). looking for as C := (λf.λn.nf)
41 | - Compose N Successor functions together. looking for A := (CS)
42 | - Compose M (add N) functions together, evaluate at 0 to get M multiplied by N...
43 | -- Start off with a set N.
44 | -- then abstract those away
45 | -- looking for: M := (λm.(λn.(C(Am)n)λa.λb.b))
46 |
47 | - Compose M (multiply by N) functions together, evaluate at 1 to get M^N
48 | -- Start off with a set N
49 | -- then abstract away
50 | -- looking for E := λm.λn.((C(Mm)n)λa.λb.a(b))
51 |
52 | - Let's try exponentiation with the number 0 in certain places. Do we get what we expect?
53 | -- We do get it!
54 |
55 | ## More obscure data types
56 | - List
57 | - Pair
58 |
59 |
60 | ## Challenges (i have no idea how hard these are)
61 | - Write the [Max(n, n)] function
62 | - Gray encoding
63 | - Write the [bitwise xor] function
64 |
--------------------------------------------------------------------------------
/src/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import generateGoldens from './util/generateGoldens';
4 |
5 | window.generateGoldens = generateGoldens;
6 |
7 | import App from './components/App';
8 |
9 | document.addEventListener("DOMContentLoaded", () => {
10 | ReactDOM.render( , document.getElementById('react-mount'));
11 | });
12 |
--------------------------------------------------------------------------------
/src/lambdaActor/actor.js:
--------------------------------------------------------------------------------
1 | import ExecutionContext from "./executionContext.js";
2 |
3 | // a lite wrapper around the executionContext, with time limits hopefully imposed
4 | // termination should be necessary or something.
5 | export default class LambdaActor {
6 | constructor() {
7 | this.executionContext = new ExecutionContext();
8 | this.executionContext.receive = this._postBack;
9 | }
10 |
11 | send = (text) => {
12 | this.executionContext.send(text);
13 | };
14 |
15 | _postBack = (msg) => {
16 | this.receive(msg);
17 | };
18 |
19 | // to be overwritten
20 | receive = () => {};
21 | }
22 |
--------------------------------------------------------------------------------
/src/lambdaActor/astToMetadata.js:
--------------------------------------------------------------------------------
1 | import {
2 | getFreeVars,
3 | renderExpression,
4 | bReduce,
5 | eReduce,
6 | renderAsChurchNumeral,
7 | renderAsChurchBoolean,
8 | toNormalForm,
9 | leftmostOutermostRedex,
10 | tokenize,
11 | } from "../lib/lambda/index.ts";
12 |
13 | function astToMetadata(ast){
14 | const freeVars = getFreeVars(ast);
15 | const renderedFromAst = renderExpression(ast);
16 | const betaReduced = bReduce(ast);
17 | const etaReduced = eReduce(ast);
18 | const asNumeral = renderAsChurchNumeral(ast);
19 | const asBoolean = renderAsChurchBoolean(ast);
20 | const normalForm = toNormalForm(ast);
21 | const normAsNumeral = renderAsChurchNumeral(normalForm);
22 | const normAsBoolean = renderAsChurchBoolean(normalForm);
23 |
24 | // -- Steps to recreate
25 | const maxDepth = 1000;
26 | let stepsToNormal = [ast];
27 | for (let i = 0; i < maxDepth; i++) {
28 | const nextStep = leftmostOutermostRedex(stepsToNormal[stepsToNormal.length - 1]);
29 | if (nextStep === undefined) {
30 | break;
31 | }
32 | stepsToNormal.push(nextStep);
33 | }
34 |
35 | return ({
36 | freeVars,
37 | renderedFromAst,
38 | betaReduced,
39 | etaReduced,
40 | asNumeral,
41 | asBoolean,
42 | normalForm,
43 | normAsNumeral,
44 | normAsBoolean,
45 | stepsToNormal,
46 | });
47 | };
48 |
49 | export default astToMetadata;
50 |
--------------------------------------------------------------------------------
/src/lambdaActor/errors.js:
--------------------------------------------------------------------------------
1 | export class FreeVarInDefinitionError extends Error {
2 | constructor(message){
3 | super(message);
4 | this.name = 'FreeVarInDefinitionError';
5 | }
6 | }
--------------------------------------------------------------------------------
/src/lambdaActor/executionContext.js:
--------------------------------------------------------------------------------
1 | // might be cool to do this within lib/lambda.
2 | // import MetadataWorker from 'worker-loader?inline!./worker.js';
3 | // import TimeoutWatcher from './TimeoutWatcher';
4 | // stick these in on move to async interface.
5 | import astToMetadata from "./astToMetadata.js";
6 | import { FreeVarInDefinitionError } from "./errors.js";
7 |
8 | import {
9 | parseExtendedSyntax,
10 | getFreeVars,
11 | replace,
12 | toNormalForm,
13 | } from "../lib/lambda/index.ts";
14 |
15 | // There's a better way of doing this I swear.
16 | // Might want to make a whole "Execution" object
17 | class ExecutionContext {
18 |
19 | constructor(){
20 | this.definedVariables = {};
21 | // this.metadataWrapper = new TimeoutWatcher(MetadataWorker);
22 | // this.metadataWrapper.receive = msg => this._handleMetadataMessage(msg);
23 | }
24 |
25 | getResolvableVariables(ast){
26 | // holy fuck there is so much badness in here.
27 | return getFreeVars(ast).map(token => token.name).filter(
28 | name => this.definedVariables[name] !== undefined
29 | );
30 | }
31 |
32 | getUnresolvableVariables(ast){
33 | return getFreeVars(ast).map(token => token.name).filter(
34 | name => this.definedVariables[name] === undefined
35 | )
36 | }
37 |
38 | defineVariableFromString(name, string){
39 | const ast = parseTerm(string);
40 | this.defineVariable(name, ast);
41 | }
42 |
43 | // Defined variables must contain no unresolvableVariables
44 | // This is so that variable resolution is guaranteed to halt at some point.
45 | defineVariable(name, ast){
46 | if(this.getUnresolvableVariables(ast).length > 0){
47 | const unresolvables = this.getUnresolvableVariables(ast).join(', ');
48 | throw new FreeVarInDefinitionError('Name Error: Expression contains free variables ' + unresolvables + '. Assigned values cannot have free variables in this REPL.');
49 | }
50 | this.definedVariables[name] = ast;
51 | }
52 |
53 | clearVariables(){
54 | this.definedVariables = {};
55 | }
56 |
57 | // string => computationData
58 | // a computationData is loosely defined right now-- kind of a grab bag of an object.
59 | evaluate(text){
60 | let ast;
61 | try {
62 | // lambda + assignment
63 | ast = parseExtendedSyntax(text);
64 | if (ast.type === 'assignment') {
65 | const lhs = ast.lhs;
66 | ast = ast.rhs;
67 | ast = this.resolveVariables(ast);
68 | this.defineVariable(lhs, ast);
69 | // duped, but we can continue separating them.
70 | this._postBack({
71 | type: 'assignment',
72 | text,
73 | lhs,
74 | ast,
75 | });
76 | } else {
77 | ast = this.resolveVariables(ast);
78 | const metadata = astToMetadata(ast);
79 | this._postBack({
80 | type: 'computation',
81 | text,
82 | ast,
83 | ...metadata
84 | });
85 | }
86 | } catch(error){
87 | // we pass AST, executionContext because in the case that we parsed
88 | // successfully, we still want to be able to use it in win conditions
89 |
90 | // TODO: Make sure max call stack doesn't really happen, or is handled
91 | // This looks like: (λa.aa)(λa.aaa)
92 | this._postBack({
93 | type: 'error',
94 | error,
95 | text,
96 | ast,
97 | });
98 | }
99 | }
100 |
101 | _handleMetadataMessage(msg){
102 | this._postBack(msg);
103 | }
104 |
105 | // ast => ast
106 | resolveVariables(ast){
107 | let currentAst = ast;
108 | // this could be much faster. Let's do clever stuff after this works.
109 | let resolvableVars = this.getResolvableVariables(ast);
110 | while(resolvableVars.length > 0){
111 | const toResolve = resolvableVars[0];
112 | currentAst = replace(
113 | toResolve,
114 | this.definedVariables[toResolve],
115 | currentAst
116 | );
117 | resolvableVars = this.getResolvableVariables(currentAst);
118 | }
119 | return currentAst;
120 | }
121 |
122 | // should be replaced with a subscribe handler.
123 | receive = () => {};
124 |
125 | send = (text) => {
126 | this.evaluate(text);
127 | };
128 |
129 | _postBack = (msg) => {
130 | this.receive(msg);
131 | };
132 | }
133 |
134 | export default ExecutionContext;
135 |
--------------------------------------------------------------------------------
/src/lambdaActor/timeoutWatcher.js:
--------------------------------------------------------------------------------
1 | export default class TimeoutWatcher {
2 | constructor(WorkerClass, timeout = 5000){
3 | this.WorkerClass = WorkerClass;
4 | this.timeout = timeout;
5 | }
6 |
7 | _postBack = (msg) => {
8 | this.receive(msg);
9 | }
10 |
11 | send = (msg) => {
12 | const workerInst = new this.WorkerClass();
13 | const watchdog = setTimeout(() => {
14 | workerInst.terminate();
15 | this._postBack({
16 | type: 'error',
17 | error: { message: 'Runtime Error: Timeout exceeded' },
18 | text: msg.text,
19 | ast: msg.ast,
20 | });
21 | }, this.timeout);
22 | workerInst.postMessage(JSON.stringify(msg));
23 | workerInst.onmessage = e => {
24 | clearTimeout(watchdog);
25 | this._postBack(JSON.parse(e.data));
26 | workerInst.terminate();
27 | }
28 | };
29 |
30 | receive = () => {};
31 | }
32 |
--------------------------------------------------------------------------------
/src/lambdaActor/worker.js:
--------------------------------------------------------------------------------
1 | // should be moved back probs.
2 | import astToMetadata from "./astToMetadata.js";
3 |
4 | onmessage = function(e) {
5 | const { ast, text } = JSON.parse(e.data);
6 | let metadata;
7 | try {
8 | metadata = astToMetadata(ast);
9 | postMessage(JSON.stringify({
10 | type: 'computation',
11 | text,
12 | ast,
13 | ...metadata
14 | }));
15 | } catch(err) {
16 | if (err instanceof Error) {
17 | // json serialized errors are fun.
18 | err = { message: err.toString() };
19 | }
20 | postMessage(JSON.stringify({
21 | type: 'error',
22 | error: err,
23 | text,
24 | ast,
25 | }))
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/lib/lambda/__tests__/bReduction.spec.js:
--------------------------------------------------------------------------------
1 | import { assert } from "chai";
2 | import { bReduce } from "../operations.js";
3 | import { purgeAstCache } from "../util.js";
4 | import { parseTerm } from "../parser.js";
5 |
6 | describe("Beta Reductions", function () {
7 | it("Beta reduces a redex", function () {
8 | const ast = {
9 | type: "application",
10 | left: {
11 | type: "function",
12 | argument: "a",
13 | body: { type: "variable", name: "a" },
14 | },
15 | right: { type: "variable", name: "b" },
16 | };
17 | const expected = { type: "variable", name: "b" };
18 | assert.deepEqual(purgeAstCache(bReduce(ast)), expected);
19 | });
20 |
21 | it("Avoids name conflicts when executing beta reductions", function () {
22 | // TODO: make this more robust with canonization, so it's not tied to specific implementation.
23 | const ast = {
24 | type: "application",
25 | left: {
26 | type: "function",
27 | argument: "a",
28 | body: {
29 | type: "function",
30 | argument: "b",
31 | body: {
32 | type: "application",
33 | left: { type: "variable", name: "a" },
34 | right: { type: "variable", name: "b" },
35 | },
36 | },
37 | },
38 | right: { type: "variable", name: "b" },
39 | };
40 | const expected = {
41 | type: "function",
42 | argument: "ε₁",
43 | body: {
44 | type: "application",
45 | left: { type: "variable", name: "b" },
46 | right: { type: "variable", name: "ε₁" },
47 | },
48 | };
49 | assert.deepEqual(purgeAstCache(bReduce(ast)), expected);
50 | });
51 |
52 | it("Avoids name conflicts when first chosen name is shadowed in an inner scope", () => {
53 | const ast = {
54 | type: "application",
55 | left: {
56 | type: "function",
57 | argument: "a",
58 | body: {
59 | type: "function",
60 | argument: "ε₁",
61 | body: {
62 | type: "application",
63 | left: {
64 | type: "variable",
65 | name: "a",
66 | },
67 | right: {
68 | type: "variable",
69 | name: "ε₁",
70 | },
71 | },
72 | },
73 | },
74 | right: {
75 | type: "variable",
76 | name: "ε₁",
77 | },
78 | };
79 | const expected = {
80 | type: "function",
81 | argument: "ε₂",
82 | body: {
83 | type: "application",
84 | left: {
85 | type: "variable",
86 | name: "ε₁",
87 | },
88 | right: {
89 | type: "variable",
90 | name: "ε₂",
91 | },
92 | },
93 | };
94 | assert.deepEqual(purgeAstCache(bReduce(ast)), expected);
95 | });
96 |
97 | it("Avoids name conflicts when first chosen name conflicts with free var in replacer", () => {
98 | const ast = {
99 | type: "application",
100 | left: {
101 | type: "function",
102 | argument: "a",
103 | body: {
104 | type: "function",
105 | argument: "b",
106 | body: {
107 | type: "application",
108 | left: {
109 | type: "variable",
110 | name: "a",
111 | },
112 | right: {
113 | type: "variable",
114 | name: "b",
115 | },
116 | },
117 | },
118 | },
119 | right: {
120 | type: "application",
121 | left: {
122 | type: "variable",
123 | name: "b",
124 | },
125 | right: {
126 | type: "variable",
127 | name: "ε₁",
128 | },
129 | },
130 | };
131 | const expected = {
132 | type: "function",
133 | argument: "ε₂",
134 | body: {
135 | type: "application",
136 | left: {
137 | type: "application",
138 | left: {
139 | type: "variable",
140 | name: "b",
141 | },
142 | right: {
143 | type: "variable",
144 | name: "ε₁",
145 | },
146 | },
147 | right: {
148 | type: "variable",
149 | name: "ε₂",
150 | },
151 | },
152 | };
153 | assert.deepEqual(purgeAstCache(bReduce(ast)), expected);
154 | });
155 |
156 | it("Avoids name conflicts when there are conflicting free vars in both", () => {
157 | const ast = {
158 | type: "application",
159 | left: {
160 | type: "function",
161 | argument: "a",
162 | body: {
163 | type: "function",
164 | argument: "ε₁",
165 | body: {
166 | type: "application",
167 | left: {
168 | type: "application",
169 | left: {
170 | type: "variable",
171 | name: "a",
172 | },
173 | right: {
174 | type: "variable",
175 | name: "ε₁",
176 | },
177 | },
178 | right: {
179 | type: "variable",
180 | name: "ε₂",
181 | },
182 | },
183 | },
184 | },
185 | right: {
186 | type: "variable",
187 | name: "ε₁",
188 | },
189 | };
190 | const expected = {
191 | type: "function",
192 | argument: "ε₃",
193 | body: {
194 | type: "application",
195 | left: {
196 | type: "application",
197 | left: {
198 | type: "variable",
199 | name: "ε₁",
200 | },
201 | right: {
202 | type: "variable",
203 | name: "ε₃",
204 | },
205 | },
206 | right: {
207 | type: "variable",
208 | name: "ε₂",
209 | },
210 | },
211 | };
212 | assert.deepEqual(purgeAstCache(bReduce(ast)), expected);
213 | });
214 |
215 | it("avoids name conflict in this odd specific case", () => {
216 | const ast = {
217 | type: "application",
218 | left: {
219 | type: "function",
220 | argument: "ε₁",
221 | body: {
222 | type: "function",
223 | argument: "a",
224 | body: {
225 | type: "function",
226 | argument: "ε₁",
227 | body: { type: "variable", name: "a" },
228 | },
229 | },
230 | },
231 | right: { type: "variable", name: "a" },
232 | };
233 | const expected = {
234 | type: "function",
235 | argument: "ε₂",
236 | body: {
237 | type: "function",
238 | argument: "ε₁",
239 | body: { type: "variable", name: "ε₂" },
240 | },
241 | };
242 | assert.deepEqual(purgeAstCache(bReduce(ast)), expected);
243 | });
244 | });
245 |
--------------------------------------------------------------------------------
/src/lib/lambda/__tests__/cannonize.spec.js:
--------------------------------------------------------------------------------
1 | import { assert } from 'chai';
2 | import { cannonize } from "../cannonize.js";
3 |
4 | describe('Cannonize', function(){
5 | it('cannonizes two alpha-equivalent church numerals equivalently', function(done){
6 | const a = {"type":"function","argument":"a","body":{"type":"function","argument":"b","body":{"type":"application","left":{"type":"variable","name":"a"},"right":{"type":"application","left":{"type":"variable","name":"a"},"right":{"type":"application","left":{"type":"variable","name":"a"},"right":{"type":"application","left":{"type":"variable","name":"a"},"right":{"type":"application","left":{"type":"variable","name":"a"},"right":{"type":"application","left":{"type":"variable","name":"a"},"right":{"type":"variable","name":"b"}}}}}}}}};
7 | const b = {"type":"function","argument":"c","body":{"type":"function","argument":"d","body":{"type":"application","left":{"type":"variable","name":"c"},"right":{"type":"application","left":{"type":"variable","name":"c"},"right":{"type":"application","left":{"type":"variable","name":"c"},"right":{"type":"application","left":{"type":"variable","name":"c"},"right":{"type":"application","left":{"type":"variable","name":"c"},"right":{"type":"application","left":{"type":"variable","name":"c"},"right":{"type":"variable","name":"d"}}}}}}}}};
8 | assert.deepEqual(cannonize(a), cannonize(b));
9 | done();
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/src/lib/lambda/__tests__/generated_suite.data.js:
--------------------------------------------------------------------------------
1 | const suiteData = [
2 | {"text":"a₁","normalForm":{"type":"variable","name":"a₁"}},
3 | {"text":"a₁b₁","normalForm":{"type":"application","left":{"type":"variable","name":"a₁"},"right":{"type":"variable","name":"b₁"}}},
4 | {"text":"(λabc.d)","normalForm":{"type":"function","argument":"a","body":{"type":"function","argument":"b","body":{"type":"function","argument":"c","body":{"type":"variable","name":"d"}}}}},
5 | {"text":"(λa.a)b","normalForm":{"type":"variable","name":"b"}},
6 | {"text":"(λa.a)λb.b","normalForm":{"type":"function","argument":"b","body":{"type":"variable","name":"b"}}},
7 | {"text":"((λb.b)c)((λd.d)e)","normalForm":{"type":"application","left":{"type":"variable","name":"c"},"right":{"type":"variable","name":"e"}}},
8 | {"text":"(λab.a(a(a(a(a(a(ab)))))))(λn.λf.λx.f(nfx))(λcd.c(c(c(c(c(c(c(c(cd)))))))))","normalForm":{"type":"function","argument":"f","body":{"type":"function","argument":"x","body":{"type":"application","left":{"type":"variable","name":"f"},"right":{"type":"application","left":{"type":"variable","name":"f"},"right":{"type":"application","left":{"type":"variable","name":"f"},"right":{"type":"application","left":{"type":"variable","name":"f"},"right":{"type":"application","left":{"type":"variable","name":"f"},"right":{"type":"application","left":{"type":"variable","name":"f"},"right":{"type":"application","left":{"type":"variable","name":"f"},"right":{"type":"application","left":{"type":"variable","name":"f"},"right":{"type":"application","left":{"type":"variable","name":"f"},"right":{"type":"application","left":{"type":"variable","name":"f"},"right":{"type":"application","left":{"type":"variable","name":"f"},"right":{"type":"application","left":{"type":"variable","name":"f"},"right":{"type":"application","left":{"type":"variable","name":"f"},"right":{"type":"application","left":{"type":"variable","name":"f"},"right":{"type":"application","left":{"type":"variable","name":"f"},"right":{"type":"application","left":{"type":"variable","name":"f"},"right":{"type":"variable","name":"x"}}}}}}}}}}}}}}}}}}}},
9 | {"text":"(λgh.g(g(gh)))(λab.b((λlm.l(λn.λf.λx.f(nfx))m)a)λde.e)(λij.i(i(ij)))","normalForm":{"type":"function","argument":"b","body":{"type":"application","left":{"type":"application","left":{"type":"variable","name":"b"},"right":{"type":"function","argument":"m","body":{"type":"function","argument":"e","body":{"type":"variable","name":"e"}}}},"right":{"type":"function","argument":"d","body":{"type":"function","argument":"e","body":{"type":"variable","name":"e"}}}}}},
10 | {"text":"(λab.b((λlm.l(λn.λf.λx.f(nfx))m)a)λde.e)(λgh.g(g(gh)))(λij.i(i(ij)))","normalForm":{"type":"function","argument":"f","body":{"type":"function","argument":"x","body":{"type":"application","left":{"type":"variable","name":"f"},"right":{"type":"application","left":{"type":"variable","name":"f"},"right":{"type":"application","left":{"type":"variable","name":"f"},"right":{"type":"application","left":{"type":"variable","name":"f"},"right":{"type":"application","left":{"type":"variable","name":"f"},"right":{"type":"application","left":{"type":"variable","name":"f"},"right":{"type":"application","left":{"type":"variable","name":"f"},"right":{"type":"application","left":{"type":"variable","name":"f"},"right":{"type":"application","left":{"type":"variable","name":"f"},"right":{"type":"variable","name":"x"}}}}}}}}}}}}},
11 | {"text":"(λn.λf.λx.n((λf.λp.(λx.λy.λf.fxy)(f(p(λa.λb.a)))(p(λa.λb.a)))f)((λx.λy.λf.fxy)xx)(λa.λb.b))(λf.λn.f(f(fn)))","normalForm":{"type":"function","argument":"f","body":{"type":"function","argument":"x","body":{"type":"application","left":{"type":"variable","name":"f"},"right":{"type":"application","left":{"type":"variable","name":"f"},"right":{"type":"variable","name":"x"}}}}}},
12 | {"text":"(λa.λb.b(λn.λf.λx.n((λf.λp.(λx.λy.λf.fxy)(f(p(λa.λb.a)))(p(λa.λb.a)))f)((λx.λy.λf.fxy)xx)(λa.λb.b))a)((λn.λf.λx.f(nfx))((λn.λf.λx.f(nfx))((λn.λf.λx.f(nfx))((λn.λf.λx.f(nfx))((λn.λf.λx.f(nfx))((λn.λf.λx.f(nfx))(λf.λn.n)))))))((λn.λf.λx.f(nfx))((λn.λf.λx.f(nfx))((λn.λf.λx.f(nfx))((λn.λf.λx.f(nfx))(λf.λn.n)))))","normalForm":{"type":"function","argument":"f","body":{"type":"function","argument":"x","body":{"type":"application","left":{"type":"variable","name":"f"},"right":{"type":"application","left":{"type":"variable","name":"f"},"right":{"type":"variable","name":"x"}}}}}},
13 | ];
14 |
15 | export default suiteData;
16 |
--------------------------------------------------------------------------------
/src/lib/lambda/__tests__/generated_suite.spec.js:
--------------------------------------------------------------------------------
1 | import { assert } from 'chai';
2 | import { toNormalForm } from "../normalize.js";
3 | import { parseTerm } from "../parser.js";
4 | import { purgeAstCache } from "../util.js";
5 | import suiteData from "./generated_suite.data.js";
6 |
7 | /*
8 | Tests evaluation of expressions end to end based on previous versions.
9 | so like, integration tests or somethin, but just makes sure stuff doesn't unintentially change.
10 | Make sure to write down when suite was generated.
11 |
12 | assuming empty execution context (no variables to resolve)
13 | suiteData = [
14 | {text: , normalForm: },
15 | ...
16 | ]
17 |
18 | To generate a suite, output text and normalized, don't assign anything, avoid epsilon issues, ensure depth 1000
19 | Later on i'll put in some nice scripts. When i'm not on a plane, i'll look up how to actually do this.
20 | */
21 |
22 | describe('Generated Expression Suite', function(){
23 | it('is unchanged from previous versions', function(done){
24 | suiteData.forEach(datum => {
25 | assert.deepEqual(
26 | purgeAstCache(toNormalForm(parseTerm(datum.text), 1000)),
27 | datum.normalForm
28 | );
29 | });
30 | done();
31 | });
32 | });
33 |
--------------------------------------------------------------------------------
/src/lib/lambda/__tests__/lexer.spec.js:
--------------------------------------------------------------------------------
1 | import { assert } from 'chai';
2 | import { tokenize } from "../lexer.js";
3 |
4 | describe('Lexer', function(){
5 | it('should correctly lex all valid token types', function(done){
6 | const source = 'B := (λa₃.a₃)';
7 | const expected = [
8 | {type: "identifier", value: "B"},
9 | {type: "assignment"},
10 | {type: "openParen"},
11 | {type: "lambda"},
12 | {type: "identifier", value: "a₃"},
13 | {type: "dot"},
14 | {type: "identifier", value: "a₃"},
15 | {type: "closeParen"}
16 | ];
17 | const tokenized = tokenize(source);
18 | assert.deepEqual(tokenized, expected);
19 | done();
20 | })
21 |
22 | it('should lex capital letters as a single token', function(done){
23 | const source = 'FUNC := λABc.dEFg';
24 | const expected = [
25 | {type: 'identifier', value: 'FUNC'},
26 | {type: 'assignment'},
27 | {type: 'lambda'},
28 | {type: 'identifier', value: 'AB'},
29 | {type: 'identifier', value: 'c'},
30 | {type: 'dot'},
31 | {type: 'identifier', value: 'd'},
32 | {type: 'identifier', value: 'EF'},
33 | {type: 'identifier', value: 'g'},
34 | ];
35 | const tokenized = tokenize(source);
36 | assert.deepEqual(tokenized, expected);
37 | done();
38 | });
39 | })
40 |
--------------------------------------------------------------------------------
/src/lib/lambda/__tests__/normalize.spec.js:
--------------------------------------------------------------------------------
1 | import { assert } from 'chai';
2 | import { toNormalForm, leftmostOutermostRedex } from "../normalize.js";
3 |
4 | describe('Normalize', function(){
5 | it('Handles top level beta reductions', function(done){
6 | const ast = {
7 | type: 'application',
8 | left: {
9 | type: 'function',
10 | argument: 'a',
11 | body: { type: 'variable', name: 'a' },
12 | },
13 | right: { type: 'variable', name: 'b' },
14 | };
15 | const expected = { type: 'variable', name: 'b' };
16 | const normalized = toNormalForm(ast);
17 | assert.deepEqual(normalized, expected);
18 | done();
19 | });
20 |
21 | it('Evaluates redexes in normal order', function(done){
22 | const ast = {"type":"application","left":{"type":"application","left":{"type":"function","argument":"b","body":{"type":"variable","name":"b"}},"right":{"type":"variable","name":"c"}},"right":{"type":"application","left":{"type":"function","argument":"d","body":{"type":"variable","name":"d"}},"right":{"type":"variable","name":"e"}}};
23 | const expected = {"type":"application","left":{"type":"variable","name":"c"},"right":{"type":"application","left":{"type":"function","argument":"d","body":{"type":"variable","name":"d"}},"right":{"type":"variable","name":"e"}}};
24 | const redexed = leftmostOutermostRedex(ast);
25 | assert.deepEqual(redexed, expected);
26 | done();
27 | });
28 |
29 | it('fails to find redexes where none exist', function(done){
30 | const ast = {"type":"application","left":{"type":"variable","name":"f"},"right":{"type":"function","argument":"a","body":{"type":"function","argument":"b","body":{"type":"application","left":{"type":"variable","name":"c"},"right":{"type":"function","argument":"d","body":{"type":"variable","name":"e"}}}}}};
31 | const redexed = leftmostOutermostRedex(ast);
32 | assert.deepEqual(undefined, redexed);
33 | done();
34 | });
35 | });
36 |
--------------------------------------------------------------------------------
/src/lib/lambda/__tests__/parser.spec.js:
--------------------------------------------------------------------------------
1 | import { assert } from 'chai';
2 | import { parseExpression, parseStatement } from "../parser.js";
3 |
4 | describe('Parser', function(){
5 | it('correctly parses a lambda expression', function(done){
6 | const tokenStream = [
7 | { type: 'lambda' },
8 | { type: 'identifier', value: 'a' },
9 | { type: 'dot' },
10 | { type: 'identifier', value: 'b' },
11 | ];
12 | const expected = ({
13 | type: 'function',
14 | argument: 'a',
15 | body: { type: 'variable', name: 'b' },
16 | });
17 | assert.deepEqual(parseExpression(tokenStream), expected);
18 | done();
19 | });
20 |
21 | it('correctly parses an assignment statement', function(){
22 | const tokenStream = [
23 | {type: "identifier", value: "B"},
24 | {type: "assignment"},
25 | {type: "identifier", value: "C"},
26 | ];
27 | const expected = ({
28 | type: 'assignment',
29 | lhs: 'B',
30 | rhs: { type: 'variable', name: 'C' },
31 | });
32 | assert.deepEqual(parseStatement(tokenStream), expected);
33 | });
34 |
35 | it('is left associative under application', function(done){
36 | const tokenStream = [
37 | {type: "identifier", value: "a"},
38 | {type: "identifier", value: "b"},
39 | {type: "identifier", value: "c"},
40 | {type: "identifier", value: "d"},
41 | ];
42 | const expected = ({
43 | type: 'application',
44 | left: {
45 | type: 'application',
46 | left: {
47 | type: 'application',
48 | left: { type: 'variable', name: 'a' },
49 | right:{ type: 'variable', name: 'b' },
50 | },
51 | right: { type: 'variable', name: 'c' },
52 | },
53 | right: { type: 'variable', name: 'd' },
54 | });
55 | assert.deepEqual(parseExpression(tokenStream), expected);
56 | done();
57 | });
58 |
59 | it('accepts multi-argument function syntax', function(done){
60 | const tokenStream = [
61 | { type: 'lambda' },
62 | { type: 'identifier', value: 'a' },
63 | { type: 'identifier', value: 'b' },
64 | { type: 'identifier', value: 'c' },
65 | { type: 'dot' },
66 | { type: 'identifier', value: 'd' },
67 | ];
68 | const expected = ({
69 | type: 'function',
70 | argument: 'a',
71 | body: {
72 | type: 'function',
73 | argument:'b',
74 | body: {
75 | type: 'function',
76 | argument:'c',
77 | body: {
78 | type: 'variable',
79 | name: 'd',
80 | },
81 | },
82 | },
83 | });
84 | assert.deepEqual(parseExpression(tokenStream), expected)
85 | done();
86 | });
87 | });
88 |
--------------------------------------------------------------------------------
/src/lib/lambda/__tests__/util.spec.js:
--------------------------------------------------------------------------------
1 | import { assert } from "chai";
2 | import { cacheOnAst, purgeAstCache } from "../util.js";
3 |
4 | describe("Caching Util", function () {
5 | it("creates cache keys on ASTs", function (done) {
6 | const ast = {};
7 | const compute = cacheOnAst((ast) => "value");
8 | compute(ast);
9 | const keys = Object.keys(ast);
10 | assert(keys.length === 1);
11 | assert(keys[0].slice(0, 9) === "__cache__");
12 | done();
13 | });
14 |
15 | it("makes cached functions not recompute", function (done) {
16 | let computeCount = 0;
17 | const compute = cacheOnAst((ast) => {
18 | computeCount++;
19 | return "value!";
20 | });
21 | const ast = {};
22 | const value1 = compute(ast);
23 | const value2 = compute(ast);
24 | assert(computeCount === 1);
25 | assert(value1 === value2);
26 | done();
27 | });
28 |
29 | it("shallow removes caches on the AST", function (done) {
30 | const compute = cacheOnAst(() => "value!");
31 | const source = {
32 | type: "variable",
33 | name: "a",
34 | };
35 | compute(source);
36 | assert.deepEqual(purgeAstCache(source), source);
37 | done();
38 | });
39 |
40 | it("deep removes caches on the AST", function (done) {
41 | const compute = cacheOnAst(() => "value!");
42 | const source = {
43 | type: "application",
44 | left: {
45 | type: "variable",
46 | name: "a",
47 | },
48 | right: {
49 | type: "variable",
50 | name: "b",
51 | },
52 | };
53 | compute(source.left);
54 | assert.deepEqual(purgeAstCache(source), source);
55 | done();
56 | });
57 | });
58 |
--------------------------------------------------------------------------------
/src/lib/lambda/cannonize.ts:
--------------------------------------------------------------------------------
1 | import { replace } from "./operations";
2 | import { cacheOnAst } from "./util";
3 | import { LambdaExpression as Expr } from "./types";
4 |
5 | // Deterministically renames all variables in an expression
6 | // such that if there exists an alpha conversion between two ASTs,
7 | // the cannonized asts are identical
8 | function cannonizeUnmemoized(ast: Expr): Expr {
9 | let count = 0;
10 | return rCannonize(ast);
11 |
12 | function generateNewName() {
13 | count++;
14 | return `[_c${count}]`;
15 | }
16 |
17 | function rCannonize(a: Expr): Expr {
18 | switch (a.type) {
19 | case "variable":
20 | return a;
21 | case "application":
22 | return {
23 | type: "application",
24 | left: rCannonize(a.left),
25 | right: rCannonize(a.right),
26 | };
27 | case "function":
28 | let newName = generateNewName();
29 | return {
30 | type: "function",
31 | argument: newName,
32 | body: rCannonize(
33 | replace(a.argument, { type: "variable", name: newName }, a.body)
34 | ),
35 | };
36 | }
37 | }
38 | }
39 |
40 | export const cannonize = cacheOnAst(cannonizeUnmemoized);
41 |
--------------------------------------------------------------------------------
/src/lib/lambda/churchPrimitives.ts:
--------------------------------------------------------------------------------
1 | import { equal } from "./equality";
2 | import { parseTerm } from "./parser";
3 | import { cannonize } from "./cannonize";
4 | import { LambdaExpression as Expr, Maybe } from "./types";
5 |
6 | // TODO: do the inverse of these -- generation of church primitives
7 | export function renderAsChurchNumeral(uncannonized: Expr): Maybe {
8 | const expression = cannonize(uncannonized);
9 | if (expression.type !== "function") {
10 | return undefined;
11 | }
12 | const outerName = expression.argument;
13 | const inner = expression.body;
14 | if (inner.type !== "function") {
15 | return undefined;
16 | }
17 | const innerName = inner.argument;
18 |
19 | function countLevels(
20 | wrapperName: string,
21 | targetName: string,
22 | piece: Expr
23 | ): Maybe {
24 | if (piece.type === "variable") {
25 | if (piece.name !== targetName) {
26 | return undefined;
27 | } else {
28 | return 0;
29 | }
30 | }
31 | if (piece.type === "application") {
32 | if (piece.left.type !== "variable" || piece.left.name !== wrapperName) {
33 | return undefined;
34 | } else {
35 | const nextLevel = countLevels(wrapperName, targetName, piece.right);
36 | if (nextLevel === undefined) {
37 | return undefined;
38 | } else {
39 | return nextLevel + 1;
40 | }
41 | }
42 | }
43 | return undefined;
44 | }
45 |
46 | return countLevels(outerName, innerName, inner.body);
47 | }
48 |
49 | const churchTrue = parseTerm("λab.a");
50 | const churchFalse = parseTerm("λab.b");
51 |
52 | export function renderAsChurchBoolean(expression: Expr): Maybe {
53 | if (equal(expression, churchTrue)) {
54 | return true;
55 | }
56 | if (equal(expression, churchFalse)) {
57 | return false;
58 | }
59 | return undefined;
60 | }
61 |
--------------------------------------------------------------------------------
/src/lib/lambda/equality.ts:
--------------------------------------------------------------------------------
1 | import { cannonize } from "./cannonize";
2 | import { LambdaExpression as Expr } from "./types";
3 |
4 | function sameType(a: A, b: Expr): b is A {
5 | return a.type === b.type;
6 | }
7 |
8 | // Equality up to alpha conversion.
9 | function rEqual(a: Expr, b: Expr): boolean {
10 | switch (a.type) {
11 | // if it's free, we should hope they're the same.
12 | // if it's not free, we should hope that whatever renaming scheme already converted it
13 | case "variable":
14 | return sameType(a, b) && a.name === b.name;
15 | case "application":
16 | return (
17 | sameType(a, b) && rEqual(a.left, b.left) && rEqual(a.right, b.right)
18 | );
19 | case "function":
20 | return sameType(a, b) && rEqual(a.body, b.body);
21 | }
22 | }
23 |
24 | export function equal(a: Expr, b: Expr): boolean {
25 | const cA = cannonize(a);
26 | const cB = cannonize(b);
27 | return rEqual(cA, cB);
28 | }
29 |
--------------------------------------------------------------------------------
/src/lib/lambda/errors.ts:
--------------------------------------------------------------------------------
1 | export class LambdaSyntaxError extends Error {
2 | constructor(message: string) {
3 | super("Syntax Error: " + message);
4 | this.name = "LambdaSyntaxError";
5 | }
6 | }
7 |
8 | export class LambdaLexingError extends Error {
9 | constructor(message: string) {
10 | super("Lexing Error: " + message);
11 | this.name = "LambdaLexingError";
12 | }
13 | }
14 |
15 | export class LambdaExecutionTimeoutError extends Error {
16 | constructor(message: string) {
17 | super(message);
18 | this.name = "LambdaExecutionTimeoutError";
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/lib/lambda/index.ts:
--------------------------------------------------------------------------------
1 |
2 | // ---
3 | // lol how do i even do named exports
4 | import { parseTerm, parseExtendedSyntax } from './parser';
5 | import { renderExpression } from './renderer';
6 | import { renderAsChurchNumeral, renderAsChurchBoolean } from './churchPrimitives';
7 | import { getFreeVars, purgeAstCache } from './util';
8 | import { bReduce, eReduce, replace } from './operations';
9 | import { toNormalForm, leftmostOutermostRedex } from './normalize';
10 | import { tokenize } from './lexer';
11 | import { equal } from './equality';
12 | import { LambdaSyntaxError, LambdaLexingError, LambdaExecutionTimeoutError } from './errors';
13 |
14 | export {
15 | parseTerm,
16 | parseExtendedSyntax,
17 | renderExpression,
18 | renderAsChurchNumeral,
19 | renderAsChurchBoolean,
20 | getFreeVars,
21 | bReduce,
22 | eReduce,
23 | toNormalForm,
24 | leftmostOutermostRedex,
25 | tokenize,
26 | replace,
27 | equal,
28 | purgeAstCache,
29 | LambdaSyntaxError,
30 | LambdaLexingError,
31 | LambdaExecutionTimeoutError
32 | }
33 |
--------------------------------------------------------------------------------
/src/lib/lambda/lexer.ts:
--------------------------------------------------------------------------------
1 | import { LambdaToken } from './types';
2 | import { LambdaLexingError } from './errors';
3 |
4 | // idk i'm just guessing here. If you want something, just add it and i'll probs approve it
5 | const validSingleChars = /[a-zα-κμ-ω+\!\-\|\&]/;
6 | const validMultiChars = /[A-Z_]/;
7 |
8 | function tokenize(str: string) : LambdaToken[] {
9 | let tokenStream : LambdaToken[] = [];
10 | for(let pos = 0; pos < str.length; pos++){
11 | const nextChar = str.slice(pos, pos + 1);
12 | if (/\s/.test(nextChar)){ // skip whitespace.
13 | continue;
14 | } if (nextChar === 'λ') {
15 | tokenStream.push({
16 | type: 'lambda'
17 | });
18 | } else if(nextChar === '.') {
19 | tokenStream.push({
20 | type: 'dot',
21 | });
22 | } else if(nextChar === '(') {
23 | tokenStream.push({
24 | type: 'openParen',
25 | });
26 | } else if(nextChar === ')') {
27 | tokenStream.push({
28 | type: 'closeParen',
29 | });
30 | } else if(validSingleChars.test(nextChar)){
31 | // scan ahead to read the whole identifier
32 | let name = nextChar;
33 | while(/[₀-₉]/.test(str[pos + 1])){
34 | pos++;
35 | name += str[pos];
36 | }
37 | tokenStream.push({
38 | type: 'identifier',
39 | value: name,
40 | });
41 | } else if(validMultiChars.test(nextChar)) {
42 | let name = nextChar;
43 | while(validMultiChars.test(str[pos + 1])) {
44 | pos++;
45 | name += str[pos];
46 | }
47 | while(/[₀-₉]/.test(str[pos + 1])){
48 | pos++;
49 | name += str[pos];
50 | }
51 | tokenStream.push({
52 | type: 'identifier',
53 | value: name,
54 | });
55 | } else if(nextChar === ':'){
56 | pos++;
57 | if (str[pos] !== '=') {
58 | throw new LambdaLexingError('\'=\' expected after :');
59 | }
60 | tokenStream.push({
61 | type: 'assignment',
62 | });
63 | } else {
64 | // TODO: associate every token with a padding, so we can get better syntax errors in the parsing stage.
65 | const excerptPadding = 5;
66 | const lower = Math.max(pos - excerptPadding, 0);
67 | const upper = Math.min(pos + excerptPadding, str.length);
68 | const excerpt = str.slice(lower, upper);
69 | throw new LambdaLexingError(`Unexpected character at ${pos}: ${excerpt}`);
70 | }
71 | }
72 | return tokenStream;
73 | }
74 |
75 | export { tokenize };
76 |
--------------------------------------------------------------------------------
/src/lib/lambda/normalize.ts:
--------------------------------------------------------------------------------
1 | import { LambdaExpression as Expr, Maybe } from "./types";
2 | import { bReducable, bReduce } from "./operations";
3 | import { LambdaExecutionTimeoutError } from "./errors";
4 |
5 | function toNormalForm(expression: Expr, depthOverflow: number = 1000): Expr {
6 | let count = 0;
7 | let current: Maybe;
8 | let reduced: Maybe = expression;
9 | do {
10 | current = reduced;
11 | reduced = leftmostOutermostRedex(current);
12 | count++;
13 | if (count >= depthOverflow) {
14 | throw new LambdaExecutionTimeoutError(
15 | "Normal form execution exceeded. This expression may not have a normal form."
16 | );
17 | }
18 | } while (reduced !== undefined);
19 | return current;
20 | }
21 |
22 | function leftmostOutermostRedex(expression: Expr): Maybe {
23 | if (bReducable(expression)) {
24 | return bReduce(expression);
25 | }
26 | if (expression.type === "function") {
27 | const res = leftmostOutermostRedex(expression.body);
28 | if (res === undefined) {
29 | return undefined;
30 | } else {
31 | return {
32 | type: "function",
33 | argument: expression.argument,
34 | body: res,
35 | };
36 | }
37 | }
38 | if (expression.type === "variable") {
39 | return undefined;
40 | }
41 | if (expression.type === "application") {
42 | const leftReduced = leftmostOutermostRedex(expression.left);
43 | if (leftReduced !== undefined) {
44 | return {
45 | type: "application",
46 | left: leftReduced,
47 | right: expression.right,
48 | };
49 | }
50 | const rightReduced = leftmostOutermostRedex(expression.right);
51 | if (rightReduced !== undefined) {
52 | return {
53 | type: "application",
54 | left: expression.left,
55 | right: rightReduced,
56 | };
57 | }
58 | return undefined;
59 | }
60 | }
61 |
62 | export { toNormalForm, leftmostOutermostRedex };
63 |
--------------------------------------------------------------------------------
/src/lib/lambda/operations.ts:
--------------------------------------------------------------------------------
1 | import { getFreeVars, getAllArgumentNames } from "./util";
2 | import {
3 | LambdaExpression as Expr,
4 | Name,
5 | Maybe,
6 | FunctionExpression,
7 | ApplicationExpression,
8 | VariableExpression,
9 | } from "./types";
10 |
11 | // Expression -> bool
12 | function bReducable(
13 | exp: Expr
14 | ): exp is ApplicationExpression {
15 | return exp.type === "application" && exp.left.type === "function";
16 | }
17 |
18 | // We don't know whether we CAN beta reduce the term
19 | function bReduce(expression: Expr): Maybe {
20 | if (!bReducable(expression)) {
21 | return undefined;
22 | }
23 | return replace(
24 | expression.left.argument,
25 | expression.right,
26 | expression.left.body
27 | );
28 | }
29 |
30 | function eReducable(
31 | expression: Expr
32 | ): expression is FunctionExpression<
33 | ApplicationExpression
34 | > {
35 | if (
36 | expression.type !== "function" ||
37 | expression.body.type !== "application" ||
38 | expression.body.right.type !== "variable"
39 | ) {
40 | return false;
41 | }
42 | // --
43 | if (expression.body.right.name !== expression.argument) {
44 | return false;
45 | }
46 |
47 | const freeInF = getFreeVars(expression.body.left).map((token) => token.name);
48 | if (freeInF.includes(expression.argument)) {
49 | return false;
50 | }
51 | return true;
52 | }
53 |
54 | function eReduce(expression: Expr): Maybe {
55 | if (!eReducable(expression)) {
56 | return undefined;
57 | }
58 | return expression.body.left;
59 | }
60 |
61 | // Total garbage implementation
62 | const replacementMapping: { [key: string]: string } = {
63 | 0: "₀",
64 | 1: "₁",
65 | 2: "₂",
66 | 3: "₃",
67 | 4: "₄",
68 | 5: "₅",
69 | 6: "₆",
70 | 7: "₇",
71 | 8: "₈",
72 | 9: "₉",
73 | L: "λ",
74 | };
75 |
76 | const replaceAll = (str: string) =>
77 | str
78 | .split("")
79 | .map((letter) => replacementMapping[letter] || letter)
80 | .join("");
81 |
82 | function generateNewName(freeVars: string[]): string {
83 | let counter = 0;
84 | let nextName: string;
85 | do {
86 | counter++;
87 | nextName = replaceAll("ε" + counter);
88 | } while (freeVars.includes(nextName));
89 | return nextName;
90 | }
91 |
92 | // Replaces everything named name in expression with replacer
93 | // Follows the rules for capture-avoiding substitutions
94 | function replace(nameToReplace: Name, replacer: Expr, expression: Expr): Expr {
95 | switch (expression.type) {
96 | case "application":
97 | return {
98 | type: "application",
99 | left: replace(nameToReplace, replacer, expression.left),
100 | right: replace(nameToReplace, replacer, expression.right),
101 | };
102 | case "function":
103 | // shadowing
104 | if (nameToReplace === expression.argument) {
105 | return expression;
106 | }
107 |
108 | // capture avoidance
109 | const freeInReplacer = getFreeVars(replacer).map((node) => node.name);
110 | let alphaSafeExpression = expression;
111 | if (freeInReplacer.includes(expression.argument)) {
112 | // Then we pick a new name that
113 | // 1: isn't free in the replacer
114 | // 2: isn't free in the expression body
115 | // 3: isn't captured by an intermediate function in the expression body
116 | const freeInExpressionBody = getFreeVars(expression.body).map(
117 | (node) => node.name
118 | );
119 | const argNames = getAllArgumentNames(expression.body);
120 | let newName = generateNewName(
121 | freeInReplacer.concat(freeInExpressionBody, argNames)
122 | );
123 |
124 | // And make that the new function arg name
125 | alphaSafeExpression = {
126 | type: "function",
127 | argument: newName,
128 | body: replace(
129 | expression.argument,
130 | { type: "variable", name: newName },
131 | expression.body
132 | ),
133 | };
134 | }
135 | return {
136 | type: "function",
137 | argument: alphaSafeExpression.argument,
138 | body: replace(nameToReplace, replacer, alphaSafeExpression.body),
139 | };
140 | case "variable":
141 | return expression.name === nameToReplace ? replacer : expression;
142 | }
143 | }
144 |
145 | export { bReducable, bReduce, eReducable, eReduce, replace };
146 |
--------------------------------------------------------------------------------
/src/lib/lambda/parser.ts:
--------------------------------------------------------------------------------
1 | import {
2 | LambdaExpression as Expr,
3 | LambdaStatement as Statement,
4 | LambdaToken as Token,
5 | ValuedToken,
6 | } from "./types";
7 | import { tokenize } from "./lexer";
8 | import { LambdaSyntaxError } from "./errors";
9 |
10 | // this one'll be a better entry point
11 | export function parseStatement(tokenStream: Token[]): Statement {
12 | // could handle errors better-- this one just will say unexpected token
13 | // when it reaches a nonstandard assignment token.
14 | if (tokenStream.length >= 2) {
15 | const first = tokenStream[0]; //to satisfy the typechecker
16 | if (first.type === "identifier" && tokenStream[1].type === "assignment") {
17 | let lhs = first.value;
18 | let rhs = parseExpression(tokenStream.splice(2));
19 | return { type: "assignment", lhs, rhs };
20 | }
21 | }
22 | return parseExpression(tokenStream);
23 | }
24 |
25 | export function parseExpression(tokenStream: Token[]): Expr {
26 | if (tokenStream.length === 0) {
27 | throw new LambdaSyntaxError("Empty Expression");
28 | }
29 | let [expression, rest] = popExpression(tokenStream);
30 | let applications = [expression];
31 | while (rest.length !== 0) {
32 | [expression, rest] = popExpression(rest);
33 | applications.push(expression);
34 | }
35 | // For left-associativity.
36 | return applications.reduce((prev, cur) => ({
37 | type: "application",
38 | left: prev,
39 | right: cur,
40 | }));
41 | // And reduce to produce the application
42 | }
43 |
44 | function popExpression(tokenStream: Token[]): [Expr, Token[]] {
45 | // 3 cases. 1:
46 | const nextToken = tokenStream[0];
47 | switch (nextToken.type) {
48 | case "identifier":
49 | return [
50 | { type: "variable", name: nextToken.value },
51 | tokenStream.slice(1),
52 | ];
53 | case "lambda":
54 | // scan forward to find the dot, add in arguments
55 | if (tokenStream.length < 2) {
56 | throw new LambdaSyntaxError("Unexpected end of lambda");
57 | }
58 | let dotPosition = 1;
59 | while (tokenStream[dotPosition].type !== "dot") {
60 | if (tokenStream[dotPosition].type !== "identifier") {
61 | throw new LambdaSyntaxError("Non-identifier in argument stream");
62 | }
63 | dotPosition++;
64 | if (dotPosition >= tokenStream.length) {
65 | throw new LambdaSyntaxError("Unexpected end of lambda");
66 | }
67 | }
68 |
69 | const args = tokenStream.slice(1, dotPosition) as ValuedToken[];
70 | if (args.length === 0) {
71 | throw new LambdaSyntaxError("Bad number of arguments");
72 | }
73 | const childExp = parseExpression(tokenStream.slice(dotPosition + 1));
74 | const exp: Expr = args.reduceRight(
75 | (acc, cur) => ({
76 | type: "function",
77 | argument: cur.value,
78 | body: acc,
79 | }),
80 | childExp
81 | );
82 | return [
83 | exp,
84 | [], //because it will always end the whole expression
85 | ];
86 | case "openParen":
87 | let depth = 0;
88 | let splitPoint = -1;
89 | for (let i = 0; i < tokenStream.length; i++) {
90 | const cur = tokenStream[i];
91 | if (cur.type === "openParen") {
92 | depth++;
93 | }
94 | if (cur.type === "closeParen") {
95 | depth--;
96 | }
97 | if (depth === 0) {
98 | splitPoint = i + 1;
99 | break;
100 | }
101 | }
102 | if (splitPoint < 0) {
103 | throw new LambdaSyntaxError("Unmatched Paren");
104 | }
105 | return [
106 | parseExpression(tokenStream.slice(1, splitPoint - 1)),
107 | tokenStream.slice(splitPoint),
108 | ];
109 | default:
110 | throw new LambdaSyntaxError("Unexpected Token");
111 | }
112 | }
113 |
114 | // We should rename these to be better.
115 | export function parseTerm(str: string) {
116 | return parseExpression(tokenize(str));
117 | }
118 |
119 | // This isn't understood by most helper functions, as it's an extension of the lambda calculus.
120 | // TODO: make this more well supported.
121 | export function parseExtendedSyntax(str: string) {
122 | return parseStatement(tokenize(str));
123 | }
124 |
--------------------------------------------------------------------------------
/src/lib/lambda/renderer.ts:
--------------------------------------------------------------------------------
1 | import { LambdaExpression as Expr } from "./types";
2 |
3 | function renderExpression(expression: Expr): string {
4 | switch (expression.type) {
5 | case "application":
6 | let leftSide: string;
7 | if (expression.left.type !== "function") {
8 | leftSide = renderExpression(expression.left);
9 | } else {
10 | leftSide = `(${renderExpression(expression.left)})`;
11 | }
12 | // I have no idea whether the rendering of the right side is valid.
13 | let rightSide: string;
14 | if (expression.right.type === "variable") {
15 | rightSide = renderExpression(expression.right);
16 | } else {
17 | rightSide = `(${renderExpression(expression.right)})`;
18 | }
19 | return `${leftSide}${rightSide}`;
20 | case "function":
21 | return `λ${expression.argument}.${renderExpression(expression.body)}`;
22 | case "variable":
23 | return expression.name;
24 | }
25 | }
26 |
27 | export { renderExpression };
28 |
--------------------------------------------------------------------------------
/src/lib/lambda/types.ts:
--------------------------------------------------------------------------------
1 | export interface SimpleToken {
2 | type: "lambda" | "dot" | "openParen" | "closeParen" | "assignment";
3 | }
4 |
5 | export interface ValuedToken {
6 | type: "identifier";
7 | value: string;
8 | }
9 |
10 | export type LambdaToken = SimpleToken | ValuedToken;
11 |
12 | export type Name = string;
13 |
14 | /* Lexer types */
15 |
16 | /* AST types */
17 | export interface FunctionExpression {
18 | type: "function";
19 | argument: Name;
20 | body: T;
21 | }
22 |
23 | export interface VariableExpression {
24 | type: "variable";
25 | name: Name;
26 | }
27 |
28 | export interface ApplicationExpression<
29 | L = LambdaExpression,
30 | R = LambdaExpression
31 | > {
32 | type: "application";
33 | left: L;
34 | right: R;
35 | }
36 |
37 | export interface AssignmentExpression {
38 | type: "assignment";
39 | lhs: Name;
40 | rhs: T;
41 | }
42 |
43 | interface CacheEntry {
44 | computedWith: LambdaExpression;
45 | value: T;
46 | }
47 |
48 | type Cacheable = {
49 | __cache__?: { [key: string]: CacheEntry };
50 | };
51 |
52 | export type LambdaExpression = (
53 | | FunctionExpression
54 | | VariableExpression
55 | | ApplicationExpression
56 | ) &
57 | Cacheable;
58 | export type LambdaStatement = AssignmentExpression | LambdaExpression;
59 |
60 | // util type:
61 | export type Maybe = T | undefined;
62 |
--------------------------------------------------------------------------------
/src/lib/lambda/util.ts:
--------------------------------------------------------------------------------
1 | import { uniqBy, map } from "ramda";
2 | import { LambdaExpression as Expr, VariableExpression } from "./types";
3 |
4 | function cacheOnAst(fn: (input: Expr) => T) {
5 | const cacheSymbol = `${fn.name}_${Math.random().toString().slice(2)}`;
6 | return (ast: Expr) => {
7 | if (!ast.__cache__) {
8 | ast.__cache__ = {};
9 | }
10 | if (
11 | ast.__cache__[cacheSymbol] &&
12 | ast.__cache__[cacheSymbol].computedWith === ast
13 | ) {
14 | return ast.__cache__[cacheSymbol].value as T;
15 | } else {
16 | const result = fn(ast);
17 | ast.__cache__[cacheSymbol] = {
18 | // if the property accidentally gets included on the wrong node (like
19 | // via the splat operator), this invalidates it.
20 | computedWith: ast,
21 | value: result,
22 | };
23 | return result;
24 | }
25 | };
26 | }
27 |
28 | // returns a new AST without the caches
29 | function purgeAstCache(ast: Expr): Expr {
30 | let newAst: Expr;
31 |
32 | switch (ast.type) {
33 | case "variable":
34 | newAst = ast;
35 | break;
36 | case "function":
37 | newAst = {
38 | ...ast,
39 | body: purgeAstCache(ast.body),
40 | };
41 | break;
42 | case "application":
43 | newAst = {
44 | ...ast,
45 | left: purgeAstCache(ast.left),
46 | right: purgeAstCache(ast.right),
47 | };
48 | break;
49 | }
50 |
51 | delete newAst.__cache__;
52 | return newAst;
53 | }
54 |
55 | // TODO: Should for consistensy change to [name]
56 | const getFreeVars = cacheOnAst(function getFreeVarsUnmemoized(
57 | expression: Expr
58 | ): VariableExpression[] {
59 | switch (expression.type) {
60 | case "variable":
61 | return [expression];
62 | case "function":
63 | return getFreeVars(expression.body).filter(
64 | (token) => token.name !== expression.argument
65 | );
66 | case "application":
67 | const leftFree = getFreeVars(expression.left);
68 | const rightFree = getFreeVars(expression.right);
69 | return uniqBy(
70 | (term: VariableExpression) => term.name,
71 | leftFree.concat(rightFree)
72 | );
73 | }
74 | });
75 |
76 | const getAllArgumentNames = cacheOnAst(function getAllArgumentNamesUnmemoized(
77 | expression: Expr
78 | ): string[] {
79 | switch (expression.type) {
80 | case "variable":
81 | return [];
82 | case "function":
83 | return [...getAllArgumentNames(expression.body), expression.argument];
84 | case "application":
85 | const leftArgs = getAllArgumentNames(expression.left);
86 | const rightArgs = getAllArgumentNames(expression.right);
87 | return [...leftArgs, ...rightArgs];
88 | }
89 | });
90 |
91 | export { getFreeVars, getAllArgumentNames, cacheOnAst, purgeAstCache };
92 |
--------------------------------------------------------------------------------
/src/util/generateGoldens.js:
--------------------------------------------------------------------------------
1 | import { parseTerm, toNormalForm, purgeAstCache } from "../lib/lambda/index.ts";
2 |
3 | export default function generateGoldens(text) {
4 | return {
5 | text,
6 | normalForm: purgeAstCache(toNormalForm(parseTerm(text), 1000)),
7 | };
8 | }
9 |
--------------------------------------------------------------------------------
/src/util/persist.js:
--------------------------------------------------------------------------------
1 | // Persistent hack, certainly not excellent code, for singletons only
2 | // plz don't think i like this.
3 |
4 | // Put a persist(localKey, readFn, writeFn) wherever you want to persist across sessions
5 | export default function persist(localKey, readFn, writeFn){
6 | const nopersist = (window.location.search === '?nopersist');
7 | if (!nopersist){
8 | const prevState = JSON.parse(localStorage.getItem(localKey));
9 | writeFn(prevState);
10 | window.addEventListener('beforeunload', () => {
11 | localStorage.setItem(localKey, JSON.stringify(readFn()));
12 | });
13 | } else {
14 | localStorage.removeItem(localKey);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "strict": true
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/webpack.config.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | context: __dirname + "/src",
3 | entry: ["@babel/polyfill", "./index"],
4 | output: {
5 | path: __dirname + "/public/build",
6 | filename: "bundle.js",
7 | publicPath: "/build",
8 | },
9 | module: {
10 | rules: [
11 | {
12 | test: /\.jsx?$/,
13 | loader: "babel-loader",
14 | exclude: /node_modules/,
15 | },
16 | {
17 | test: /\.tsx?$/,
18 | loader: "ts-loader",
19 | exclude: /node_modules/,
20 | },
21 | ],
22 | },
23 | resolve: {
24 | extensions: [".js", ".jsx", ".ts", ".tsx"],
25 | extensionAlias: {
26 | ".js": [".ts", ".js"],
27 | ".mjs": [".mts", ".mjs"],
28 | },
29 | },
30 | devtool: "source-map",
31 | };
32 |
--------------------------------------------------------------------------------