commit 5a3e5272a2afc8daea9e9791e4acf3f505d92585 Author: Elf M. Sternberg Date: Sun Mar 15 20:11:14 2015 -0700 A working version of Scheme in 20 Minutes. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e7ee1d2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +*# +.#* +*~ +*.orig +npm-debug.log +node_modules/* +tmp/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..7b69ed2 --- /dev/null +++ b/Makefile @@ -0,0 +1,7 @@ +lib/lisp_parser.js: lib/lisp_parser.peg + node_modules/.bin/pegjs $< $@ + +test: lib/lisp_parser.js + node_modules/.bin/coffee bin/lisp test.scm + + diff --git a/README.md b/README.md new file mode 100644 index 0000000..767d69d --- /dev/null +++ b/README.md @@ -0,0 +1,56 @@ +# James Coglan's "A Simple Scheme In 20 Minutes" + +## Description + +A version of James Coglan's "A Language In 20 Minutes" (see: +https://www.youtube.com/watch?v=CqhL-BDT8lg for the whole 40 minute +presentation). It took me about three hours to make this work. This +version is tighter, using Coffeescript and PegJS instead of Javascript +and whatever parser/lexer he described. + +## Purpose + +When I went to university, my degree was deliberately steered away from +the esoterica of compilers and interpreters. My degree and financial +backing was predicated on my getting "practical" programming skills, so +I took classes in COBOL, Fortran, ADA, SQL, Accounting, and similar +subjects intended to make me a "financial products" programmer. + +The Internet came along and gave me a much more interesting career, but +now it's time to rectify the shortcoming of my education and study +programming languages themselves. This is a "My First Programming +Lanugage." + +## Usage + +It only has three special forms: 'define', 'lambda', and 'if'. It +understands addition, subtraction, multiplication, division, and +equality for the purposes of the 'if' special form, although it's +clearly treating arithmetic as 'special forms' for the purpose of doing +the math. It's good enough to handle lexical scoping and recursion, and +it handles basic integer arithmetic. There is a bug is the lexer such +that symbols that start with a numeral won't be read right, but I'm too +lazy to fix it. + +It has no macros. Sorry about that. + +## Requirements + +Coffeescript, pegjs. + +## LICENSE AND COPYRIGHT NOTICE: NO WARRANTY GRANTED OR IMPLIED + +Copyright (c) 2015 Elf M. Sternberg + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + diff --git a/bin/lisp b/bin/lisp new file mode 100644 index 0000000..1ac1e94 --- /dev/null +++ b/bin/lisp @@ -0,0 +1,5 @@ +lisp = require '../lib/lisp' +fs = require 'fs' +{inspect} = require 'util' + +console.log inspect((lisp.run process.argv[2]), true, null, false) diff --git a/lib/eval.coffee b/lib/eval.coffee new file mode 100644 index 0000000..6988489 --- /dev/null +++ b/lib/eval.coffee @@ -0,0 +1,14 @@ +lispeval = (element, scope) -> + switch element.type + when 'boolean' then element.value == '#t' + when 'number' then parseInt(element.value, 10) + when 'symbol' + scope.lookup(element.value) + when 'list' + proc = lispeval(element.value[0], scope) + args = element.value.slice(1) + proc.apply(args, scope) + + else throw new Error ("Unrecognized type in parse") + +module.exports = lispeval diff --git a/lib/fn.coffee b/lib/fn.coffee new file mode 100644 index 0000000..a6ff487 --- /dev/null +++ b/lib/fn.coffee @@ -0,0 +1,21 @@ +lispeval = require './eval' + +class Proc + constructor: (@scope, @params, @body) -> + apply: (cells, scope) -> + args = (cells.map (i) -> lispeval(i, scope)) + if @body instanceof Function + @body.apply(this, args) + else + inner = @scope.fork() + @params.forEach((name, i) -> inner.set(name, args[i])) + @body.map((e) -> lispeval(e, inner)).pop() + +class Syntax extends Proc + apply: (cells, scope) -> + return @body(cells, scope) + + +module.exports = + Proc: Proc + Syntax: Syntax diff --git a/lib/lisp.coffee b/lib/lisp.coffee new file mode 100644 index 0000000..69e84c3 --- /dev/null +++ b/lib/lisp.coffee @@ -0,0 +1,12 @@ +fs = require 'fs' +{parse} = require './lisp_parser' +lisp = require './parser' +Scope = require './scope' +{inspect} = require 'util' + +module.exports = + run: (pathname) -> + text = fs.readFileSync(pathname, 'utf8') + ast = parse(text) + return lisp(ast, new Scope.Toplevel()) + diff --git a/lib/lisp_parser.peg b/lib/lisp_parser.peg new file mode 100644 index 0000000..5893efc --- /dev/null +++ b/lib/lisp_parser.peg @@ -0,0 +1,34 @@ +lisp + = cell* + +cell + = _* datum:datum _* + { return datum } + +datum + = list / boolean / number / symbol + +list + = "(" items:cell* ")" + { return { type: "list", value: items } } + +boolean + = b:("#t" / "#f") + { return { type: 'boolean', value: b } } + +delim + = paren / _ + +number + = b:( [0-9]+ ) + { return { type: 'number', value: b.join("") } } + +symbol + = b:(!delim c:. { return c })+ + { return { type: 'symbol', value: b.join("") } } + +paren + = "(" / ")" + +_ + = w:[ \t\n\r]+ diff --git a/lib/parser.coffee b/lib/parser.coffee new file mode 100644 index 0000000..ed527b3 --- /dev/null +++ b/lib/parser.coffee @@ -0,0 +1,8 @@ +lispeval = require './eval' + +lisp = (ast, scope) -> + ast.map((e) -> lispeval(e, scope)).pop() + +module.exports = lisp + + diff --git a/lib/scope.coffee b/lib/scope.coffee new file mode 100644 index 0000000..29cbb90 --- /dev/null +++ b/lib/scope.coffee @@ -0,0 +1,50 @@ +parser = require './parser' +lispeval = require './eval' + +{Proc, Syntax} = require './fn' + +class Scope + constructor: (@parent) -> + @_symbols = {} + + lookup: (name) -> + if @_symbols[name]? + return @_symbols[name] + + if @parent + return @parent.lookup(name) + + throw new Error "Unknown variable '#{name}'" + + define: (name, body) -> + @set name, (new Proc(this, [], body)) + + syntax: (name, body) -> + @set name, (new Syntax(this, [], body)) + + fork: -> new Scope(@) + + set: (name, value) -> + @_symbols[name] = value + +class Toplevel extends Scope + constructor: (@parent = null) -> + super + @define '+', (a, b) -> a + b + @define '-', (a, b) -> a - b + @define '*', (a, b) -> a * b + @define '==', (a, b) -> a == b + + @syntax 'define', (list, scope) -> + scope.set(list[0].value, lispeval(list[1], scope)) + + @syntax 'lambda', (list, scope) -> + params = list[0].value.map (n) -> return n.value + new Proc(scope, params, list.slice(1)) + + @syntax 'if', (list, scope) -> + lispeval(list[if lispeval(list[0], scope) then 1 else 2], scope) + +Scope.Toplevel = Toplevel + +module.exports = Scope diff --git a/package.json b/package.json new file mode 100644 index 0000000..9249a86 --- /dev/null +++ b/package.json @@ -0,0 +1,25 @@ +{ + "name": "LangIn20", + "version": "0.0.2", + "description": "A Coffeescript & PegJS rendition of James Coglan's 'Javascript in 20 Minutes'", + "main": "bin/lisp", + "dependencies": { + "coffee-script": "^1.9.1" + }, + "devDependencies": { + "pegjs": "^0.8.0" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "scheme", + "practice", + "interpreter", + "javascript", + "coffeescript", + "pegjs" + ], + "author": "Elf M. Sternberg", + "license": "ISC" +} diff --git a/test.scm b/test.scm new file mode 100644 index 0000000..43bac99 --- /dev/null +++ b/test.scm @@ -0,0 +1,7 @@ +(define fact + (lambda (x) + (if (== x 1) 1 + (* x (fact (- x 1)))))) + +(fact 6) +