diff options
-rw-r--r-- | html/playground/scheme.html | 206 | ||||
-rw-r--r-- | html/schemer/index.html | 84 |
2 files changed, 265 insertions, 25 deletions
diff --git a/html/playground/scheme.html b/html/playground/scheme.html index fc2d5fa..dc6fb55 100644 --- a/html/playground/scheme.html +++ b/html/playground/scheme.html @@ -83,10 +83,7 @@ </head> <body> - <div class="playground" id="playground"> - <div class="seesaw" id="seesaw"></div> - <div class="slide" id="slide"></div> - </div> + <div class="playground" id="playground"><div style="display: flex; flex-direction: column; width: 100%;"><textarea placeholder="Scheme here..." style="width: 100%; height: 100px; margin-bottom: 10px; font-family: monospace;"></textarea><button>Evaluate</button><pre style="width: 100%; height: 200px; overflow-y: auto; background-color: rgb(240, 240, 240); padding: 10px; font-family: monospace;"></pre></div></div> <div class="button-container"> <button onclick="clearEverything()">Clear</button> @@ -184,14 +181,42 @@ const globalEnv = { '+': (...args) => args.reduce((acc, curr) => acc + curr), '-': (...args) => args.reduce((acc, curr) => acc - curr), '*': (...args) => args.reduce((acc, curr) => acc * curr), - '/': (a, b) => a / b, // Only two arguments for division...right? + '/': (a, b) => a / b, // Only two arguments for division 'eq?': (...args) => args.every((val, i, arr) => val === arr[0]), - 'car': (list) => list[0], - 'cdr': (list) => list.slice(1), - 'cons': (a, b) => [a].concat(b), - 'null?': (list) => list.length === 0, + 'car': (list) => { + if (list.type !== 'List' || list.value.length === 0) { + throw new Error('car expects a non-empty list'); + } + return list.value[0]; + }, + 'cdr': (list) => { + if (list.type !== 'List' || list.value.length === 0) { + throw new Error('cdr expects a non-empty list'); + } + return { type: 'List', value: list.value.slice(1) }; + }, + 'cons': (a, b) => { + if (b.type !== 'List') { + throw new Error('cons expects second argument to be a list'); + } + return { type: 'List', value: [a].concat(b.value) }; + }, + 'null?': (list) => list.type === 'List' && list.value.length === 0, + 'zero?': (n) => n === 0, + 'atom?': (x) => typeof x !== 'object' || x === null, + 'number?': (x) => typeof x === 'number', + 'add1': (n) => n + 1, + 'sub1': (n) => n - 1, + 'quote': (x) => x, // Simply return the quoted expression + 'and': (...args) => args.every(Boolean), + 'or': (...args) => args.some(Boolean), + 'true': true, + 'false': false }; + + + function evaluate(node, env = globalEnv) { if (node.type === 'NumberLiteral') { return node.value; @@ -242,12 +267,41 @@ function evaluate(node, env = globalEnv) { const condition = evaluate(test, env); return condition ? evaluate(consequent, env) : evaluate(alternate, env); } + + // Special case for quote + if (operator === 'quote') { + return rest[0]; // Return the quoted expression without evaluating it + } + + // Special case for cond + if (operator === 'cond') { + for (let clause of rest) { + const [test, expr] = clause.value; + if (evaluate(test, env)) { + return evaluate(expr, env); + } + } + return null; // No matching condition + } + + // Special case for letrec (recursive let) + if (operator === 'letrec') { + const [bindings, body] = rest; + const letEnv = { ...env }; + + // Loop through bindings and evaluate each + bindings.value.forEach(binding => { + const [name, expr] = binding.value; + letEnv[name.value] = evaluate(expr, letEnv); + }); + + return evaluate(body, letEnv); + } } // Evaluate the first element const func = evaluate(first, env); - // Check that `func` is a function. If it isn't, don't try to apply it if (typeof func !== 'function') { throw new Error(`Expected a function but got: ${func}`); } @@ -259,19 +313,19 @@ function evaluate(node, env = globalEnv) { throw new Error(`Unexpected node type: ${node.type}`); } + + function evalScheme(input) { const tokens = tokenizeScheme(input); const ast = parseScheme(tokens); return evaluate(ast); } -/* -console.log(evalScheme("(define add1 (lambda (x) (+ x 1)))")); -console.log(evalScheme("(add1 10)")); -console.log(evalScheme("(eq? 1 1)")); -console.log(evalScheme("(eq? 1 2)")); -console.log(evalScheme("(+ 1 2 3 6)")); -*/ + + + + + function mountRepl(playground) { // Create a REPL container @@ -328,8 +382,122 @@ function mountRepl(playground) { } -mount(mountRepl);</textarea> - <div id="console"></div> +mount(mountRepl); + + + +// Testing 'define' and arithmetic operations +console.log(evalScheme("(define x 10)")); // Should define 'x' as 10 +console.log(evalScheme("(+ x 5)")); // Should return 15 +console.log(evalScheme("(- x 3)")); // Should return 7 +console.log(evalScheme("(* x 2)")); // Should return 20 +console.log(evalScheme("(/ x 2)")); // Should return 5 + +// Testing 'eq?' for equality +console.log(evalScheme("(eq? 1 1)")); // Should return true +console.log(evalScheme("(eq? 1 2)")); // Should return false + +// Testing 'car', 'cdr', 'cons', 'null?' +console.log(evalScheme("(car (quote (1 2 3)))")); // Should return 1 +console.log(evalScheme("(cdr (quote (1 2 3)))")); // Should return (2 3) +console.log(evalScheme("(cons 1 (quote (2 3)))")); // Should return (1 2 3) +console.log(evalScheme("(null? (quote ()))")); // Should return true +console.log(evalScheme("(null? (quote (1 2 3)))")); // Should return false + +// Testing 'zero?' for checking zero +console.log(evalScheme("(zero? 0)")); // Should return true +console.log(evalScheme("(zero? 1)")); // Should return false + +// Testing 'atom?' for checking if an element is an atom +console.log(evalScheme("(atom? 1)")); // Should return true +console.log(evalScheme("(atom? (quote (1 2 3)))")); // Should return false + +// Testing 'number?' for checking if an element is a number +console.log(evalScheme("(number? 42)")); // Should return true +console.log(evalScheme("(number? (quote hello))")); // Should return false + +// Testing 'add1' and 'sub1' +console.log(evalScheme("(add1 5)")); // Should return 6 +console.log(evalScheme("(sub1 5)")); // Should return 4 + +// Testing 'quote' for returning unevaluated expressions +console.log(evalScheme("(quote (1 2 3))")); // Should return (1 2 3) + +// Testing 'and' and 'or' +console.log(evalScheme("(and true true)")); // Should return true +console.log(evalScheme("(and true false)")); // Should return false +console.log(evalScheme("(or false false)")); // Should return false +console.log(evalScheme("(or true false)")); // Should return true + +// Testing 'lambda' with a function definition +console.log(evalScheme("(define add1 (lambda (x) (+ x 1)))")); // Should define add1 function +console.log(evalScheme("(add1 10)")); // Should return 11 + +// Testing 'if' for conditional expressions +console.log(evalScheme("(if (eq? 1 1) (quote yes) (quote no))")); // Should return 'yes +console.log(evalScheme("(if (eq? 1 2) (quote yes) (quote no))")); // Should return 'no + +// Testing 'cond' for multiple conditions +console.log(evalScheme("(cond ((eq? 1 2) (quote no)) ((eq? 2 2) (quote yes)))")); // Should return 'yes +console.log(evalScheme("(cond ((eq? 1 2) (quote no)) ((eq? 3 2) (quote nope)))")); // Should return null (no matching condition) + +// Testing 'letrec' for recursive bindings +console.log(evalScheme("(letrec ((factorial (lambda (n) (if (zero? n) 1 (* n (factorial (sub1 n))))))) (factorial 5))")); +// Should return 120 (5!)</textarea> + <div id="console"><div></div><div>15</div><div>7</div><div>20</div><div>5</div><div>true</div><div>false</div><div>{ + "type": "NumberLiteral", + "value": 1 +}</div><div>{ + "type": "List", + "value": [ + { + "type": "NumberLiteral", + "value": 2 + }, + { + "type": "NumberLiteral", + "value": 3 + } + ] +}</div><div>{ + "type": "List", + "value": [ + 1, + { + "type": "NumberLiteral", + "value": 2 + }, + { + "type": "NumberLiteral", + "value": 3 + } + ] +}</div><div>true</div><div>false</div><div>true</div><div>false</div><div>true</div><div>false</div><div>true</div><div>false</div><div>6</div><div>4</div><div>{ + "type": "List", + "value": [ + { + "type": "NumberLiteral", + "value": 1 + }, + { + "type": "NumberLiteral", + "value": 2 + }, + { + "type": "NumberLiteral", + "value": 3 + } + ] +}</div><div>true</div><div>false</div><div>false</div><div>true</div><div></div><div>11</div><div>{ + "type": "Symbol", + "value": "yes" +}</div><div>{ + "type": "Symbol", + "value": "no" +}</div><div>{ + "type": "Symbol", + "value": "yes" +}</div><div>null</div><div>120</div></div> <script> function evaluateCode() { diff --git a/html/schemer/index.html b/html/schemer/index.html index ebadf83..2220e57 100644 --- a/html/schemer/index.html +++ b/html/schemer/index.html @@ -58,7 +58,6 @@ </div> <script> - // Scheme interpreter (unchanged) function tokenizeScheme(input) { const tokens = []; let current = 0; @@ -66,6 +65,7 @@ const isWhitespace = (char) => /\s/.test(char); const isDigit = (char) => /[0-9]/.test(char); const isParen = (char) => char === '(' || char === ')'; + // Symbols can include letters, numbers, and some punctuation like - _ ! ? const isSymbolChar = (char) => /[a-zA-Z0-9\+\-\*\/\=\?\!\_]/.test(char); while (current < input.length) { @@ -92,6 +92,7 @@ continue; } + // Handle symbols, including letters, numbers, punctuation if (isSymbolChar(char)) { let symbol = ''; while (isSymbolChar(char)) { @@ -108,6 +109,7 @@ return tokens; } + function parseScheme(tokens) { let current = 0; @@ -146,14 +148,42 @@ '+': (...args) => args.reduce((acc, curr) => acc + curr), '-': (...args) => args.reduce((acc, curr) => acc - curr), '*': (...args) => args.reduce((acc, curr) => acc * curr), - '/': (a, b) => a / b, + '/': (a, b) => a / b, // Only two arguments for division 'eq?': (...args) => args.every((val, i, arr) => val === arr[0]), - 'car': (list) => list[0], - 'cdr': (list) => list.slice(1), - 'cons': (a, b) => [a].concat(b), - 'null?': (list) => list.length === 0, + 'car': (list) => { + if (list.type !== 'List' || list.value.length === 0) { + throw new Error('car expects a non-empty list'); + } + return list.value[0]; + }, + 'cdr': (list) => { + if (list.type !== 'List' || list.value.length === 0) { + throw new Error('cdr expects a non-empty list'); + } + return { type: 'List', value: list.value.slice(1) }; + }, + 'cons': (a, b) => { + if (b.type !== 'List') { + throw new Error('cons expects second argument to be a list'); + } + return { type: 'List', value: [a].concat(b.value) }; + }, + 'null?': (list) => list.type === 'List' && list.value.length === 0, + 'zero?': (n) => n === 0, + 'atom?': (x) => typeof x !== 'object' || x === null, + 'number?': (x) => typeof x === 'number', + 'add1': (n) => n + 1, + 'sub1': (n) => n - 1, + 'quote': (x) => x, // Simply return the quoted expression + 'and': (...args) => args.every(Boolean), + 'or': (...args) => args.some(Boolean), + 'true': true, + 'false': false }; + + + function evaluate(node, env = globalEnv) { if (node.type === 'NumberLiteral') { return node.value; @@ -169,34 +199,74 @@ if (node.type === 'List') { const [first, ...rest] = node.value; + // Is the first element a symbol, like an operator or function name? if (first.type === 'Symbol') { const operator = first.value; + // Special case for define if (operator === 'define') { const [symbol, expr] = rest; env[symbol.value] = evaluate(expr, env); return; } + // Special case for lambda if (operator === 'lambda') { const [params, body] = rest; + // Create a closure to return return function (...args) { const lambdaEnv = { ...env }; + + // Bind each argument to the corresponding parameter... params.value.forEach((param, i) => { lambdaEnv[param.value] = args[i]; }); + + // ...and then evaluate the body with the environment return evaluate(body, lambdaEnv); }; } + // Special case for if if (operator === 'if') { const [test, consequent, alternate] = rest; const condition = evaluate(test, env); return condition ? evaluate(consequent, env) : evaluate(alternate, env); } + + // Special case for quote + if (operator === 'quote') { + return rest[0]; // Return the quoted expression without evaluating it + } + + // Special case for cond + if (operator === 'cond') { + for (let clause of rest) { + const [test, expr] = clause.value; + if (evaluate(test, env)) { + return evaluate(expr, env); + } + } + return null; // No matching condition + } + + // Special case for letrec (recursive let) + if (operator === 'letrec') { + const [bindings, body] = rest; + const letEnv = { ...env }; + + // Loop through bindings and evaluate each + bindings.value.forEach(binding => { + const [name, expr] = binding.value; + letEnv[name.value] = evaluate(expr, letEnv); + }); + + return evaluate(body, letEnv); + } } + // Evaluate the first element const func = evaluate(first, env); if (typeof func !== 'function') { @@ -210,6 +280,8 @@ throw new Error(`Unexpected node type: ${node.type}`); } + + function evalScheme(input) { const tokens = tokenizeScheme(input); const ast = parseScheme(tokens); |