A working version of Scheme in 20 Minutes.

This commit is contained in:
Elf M. Sternberg 2015-03-15 20:11:14 -07:00
commit 5a3e5272a2
12 changed files with 246 additions and 0 deletions

7
.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
*#
.#*
*~
*.orig
npm-debug.log
node_modules/*
tmp/

7
Makefile Normal file
View File

@ -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

56
README.md Normal file
View File

@ -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.

5
bin/lisp Normal file
View File

@ -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)

14
lib/eval.coffee Normal file
View File

@ -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

21
lib/fn.coffee Normal file
View File

@ -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

12
lib/lisp.coffee Normal file
View File

@ -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())

34
lib/lisp_parser.peg Normal file
View File

@ -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]+

8
lib/parser.coffee Normal file
View File

@ -0,0 +1,8 @@
lispeval = require './eval'
lisp = (ast, scope) ->
ast.map((e) -> lispeval(e, scope)).pop()
module.exports = lisp

50
lib/scope.coffee Normal file
View File

@ -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

25
package.json Normal file
View File

@ -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"
}

7
test.scm Normal file
View File

@ -0,0 +1,7 @@
(define fact
(lambda (x)
(if (== x 1) 1
(* x (fact (- x 1))))))
(fact 6)