A working version of Scheme in 20 Minutes.
This commit is contained in:
commit
5a3e5272a2
|
@ -0,0 +1,7 @@
|
|||
*#
|
||||
.#*
|
||||
*~
|
||||
*.orig
|
||||
npm-debug.log
|
||||
node_modules/*
|
||||
tmp/
|
|
@ -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
|
||||
|
||||
|
|
@ -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.
|
||||
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
|
@ -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())
|
||||
|
|
@ -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]+
|
|
@ -0,0 +1,8 @@
|
|||
lispeval = require './eval'
|
||||
|
||||
lisp = (ast, scope) ->
|
||||
ast.map((e) -> lispeval(e, scope)).pop()
|
||||
|
||||
module.exports = lisp
|
||||
|
||||
|
|
@ -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
|
|
@ -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"
|
||||
}
|
Loading…
Reference in New Issue