├── README.markdown ├── core.jr ├── fib.jr ├── jasper.js └── repl.js /README.markdown: -------------------------------------------------------------------------------- 1 | Lispy Javascript 2 | 3 | For a demo, see [the Wiki](http://github.com/defunkt/jasper/wikis). 4 | 5 | Chris Wanstrath // chris@ozmm.org -------------------------------------------------------------------------------- /core.jr: -------------------------------------------------------------------------------- 1 | "Comments are just strings (for now)" 2 | (defmacro defun (name args &rest body) 3 | (cons '= (cons name (list (append (list 'lambda args) body))))) 4 | 5 | "Lispy aliases." 6 | (= setq =) 7 | (setq eq? ==) 8 | 9 | "t and f and nil" 10 | (= t (js "true")) 11 | (= f (js "false")) 12 | (= nil (js "null")) 13 | 14 | (defun not (condition) 15 | (empty? condition)) 16 | 17 | (defmacro when (condition &rest body) 18 | (list 'if condition (append (list 'progn) body) f)) 19 | 20 | (defmacro unless (condition &rest body) 21 | (list 'if condition f (append (list 'progn) body))) 22 | 23 | (defun each (items iterator) 24 | (unless (empty? items) 25 | (iterator (car items)) 26 | (each (cdr items) iterator))) -------------------------------------------------------------------------------- /fib.jr: -------------------------------------------------------------------------------- 1 | (defun fib (n) 2 | (if (< n 2) 3 | 1 4 | (+ (fib (- n 1)) (fib (- n 2))))) 5 | (puts (fib 10)) -------------------------------------------------------------------------------- /jasper.js: -------------------------------------------------------------------------------- 1 | var Jasper = (function(global) { 2 | this.globalObject = global 3 | 4 | // entrance 5 | function jasper(input) { 6 | return jevalForms(this, parse(input)) 7 | } 8 | jasper.debug = false 9 | 10 | // jasper eval 11 | function jeval(context, stream) { 12 | if (emptyp(stream)) return null 13 | 14 | if (stream.constructor == Array) { 15 | if (emptyp(car(stream))) return null 16 | 17 | if (car(stream).constructor == Array && emptyp(cdr(stream))) { 18 | return jeval(context, car(stream)) 19 | } else { 20 | return apply(context, car(stream), cdr(stream)) 21 | } 22 | } else { 23 | return valueOfToken(context, stream) 24 | } 25 | } 26 | 27 | // important! 28 | function jevalForms(context, forms) { 29 | var ret 30 | for (var key in forms) ret = jeval(context, forms[key]) 31 | return ret 32 | } 33 | 34 | // evaluate a token 35 | function valueOfToken(context, token) { 36 | if (/^([\"0-9].*|true|false|null|undefined)$/.test(token)) { 37 | // numbers and strings eval to themselves 38 | return eval(token) 39 | } else if (/^\'/.test(token)) { 40 | // quote literal 41 | return token.slice(1, token.length) 42 | } else if (typeof token == 'function') { 43 | return token 44 | } else { 45 | // it's a symbol - look it up 46 | return symbolLookup(context, token) 47 | } 48 | } 49 | 50 | // call a function or macro 51 | function apply(context, name, rest) { 52 | var result, args = [], form 53 | 54 | form = symbolLookup(context, name) 55 | 56 | if (!form) throw "Form undefined: " + name 57 | 58 | if (form.special || form.macro) { 59 | args = rest 60 | } else { 61 | for (var key in rest) args.push(jeval(context, rest[key])) 62 | } 63 | 64 | debug('funcall: ' + name + '; args: ' + args.toString()) 65 | result = form.apply(context, args) 66 | if (result) debug('funcall: ' + name + '; result: ' + result.toString()) 67 | return form.macro ? jeval(context, result) : result 68 | } 69 | 70 | function symbolLookup(context, target) { 71 | while (context) { 72 | if (typeof context[target] != 'undefined') return context[target] 73 | context = context.parentContext 74 | } 75 | throw "Can't find " + target 76 | } 77 | 78 | // our two parsing methods 79 | function parse(input) { 80 | var token, tokens = tokenize(input), stack = [] 81 | 82 | while (tokens.length > 0) { 83 | token = tokens.shift() 84 | 85 | if (token == '(') { 86 | stack.push(parse(tokens)) 87 | } else if (token == ')') { 88 | return stack 89 | } else { 90 | stack.push(token) 91 | } 92 | } 93 | 94 | return stack 95 | } 96 | 97 | function tokenize(input) { 98 | if (input.constructor == Array) return input 99 | var match, token, regexp = /\s*(\(|\)|".+?"|[^\s()]+|$)/g, tokens = [] 100 | 101 | while ((match = input.match(regexp)).length > 1) { 102 | input = input.replace(match[0], '') 103 | tokens.push( match[0].replace( /^\s+|\s+$/g, '' ) ) 104 | } 105 | 106 | return tokens 107 | } 108 | 109 | // debug 110 | this['puts'] = function(string) { 111 | if (globalObject['console']) return console.log(string) 112 | if (globalObject['Ruby']) return Ruby.puts(string) 113 | if (globalObject['print']) return print(string) 114 | } 115 | 116 | this['debug'] = function(string) { 117 | if (jasper.debug) puts(string) 118 | } 119 | 120 | // everyone's favorites - list building blocks 121 | this['cons'] = function(a, b) { 122 | return append([a], b) 123 | } 124 | 125 | this['car'] = function(sexp) { 126 | return sexp[0] 127 | } 128 | 129 | this['cdr'] = function(sexp) { 130 | if (sexp && sexp.slice) 131 | return sexp.slice(1, sexp.length) 132 | else 133 | return sexp 134 | } 135 | 136 | // essentials 137 | this['if'] = function(sif, sthen, selse) { 138 | return jeval(this, sif) ? jeval(this, sthen) : jevalForms(this, [selse]) 139 | } 140 | this['if'].special = true 141 | 142 | this['and'] = function() { 143 | var ret, i 144 | 145 | for (i = 0; i < arguments.length; i++) { 146 | if (ret === false) return false 147 | ret = jeval(this, arguments[i]) 148 | } 149 | 150 | return ret 151 | } 152 | this['and'].special = true 153 | 154 | this['empty?'] = function(sexp) { 155 | return !sexp || sexp.length == 0 156 | } 157 | // alias 158 | var emptyp = this['empty?'] 159 | 160 | this['list?'] = function(sexp) { 161 | return sexp && sexp.constructor == Array 162 | } 163 | var listp = this['list?'] 164 | 165 | this['list'] = function() { 166 | var i, arr = [] 167 | for (i = 0; i < arguments.length; i++) arr.push(arguments[i]) 168 | return arr 169 | } 170 | 171 | this['append'] = function() { 172 | var i, j, arr = [] 173 | for (i = 0; i < arguments.length; i++) 174 | for (j = 0; j < arguments[i].length; j++) 175 | arr.push(arguments[i][j]) 176 | return arr 177 | } 178 | 179 | this['progn'] = function() { 180 | var i, ret 181 | for (i = 0; i < arguments.length; i++) ret = jeval(this, arguments[i]) 182 | return ret 183 | } 184 | 185 | // λ 186 | this['lambda'] = function(params, rest) { 187 | rest = Array.prototype.slice.call(arguments, 1, arguments.length) 188 | return function() { 189 | var i, context = {} 190 | context.parentContext = this 191 | 192 | if (params.length > 0) { 193 | // bind variables 194 | for (i = 0; i < params.length; i++) { 195 | if (params[i] == '&rest') { 196 | i++ 197 | context[params[i]] = Array.prototype.slice.call(arguments, (i-1), arguments.length) 198 | } else { 199 | context[params[i]] = arguments[i] 200 | } 201 | } 202 | } 203 | 204 | return jevalForms(context, rest) 205 | } 206 | } 207 | this['lambda'].special = true 208 | 209 | // basic assignment 210 | this['='] = function(symbol, value) { 211 | this[symbol] = jeval(this, value) 212 | } 213 | this['='].special = true 214 | 215 | // basic comparison 216 | this['=='] = function(a, b) { 217 | return a == b 218 | } 219 | 220 | // can't write this in jasper, you can't apply eval 221 | this['js'] = function(string) { 222 | return eval(string) 223 | } 224 | 225 | // creation of macros 226 | this['defmacro'] = function(name, args, rest) { 227 | rest = Array.prototype.slice.call(arguments, 2, arguments.length) 228 | this[name] = lambda.call(this, args, rest) 229 | this[name].macro = true 230 | return null 231 | } 232 | this['defmacro'].special = true 233 | 234 | // math primitives 235 | this['+'] = function() { 236 | var i, sum = 0 237 | 238 | for (i = 0; i < arguments.length; i++) 239 | sum += arguments[i] 240 | 241 | return sum 242 | } 243 | 244 | this['-'] = function() { 245 | var i, diff = arguments[0] 246 | 247 | for (i = 1; i < arguments.length; i++) 248 | diff -= arguments[i] 249 | 250 | return diff 251 | } 252 | 253 | this['*'] = function() { 254 | var i, product = arguments[0] 255 | 256 | for (i = 1; i < arguments.length; i++) 257 | product *= arguments[i] 258 | 259 | return product 260 | } 261 | 262 | this['/'] = function() { 263 | var i, quotient = arguments[0] 264 | 265 | for (i = 1; i < arguments.length; i++) 266 | quotient /= arguments[i] 267 | 268 | return quotient 269 | } 270 | 271 | this['<'] = function(a, b) { return a < b } 272 | this['<='] = function(a, b) { return a <= b } 273 | this['>'] = function(a, b) { return a > b } 274 | this['>='] = function(a, b) { return a >= b } 275 | 276 | // loading files 277 | this['load'] = function(file) { 278 | if (!/\.jr$/.test(file)) 279 | file = file + ".jr" 280 | 281 | if ('Ruby' in globalObject) { 282 | jasper( Ruby.File.read(file) ) 283 | } else if ('XMLHttpRequest' in globalObject) { 284 | var xhr = new XMLHttpRequest 285 | xhr.open('GET', file, false) 286 | xhr.send(null) 287 | jasper( xhr.responseText ) 288 | } else { 289 | throw "Can't load " + file 290 | } 291 | } 292 | 293 | // dirt simple api 294 | jasper.scope = this 295 | 296 | // Jasper(string) 297 | return jasper 298 | })(this); 299 | 300 | Jasper.init = function() { 301 | Jasper.scope.load('core.jr') 302 | } 303 | -------------------------------------------------------------------------------- /repl.js: -------------------------------------------------------------------------------- 1 | Jasper.REPL = { 2 | eval: function() { 3 | var obuffer = '', textarea = document.getElementById('jasper-out') 4 | 5 | Jasper.scope.puts = function(string) { 6 | obuffer += string 7 | obuffer += "\n" 8 | } 9 | 10 | var out = Jasper(document.getElementById('jasper-in').value) 11 | textarea.value = (textarea.value ? textarea.value + "\n" : '') + obuffer + '=> ' + out 12 | textarea.scrollTop = textarea.scrollHeight 13 | }, 14 | 15 | init: function() { 16 | Jasper.load('http://github.com/defunkt/jasper/raw/master/core.jr') 17 | } 18 | } --------------------------------------------------------------------------------