├── cases ├── 8.js ├── 5.js ├── 8.js.wrong ├── c.js ├── 1.js ├── 2.js ├── g.js ├── 4.js ├── f.js ├── a.js ├── 6.js ├── 7.js ├── 9.js ├── 9.js.wrong ├── j.js ├── 3.js ├── k.js ├── l.js ├── m.js ├── b.js ├── b.js.wrong ├── e.js.wrong ├── e.js ├── h.js ├── d.js ├── d.js.wrong ├── h.js.wrong ├── nested.html ├── i.js.wrong └── i.js ├── hw.html ├── README.mdown ├── jx └── jexpr.js /cases/8.js: -------------------------------------------------------------------------------- 1 | let {x: 5} in: (display x) 2 | -------------------------------------------------------------------------------- /cases/5.js: -------------------------------------------------------------------------------- 1 | let {x: 5} in: 2 | display x 3 | -------------------------------------------------------------------------------- /cases/8.js.wrong: -------------------------------------------------------------------------------- 1 | let {x: 5} in: display x 2 | -------------------------------------------------------------------------------- /cases/c.js: -------------------------------------------------------------------------------- 1 | display (length (list 1 2 3)) 2 | -------------------------------------------------------------------------------- /cases/1.js: -------------------------------------------------------------------------------- 1 | display "hello world" 2 | display "been there" 3 | -------------------------------------------------------------------------------- /cases/2.js: -------------------------------------------------------------------------------- 1 | display "hello world" 2 | display "been there" 3 | -------------------------------------------------------------------------------- /cases/g.js: -------------------------------------------------------------------------------- 1 | let {x: table {cat: "meow"}} in: 2 | display x.cat 3 | -------------------------------------------------------------------------------- /cases/4.js: -------------------------------------------------------------------------------- 1 | if false 2 | then: (display "hello") 3 | else: (display "world") 4 | -------------------------------------------------------------------------------- /cases/f.js: -------------------------------------------------------------------------------- 1 | let {msg: "hello"} in: 2 | display ($_ (_$ msg) 'let world') 3 | -------------------------------------------------------------------------------- /cases/a.js: -------------------------------------------------------------------------------- 1 | macro hello 2 | lambda: x 3 | body: x.hello 4 | display (hello 3) 5 | -------------------------------------------------------------------------------- /cases/6.js: -------------------------------------------------------------------------------- 1 | let {x: lambda y body: y} in: 2 | display (x 5) 3 | display "done" 4 | -------------------------------------------------------------------------------- /cases/7.js: -------------------------------------------------------------------------------- 1 | let {x: lambda y z body: z} in: 2 | display (x 5 6) 3 | display "done" 4 | -------------------------------------------------------------------------------- /cases/9.js: -------------------------------------------------------------------------------- 1 | display 2 | for {x: from 10 to: 13, 3 | y: from 100 to: 104} 4 | expr: (list "hello" x y) 5 | -------------------------------------------------------------------------------- /cases/9.js.wrong: -------------------------------------------------------------------------------- 1 | display 2 | for {x: from 10 to: 13, 3 | y: from 100 to: 104} 4 | expr: list "hello" x y 5 | -------------------------------------------------------------------------------- /cases/j.js: -------------------------------------------------------------------------------- 1 | do 2 | display "hello world using fn" 3 | let {f: fn x to: x.length} in: 4 | display (f "meow") 5 | -------------------------------------------------------------------------------- /cases/3.js: -------------------------------------------------------------------------------- 1 | do 2 | display "hello world" 3 | let {f: lambda x body: x.length} in: 4 | display (f "meow") 5 | -------------------------------------------------------------------------------- /cases/k.js: -------------------------------------------------------------------------------- 1 | for { i: from 1 to: 10 2 | , j: from 1 to: 10 when: (fn j to: math j * 2 > i) 3 | } 4 | body: 5 | display (list i j) 6 | -------------------------------------------------------------------------------- /cases/l.js: -------------------------------------------------------------------------------- 1 | for { i: from 1 to: 10 2 | , j: from 1 to: 10 when: (fn j to: math j * 2 > i and i < 5) 3 | } 4 | body: 5 | display (list i j) 6 | -------------------------------------------------------------------------------- /cases/m.js: -------------------------------------------------------------------------------- 1 | for { i: from 1 to: 10 2 | , j: from 1 to: 10 when: (fn j to: math (j * 2) > i and (i < 5)) 3 | } 4 | body: 5 | display (list i j) 6 | -------------------------------------------------------------------------------- /cases/b.js: -------------------------------------------------------------------------------- 1 | do 2 | define {x: 5} 3 | define {fn: lambda y body: (table {x: x, y: y})} 4 | define {x: 10} 5 | display (fn 10) 6 | display x 7 | -------------------------------------------------------------------------------- /cases/b.js.wrong: -------------------------------------------------------------------------------- 1 | do 2 | define {x: 5} 3 | define {fn: lambda y body: table {x: x, y: y}} 4 | define {x: 10} 5 | display (fn 10) 6 | display x 7 | -------------------------------------------------------------------------------- /cases/e.js.wrong: -------------------------------------------------------------------------------- 1 | macro hello 2 | lambda: expr 3 | body: $_ (display (list "In macro!" (_$$ meow expr.hello))) 4 | where: {meow: $ "bowow"} 5 | hello "macro" "world!" 6 | -------------------------------------------------------------------------------- /cases/e.js: -------------------------------------------------------------------------------- 1 | macro hello 2 | lambda: expr 3 | body: ($_ (display (list "In macro!" (_$$ meow ($_ (list (_$ expr.hello))))))) 4 | where: {meow: $ "bowow"} 5 | hello "macro" "world!" 6 | -------------------------------------------------------------------------------- /cases/h.js: -------------------------------------------------------------------------------- 1 | let {greet: lambda msg body: 2 | display "Hello lambda world!" 3 | display msg 4 | display keywords.lockword} 5 | in: (greet "Planett earth rocks!" lockword: "haha!") 6 | 7 | -------------------------------------------------------------------------------- /cases/d.js: -------------------------------------------------------------------------------- 1 | apply (lambda msg body: 2 | display "hello lambda world ..." 3 | display msg 4 | display keywords) 5 | 6 | args: (list "planet earth rocks!") 7 | keywords: (table (global "cooling ftw!")) 8 | 9 | -------------------------------------------------------------------------------- /cases/d.js.wrong: -------------------------------------------------------------------------------- 1 | apply (lambda msg body: 2 | display "hello lambda world ..." 3 | display msg 4 | display keywords) 5 | 6 | args: list "planet earth rocks!" 7 | keywords: table (global "cooling ftw!") 8 | 9 | -------------------------------------------------------------------------------- /cases/h.js.wrong: -------------------------------------------------------------------------------- 1 | let {greet: lambda msg 2 | body: display "Hello lambda world!" 3 | display msg 4 | display keywords.lockword} 5 | in: greet "Planett earth rocks!" lockword: "haha!" 6 | 7 | -------------------------------------------------------------------------------- /cases/nested.html: -------------------------------------------------------------------------------- 1 | 8 | 9 | -------------------------------------------------------------------------------- /cases/i.js.wrong: -------------------------------------------------------------------------------- 1 | let {ruler: lambda arg 2 | body: if keywords.double_rule 3 | then: display "===================" 4 | else: display "-------------------" 5 | display arg 6 | , another: "yeow"} 7 | in: ruler (list "An important message!" another) 8 | double_rule: true 9 | -------------------------------------------------------------------------------- /cases/i.js: -------------------------------------------------------------------------------- 1 | let {ruler: lambda arg body: 2 | if keywords.double_rule 3 | then: (display "===================") 4 | else: (display "-------------------") 5 | display arg 6 | , another: "yeow"} 7 | in: 8 | ruler (list "An important message!" another) 9 | double_rule: true 10 | -------------------------------------------------------------------------------- /hw.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /README.mdown: -------------------------------------------------------------------------------- 1 | An attempt at using JSON to notate the abstract syntax tree of a language, thus 2 | yielding a language capable of natural macros just like the lisp family. 3 | 4 | The current compiler implementation is written in "stream of thought" style. 5 | Works, but it isn't production code and intended as proof of concept. 6 | 7 | See [the jexpr docco docs](http://srikumarks.github.io/jexpr) for more info. 8 | 9 | The `node.js` script named `jx` can compile jexpr files into Javascript or run 10 | them directly. Run the script with no arguments for info on how to use it. 11 | 12 | The `jexpr.js` file can be script-included in a web page and it will scan and 13 | execute all the script tags with `type="application/x-jexpr"` attribute. 14 | 15 | -------------------------------------------------------------------------------- /jx: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | J_enable_tests = false; 4 | 5 | var J = require('./jexpr.js') 6 | var fs = require('fs'); 7 | var verbose = false; 8 | 9 | if (process.argv.length <= 2) { 10 | process.stderr.write('Usage: jx file1.js file2.js ...\n'); 11 | process.stderr.write(' Will run all the code in the given files.\n\n'); 12 | process.stderr.write('Usage: jx -C file1.js file2.js ... > out.js\n'); 13 | process.stderr.write(' Will compile all the code in the given files\n'); 14 | process.stderr.write(' and output the result to stdout.\n\n'); 15 | process.stderr.write('Usage: jx -P file1.js file2.js ... > out.js\n'); 16 | process.stderr.write(' Will parse all the code in the given files and\n'); 17 | process.stderr.write(' write it out as an array of expressions in JSON form.\n\n'); 18 | process.stderr.write('Usage: jx file1.js -c file2.js file3.js ...\n'); 19 | process.stderr.write(' Will run file1.js, file3.js, and others and \n'); 20 | process.stderr.write(' then compile file2.js. Useful to load macros.\n'); 21 | process.stderr.write('\nThe "-v" flag turns on verbose dumping of the parsed JSON.\n\n'); 22 | } else { 23 | var exprs_to_run = [], exprs_to_compile = [], all_exprs = []; 24 | var compile_next = false, compile_reset = true, parse_only = false; 25 | process.argv.slice(2).forEach(function (f) { 26 | if (f === '-c') { 27 | compile_next = true; 28 | } else if (f === '-C') { 29 | compile_next = true; 30 | compile_reset = false; 31 | } else if (f === '-v') { 32 | verbose = true; 33 | } else if (f === '-P') { 34 | parse_only = true; 35 | } else { 36 | var source = fs.readFileSync(f, 'utf8'); 37 | var p = J.parse(source); 38 | var exprs = compile_next ? exprs_to_compile : exprs_to_run; 39 | var expr; 40 | while ((expr = p()) !== undefined) { 41 | if (verbose) { 42 | console.log(JSON.stringify(expr)); 43 | } 44 | exprs.push(expr); 45 | all_exprs.push(expr); 46 | } 47 | if (compile_reset) { 48 | compile_next = false; 49 | } 50 | } 51 | }) 52 | 53 | if (parse_only) { 54 | process.stdout.write(JSON.stringify(all_exprs)); 55 | } else { 56 | var rt = J.runtime(); 57 | if (exprs_to_run.length > 0) { 58 | J.eval({do: exprs_to_run}, rt); 59 | } 60 | if (exprs_to_compile.length > 0) { 61 | process.stdout.write(J.compile_to_js({do: exprs_to_compile})); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /jexpr.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012, Srikumar K. S. (srikumarks.github.com) 2 | // 3 | // Code licensed for use and redistribution with warranties 4 | // (or the lack thereof) as described in the MIT licence. 5 | // License URL: http://www.opensource.org/licenses/MIT 6 | 7 | // This is an attempt at developing a language using JSON objects as containers 8 | // for the AST, similar to how list expressions serve as a representation for 9 | // ASTs in the lisp family of languages. The key idea exploited here is that 10 | // browser based Javascript engines such as V8 always enumerate the keys of an 11 | // object in the same order as they were inserted. Though this is not required 12 | // by ECMAScript, it is considered standard behaviour in browser environments 13 | // and in Node.js too (since it uses V8). 14 | // 15 | // The overall structure of a j-expression is like this - 16 | // 17 | // {operator: [args...], 18 | // keyword1: value1, 19 | // keyword2: value2, 20 | // ...} 21 | // 22 | // .. and we'll call the language "J" here for brevity. 23 | // 24 | // #### Relevant posts 25 | // 26 | // 1. [J-expressions] 27 | // 2. [DSLs using JSON expressions] 28 | // 29 | // [DSLs using JSON expressions]: http://srikumarks.github.com/gyan/2012/04/14/creating-dsls-in-javascript-using-j-expressions/ 30 | // [J-expressions]: http://srikumarks.github.com/gyan/2012/04/15/j-expressions/ 31 | 32 | var J = (function (enable_tests) { 33 | 34 | // We single out the first key presented in a JSON object as the name of the 35 | // operator. 36 | function operatorName(obj) { 37 | if (obj && obj.constructor === Object) { 38 | for (var k in obj) { 39 | return k; 40 | } 41 | } else { 42 | return undefined; 43 | } 44 | }; 45 | 46 | // ## Compilation environments 47 | 48 | // We need an environment structure to remember the scope of bindings 49 | // established as we develop the compiler and as the compiler walks through the 50 | // AST. We start with a simple environment definition with the ability to 51 | // construct an environment with another one as its "parent scope". 52 | var Env = function (base) { 53 | if (base) { 54 | this.base = base; 55 | this.symbols = Object.create(base.symbols); 56 | } else { 57 | this.symbols = {}; 58 | } 59 | } 60 | 61 | // We now start with the basic compiler that supports simple object types - 62 | // numbers and booleans. The compiled form of these is simply the JSON 63 | // stringification so that we can insert them into the compiled code directly 64 | // as literals. 65 | function compile_lit(env, expr) { 66 | 67 | if (expr === undefined || expr === null) { 68 | return JSON.stringify(expr); 69 | } 70 | 71 | if (expr.constructor === Number || expr.constructor == Boolean) { 72 | return JSON.stringify(expr); 73 | } 74 | 75 | return undefined; 76 | } 77 | 78 | // Strings are a bit special. We're going to need symbols in our 79 | // language. Since JS doesn't have a separate symbol type, we'll 80 | // just use plain strings as symbols and worry about strings later on. 81 | // This means we're going to need a way to lookup the compile-value of a 82 | // symbol in an environment first. Use a namespace prefix to avoid 83 | // touching the builtin properties. 84 | function lookupSymbol(env, sym) { 85 | return env.symbols['J_' + sym]; 86 | } 87 | 88 | // We now add a simple function to define new things into 89 | // a given environment. 90 | function define(env, name, value) { 91 | return env.symbols['J_' + name] = value; 92 | } 93 | 94 | // We can now write our symbol compilation. This looks up 95 | // the value in the environment and just returns it if found. 96 | function compile_sym(env, sym) { 97 | if (sym && sym.constructor === String) { 98 | return lookupSymbol(env, sym); 99 | } 100 | 101 | return undefined; 102 | } 103 | 104 | // A new "variable" in our language will be mapped to a javascript 105 | // variable by attaching a special prefix so that the language 106 | // cannot escape its boundaries. We also use the environment's 107 | // "id" number in the name so that the JS variables associated 108 | // with different environments can be told apart. 109 | var idRx = /^[a-zA-Z_\$][a-zA-Z0-9_\$]*$/; 110 | function varname(env, sym) { 111 | if (!idRx.test(sym)) { 112 | throw "Bad identifier!"; 113 | } 114 | return 'var$' + env.id + '$' + sym; 115 | } 116 | 117 | function varnames(env, syms) { 118 | return syms.map(function (sym) { 119 | return varname(env, sym); 120 | }); 121 | } 122 | 123 | function newvar(env, sym) { 124 | return define(env, sym, varname(env, sym)); 125 | } 126 | 127 | function newvars(env, syms) { 128 | if (typeof syms === 'string') { 129 | return [newvar(env, syms)]; 130 | } else { 131 | return syms.map(function (sym) { 132 | return newvar(env, sym); 133 | }); 134 | } 135 | }; 136 | 137 | // Oops. We haven't defined an environment's ID. Let's patch 138 | // Env to add that. 139 | function patch(oldEnv, change) { 140 | function NewEnv(base) { 141 | oldEnv.call(this, base); 142 | change.call(this, base); 143 | } 144 | 145 | NewEnv.prototype = oldEnv.prototype; 146 | return NewEnv; 147 | } 148 | 149 | var globallyUniqueEnvID = 1; 150 | Env = patch(Env, function (base) { 151 | this.id = (globallyUniqueEnvID++); 152 | }); 153 | 154 | // Lets also add some options that we can store and 155 | // inherit over the Env chain. 156 | Env = patch(Env, function (base) { 157 | this.options = (base ? Object.create(base.options) : {}); 158 | }); 159 | 160 | // We're now ready to process our first J-expression. We treat 161 | // the first key as the symbol standing for an operator, fetch 162 | // the function that implements the operator and just call it. 163 | // Note that if the looked up value is a function, it is really 164 | // a "macro" because we're writing a *compiler*. Actual value 165 | // lookup will yield a string which we can use as a JS expression 166 | // in the compiled result directly. 167 | // {operator: [arguments...], keyword1: value1, ...} 168 | function compile_jexpr(env, jexpr) { 169 | if (jexpr && jexpr.constructor === Object) { 170 | var op = lookupSymbol(env, operatorName(jexpr)); 171 | if (op && op.constructor === Function) { 172 | return op(env, jexpr); /* We have a native implementation available. 173 | * We pass the entire body of the expression 174 | * to it without evaluating anything else. 175 | */ 176 | } 177 | 178 | if (op && op.constructor === String) { 179 | return compile_apply(env, op, jexpr); /* This is an already compiled value. So just 180 | * treat it as a function and apply it. 181 | */ 182 | } 183 | 184 | if (!op && env.options.unsafe) { 185 | // Treat it as a globally available thingie. 186 | return compile_apply(env, operatorName(jexpr), jexpr); 187 | } 188 | } else if (jexpr && jexpr.constructor === Array) { 189 | // This is s-expression fallback case where the operator 190 | // expression is not a string. 191 | var kw = jexpr.keywords; 192 | var jexpr2 = {}; 193 | jexpr2['sexpr'] = jexpr.slice(1); 194 | if (kw) { 195 | Object.keys(kw).forEach(function (k) { 196 | jexpr2[k] = kw[k]; 197 | }); 198 | } 199 | return compile_apply(env, compile(env, jexpr[0]), jexpr2); 200 | } 201 | 202 | return undefined; 203 | } 204 | 205 | // We now need to build our whole compilation function so 206 | // that we can just call it to compile any j-expression 207 | // or primitive type. 208 | function compile(env, expr) { 209 | return compile_jexpr(env, expr) 210 | || compile_sym(env, expr) 211 | || compile_lit(env, expr); 212 | } 213 | 214 | // To help ourselves a bit, let's define a mapping utility 215 | // that applies a two-argument "macro-like" function to 216 | // an array of expressions. 217 | function map(env, fn, exprs) { 218 | return exprs.map(function (expr) { 219 | return fn(env, expr); 220 | }); 221 | } 222 | 223 | // The "arguments" of an operator are provided as an array value. 224 | // Each element is compiled in turn and the result used as the 225 | // argument-list of the compiled javascript function. 226 | function compile_args(env, argv) { 227 | if (argv && argv.constructor === Array) { 228 | return map(env, compile_args, argv).join(','); 229 | } else { 230 | // Compile it as a single expression. 231 | return compile(env, argv); 232 | } 233 | } 234 | 235 | // Now we are ready to write the compile_apply, which will 236 | // apply a compiled function by symbol reference to a given 237 | // arguments list. 238 | function compile_apply(env, op, jexpr) { 239 | return op + '(' + compile_args(env, jexpr[operatorName(jexpr)]) + ')'; 240 | } 241 | 242 | 243 | // ## Primitives 244 | // 245 | // Ok, so far we have not implemented any primitives. Our first one 246 | // is going to be a mechanism for expressions to return literal JSON 247 | // objects. This is the analog of "quote" in Scheme and we'll use 248 | // the succinct "$" symbol as the key of a jexpr to represent quoted 249 | // forms. We'll insert these primitives into a "primitives" environment. 250 | var Prim = new Env; 251 | 252 | define(Prim, '$', function (env, expr) { 253 | return JSON.stringify(expr.$); 254 | }); 255 | 256 | // And now for an ultra simple "display" implementation. 257 | // After all, how are we going to write a "hello world" program 258 | // without this one! 259 | define(Prim, 'display', function (env, expr) { 260 | return '(console.log(' + compile(env, expr.display) + '), null)'; 261 | }); 262 | 263 | // We're now ready to do a "hello world". But first some helper stuff. 264 | // We're going to be making new environments. So let's make a helper 265 | // method on Env to make a new derived environment. 266 | function subenv(env) { 267 | return new Env(env); 268 | } 269 | 270 | // HELLO WORLD! 271 | if (enable_tests) { 272 | eval(compile(subenv(Prim), {display: {$: "Hello world!"}})); 273 | } 274 | 275 | // Let's wrap that little piece of code into a "run" method 276 | // and insert it into the environment so that programs can 277 | // be run within environments. 278 | 279 | // "run" will run the expressions in a new child environment 280 | // without affecting the target environment. 281 | Env.prototype.run = function () { 282 | var env = subenv(this); 283 | var result; 284 | Array.prototype.forEach.call(arguments, function (expr) { 285 | result = eval(compile(env, expr)); 286 | }); 287 | return result; 288 | }; 289 | 290 | 291 | // Now lets implement some more primitives! 292 | // We'll do the useful "list" macro which will take 293 | // a bunch of arguments and produce an list (a JS array) 294 | // out of them. This has to be a macro because we're 295 | // constructing an array for use at *runtime*. 296 | define(Prim, 'list', function (env, expr) { 297 | if (expr.list.constructor === Array) { 298 | return '[' 299 | + expr.list.map(function (e) { 300 | return compile(env, e); 301 | }).join(',') 302 | + ']'; 303 | } else { 304 | return '[' + compile(env, expr.list) + ']'; 305 | } 306 | }); 307 | 308 | // ... and list length 309 | define(Prim, 'length', '(function (x) { return x.length; })'); 310 | 311 | if (enable_tests) { 312 | Prim.run({display: {length: [{list: [1,2,3]}]}}); 313 | } 314 | 315 | // We'll also put in a macro for constructing tables. 316 | // This has to be a macro because we're going to have 317 | // to evaluate the value fields of the given object. 318 | // 319 | // {table: {x: 2, y: {$: "why?"}}} 320 | // 321 | // should produce what you think it should. 322 | define(Prim, 'table', function (env, expr) { 323 | var keys = Object.keys(expr.table); 324 | return '{' 325 | + keys.map(function (k) { 326 | return JSON.stringify(k) + ':' + compile(env, expr.table[k]); 327 | }).join(',') 328 | + '}'; 329 | }); 330 | 331 | // ### Let there be lambda 332 | // 333 | // Now for the BIG BOY! The syntax we use for lambda is like this - 334 | // 335 | // {lambda: ["arg1", "arg2", ...], 336 | // body: expr|[expr1, expr2, ..., exprN]} 337 | // 338 | // We turn that into a JS function like this - 339 | // 340 | // function (arg1, arg2, ...) { 341 | // var keywords = this; 342 | // return (expr1, expr2, ... exprN); 343 | // } 344 | // 345 | // We also make "this" available as the special symbol 'keywords' 346 | // with the intention of passing in optional arguments through 'this'. 347 | define(Prim, 'lambda', function (env, expr) { 348 | var env2 = subenv(env); 349 | return '(function (' 350 | + newvars(env2, expr.lambda).join(',') 351 | + ') {' 352 | + 'var ' + newvar(env2, 'keywords') + ' = this;' 353 | + 'return (' + compile_args(env2, expr.body) + ');})'; 354 | }); 355 | 356 | // Alias 'lambda:body:' as 'fn:to:'. 357 | define(Prim, 'fn', function (env, expr) { 358 | var k = Object.keys(expr); 359 | k.shift(); 360 | var expr2 = {lambda: expr.fn}; 361 | k.forEach(function (k) { 362 | if (k === 'to') { 363 | expr2['body'] = expr[k]; 364 | } else { 365 | expr2[k] = expr[k]; 366 | } 367 | }); 368 | return lookupSymbol(Prim, 'lambda')(env, expr2); 369 | }); 370 | 371 | // ### Optional keyword arguments 372 | 373 | // That was easy! ... but the lambda is unable to make 374 | // use of optional keyword arguments yet and that would be a real waste. 375 | // To support that, at call time, we'll pass the compiled version of 376 | // the call expression body to the lambda as a table so that it can 377 | // access the arguments other than the args array through the local 378 | // "keywords" symbol. 379 | 380 | // First, we need to compile the entire expression as a value. 381 | function compile_exprval(env, expr, keys) { 382 | return '{' 383 | + keys.map(function (k) { 384 | return JSON.stringify(k) 385 | + ':' 386 | + (compile_array(env, expr[k]) || compile(env, expr[k])); 387 | }).join(',') 388 | + '}'; 389 | } 390 | 391 | // Now we need to patch compile_apply to check for the presence 392 | // of keywords and if so pass it as the "this" part of a call. 393 | var compile_apply = (function (prevCompileApply) { 394 | return function (env, op, jexpr) { 395 | var keys = Object.keys(jexpr); 396 | if (keys.length === 1) { 397 | return prevCompileApply(env, op, jexpr); // Avoid the overhead of a ".call" 398 | } else { 399 | var opname = operatorName(jexpr); 400 | keys.shift(); // Drop the operator. 401 | return op 402 | + '.call(' 403 | + compile_exprval(env, jexpr, keys) 404 | + ',' 405 | + compile_args(env, jexpr[opname]) 406 | + ')'; 407 | } 408 | }; 409 | }(compile_apply)); 410 | 411 | 412 | // This just compiles the parts of the array and wraps it with the 413 | // array constructor. 414 | function compile_array(env, arr) { 415 | if (arr && arr.constructor === Array) { 416 | return '[' + compile_args(env, arr) + ']'; 417 | } 418 | 419 | return undefined; 420 | } 421 | 422 | // And to *use* lambda, we're going to need apply. 423 | // 424 | // {apply: funval, args: listval, keywords: tableval} 425 | define(Prim, 'apply', function (env, expr) { 426 | return compile(env, expr.apply) 427 | + '.apply(' 428 | + (expr.keywords ? compile(env, expr.keywords) : 'null') 429 | + ',' 430 | + compile(env, expr.args) 431 | + ')'; 432 | }); 433 | 434 | // call func arg1 arg2 ... kw1: val1 kw2: val2 ... 435 | define(Prim, 'call', function (env, expr) { 436 | var op, argv, keywords; 437 | keywords = Object.keys(expr); 438 | keywords.unshift(); // Drop 'call'. 439 | var keyvals = {}; 440 | keyvals.call = expr.call.constructor === Array ? expr.call.slice(1) : []; 441 | keywords.forEach(function (k) { keyvals[k] = expr[k]; }); 442 | var op = '(' + compile_args(env, expr.call.constructor === Array ? expr.call[0] : expr.call) + ')'; 443 | return compile_apply(env, op, keywords); 444 | }); 445 | 446 | // Lets now try a lambda hello world. 447 | if (enable_tests) { 448 | Prim.run({apply: {lambda: ["msg"], 449 | body: [ 450 | {display: {$: "Hello lambda world ..."}}, 451 | {display: "msg"}, 452 | {display: "keywords"} 453 | ]}, 454 | args: {list: [{$: "planet earth rocks!"}]}, 455 | keywords: {table: {global: {$: "cooling ftw!"}}}}); 456 | } 457 | 458 | 459 | // ### "where" clauses 460 | // 461 | // Now let's add something "interesting" to lambda 462 | // - a "where" clause. The idea is that whenever we have an extra 463 | // "where: {key1: val1, key2: val2,..}" entry in a j-expression, 464 | // we make those keys available like local variables within the 465 | // scope of the expression. Let's generalize this feature first. 466 | // 467 | // What we do is to turn {...where: {x: val1, y: val2} ...} 468 | // as a function wrapper like - 469 | // 470 | // (function (x, y) { 471 | // ..expr.. 472 | // }(val1, val2)) 473 | function whereClause(env, expr, where, macro) { 474 | if (!where) { 475 | return macro(env, expr); 476 | } 477 | 478 | var whereEnv = subenv(env); 479 | var whereVars = Object.keys(where); 480 | 481 | return '(function (' + newvars(whereEnv, whereVars) + ') {' 482 | + 'return (' + macro(whereEnv, expr) + ');}' 483 | + '(' 484 | + whereVars.map(function (v) { 485 | return '('+compile_args(env, where[v])+')'; 486 | }).join(',') 487 | + '))'; 488 | } 489 | 490 | // Now we can add where clause support to lambda. 491 | define(Prim, 'lambda', (function (oldLambda) { 492 | return function (env, expr) { 493 | return whereClause(env, expr, expr.where, oldLambda); 494 | }; 495 | }(lookupSymbol(Prim, 'lambda')))); 496 | 497 | // ### Macros 498 | // 499 | // Now we up the game a bit and define the ability to 500 | // write macros. We've already been writing macros, 501 | // so we just need to expose that bit of functionality 502 | // to the language itself. Macros are just lambdas that 503 | // take the entire expression as a single argument 504 | // and return an expression to be used instead. 505 | // 506 | // {macro: "name", 507 | // lambda: ["expr"], 508 | // body: ..., 509 | // where: ...} 510 | define(Prim, 'macro', function (env, expr) { 511 | var macrodefn = eval(lookupSymbol(env, 'lambda')(env, expr)); 512 | define(env, expr.macro, function (env, expr) { 513 | var expn = macrodefn(expr); 514 | return compile(env, expn); 515 | }); 516 | return 'undefined'; 517 | }); 518 | 519 | // Woot! We have macros! ... but we can't even write a hello world 520 | // with macros now because we don't have a proper way to construct object 521 | // literals in our language. We can use 'list' and 'table', but yuck! 522 | // we need a quasiquoter! 523 | // 524 | // {$_: {_$: } ...} 525 | // 526 | // We first write a "$_" macro that will quasi quote. We make the 527 | // unquoting mechanism generic by putting a table of unquoters for 528 | // the quasi quoter to look for, right into the environment. 529 | function AddUnquoters(base) { 530 | this.unquoters = base ? Object.create(base.unquoters) : {}; 531 | } 532 | Env = patch(Env, AddUnquoters); 533 | AddUnquoters.call(Prim, Prim.base); 534 | 535 | function quasiQuote(env, expr) { 536 | if (expr && expr.constructor === Array) { // Array literal. 537 | return '[' 538 | + map(env, quasiQuote, expr).join(',') 539 | + ']'; 540 | } 541 | 542 | if (expr && expr.constructor === Object) { // Object literal ... 543 | var unquoter = env.unquoters[operatorName(expr)]; 544 | if (unquoter) { // ... but maybe an unquoter here? 545 | return unquoter(env, expr); 546 | } else { 547 | return '{' 548 | + Object.keys(expr).map(function (k) { 549 | return JSON.stringify(k) + ':' + quasiQuote(env, expr[k]); 550 | }).join(',') 551 | + '}'; 552 | } 553 | } 554 | 555 | return JSON.stringify(expr); // else literal. 556 | } 557 | 558 | // Quasiquote operator 559 | define(Prim, '$_', function (env, expr) { 560 | return quasiQuote(env, expr.$_); 561 | }); 562 | 563 | // Now we add one unquoter '_$'. 564 | Prim.unquoters['_$'] = function (env, expr) { 565 | return compile(env, expr._$); 566 | }; 567 | 568 | // Unquote splice is simple enough as well. 569 | // Beware that it can only be used sensibly 570 | // when expanding arrays. 571 | Prim.unquoters['_$$'] = function (env, expr) { 572 | return compile_args(env, expr._$$); 573 | }; 574 | 575 | // Hooray! We can now do a macro hello world! 576 | if (enable_tests) { 577 | Prim.run({macro: "hello", 578 | lambda: ["expr"], 579 | body: [{$_: {display: {$: ["In macro!", {_$$: ["meow", "expr"]}]}}}], 580 | where: {meow: {$: "bowow"}} 581 | }, 582 | {hello: ["macro", "world!"]}); 583 | } 584 | 585 | // ## Going to town! 586 | // 587 | // Now we go to town and add all sorts of bells and whistles. 588 | 589 | // ### let:in: 590 | // First up is a variant on the "where" clause - the "let:in:". 591 | // 592 | // {let: {x: blah, y: bling}, in: expr|[expr1, expr2, ...]} 593 | define(Prim, 'let', function (env, expr) { 594 | return whereClause(env, expr, expr.let, function (envw, expr) { 595 | return compile_args(envw, expr.in); 596 | }); 597 | }); 598 | 599 | if (enable_tests) { 600 | Prim.run({let: {msg: {$: "hello"}}, 601 | in: [{display: {$_: [{_$: "msg"}, "let world"]}}]}); 602 | } 603 | 604 | // ### if:then:else: 605 | // {if: cond, then: expr1, else: expr2} 606 | define(Prim, 'if', function (env, expr) { 607 | return '(' + compile(env, expr.if) 608 | + '?' + compile(env, expr.then) 609 | + ':' + compile(env, expr.else) 610 | + ')'; 611 | }); 612 | 613 | 614 | // ### Generators 615 | // Since JS doesn't support tail call elimination, we need some 616 | // way to loop. For that, it is useful to have generators like 617 | // in python - basically functions that you can call repeatedly 618 | // to get a sequence of values. Our protocol will be that the 619 | // generator is considered to end when the function returns 620 | // 'undefined', and we can pass in a bool value of 'true' to 621 | // reset the generator. 622 | 623 | // {from: ix1, to: ix2, step: dix} 624 | // Usual defaults apply. 625 | define(Prim, 'from', function (env, body) { 626 | function iterator(comp) { 627 | return '(function (reset) {' 628 | + 'if (reset) {i = from + step; return from;}\n' 629 | + 'var result = i;' 630 | + 'return (i ' + comp + ' to ? ((i += step), result) : undefined);})'; 631 | } 632 | 633 | return '((function (from, to, step) {var i = from; ' 634 | + 'if (to === undefined) {' 635 | + 'to = from + step * 1e16;' 636 | + '}\n' 637 | + 'return (step > 0 ?' + iterator('<') + ':' + iterator('>') + ');})(' 638 | + compile(env, body.from) + ',' 639 | + (body.to ? compile(env, body.to) : 'undefined') + ',' 640 | + (body.step ? compile(env, body.step) : '1') 641 | + '))'; 642 | }); 643 | 644 | // {in: list, from: ix1, to: ix2, step: dix} 645 | // Similar to from: but steps through array. 646 | define(Prim, 'in', function (env, body) { 647 | function iterator(comp) { 648 | return '(function (reset) {' 649 | + 'if (reset) {i = from + step; return arr[from];}\n' 650 | + 'var result = arr[i];' 651 | + 'return (i ' + comp + ' to ? ((i += step), result) : undefined);})'; 652 | } 653 | 654 | return '((function (arr, from, to, step) {var i = from; ' 655 | + 'if (to === undefined) {' 656 | + 'to = (step > 0 ? arr.length : -1);' 657 | + '}\n' 658 | + 'return (step > 0 ?' + iterator('<') + ':' + iterator('>') + ');})(' 659 | + compile(env, body.in) + ',' 660 | + compile(env, body.from) + ',' 661 | + (body.to ? compile(env, body.to) : 'undefined') + ',' 662 | + (body.step ? compile(env, body.step) : '1') 663 | + '))'; 664 | }); 665 | 666 | // ### Looping using for: 667 | // 668 | // {for: {x: gen1, y: gen2,...}, 669 | // when: cond, 670 | // expr: value|[expr1, expr2, ...], 671 | // where: {...}} 672 | // {for: {x: gen1, y: gen2,...}, 673 | // when: cond, 674 | // body: stmt|[stmt1, stmt2,...], 675 | // where: {...}} 676 | // 677 | // The "expr" version produces an array with those values, whereas the "body" 678 | // and "dosync" versions are for side effects only. An extra "sync: true|false" 679 | // keyword can be specified to indicate whether only synchronous computations 680 | // are being done within - i.e. whether any closures are being created within 681 | // the body of the loop that warrants wrapping the body in a function. "sync:" 682 | // defaults to "false" so it is always safe in the default case. 683 | // 684 | // TODO: Optimize away the use of generators for the simple integer iteration 685 | // cases. 686 | // 687 | define(Prim, 'for', function (env, expr) { 688 | var numForms = (expr.expr ? 1 : 0) + (expr.body ? 1 : 0) + (expr.dosync ? 1 : 0); 689 | if (numForms !== 1) { 690 | throw new Error('for: Only one of expr: body: or dosync: can be specified.'); 691 | } 692 | 693 | return whereClause(env, expr, expr.where, function (env, expr) { 694 | var env2 = subenv(env); 695 | var envb = subenv(env2); 696 | var iters = Object.keys(expr.for); 697 | return '(function () {' 698 | + iters.map(function (ivar) { 699 | var v = newvar(env2, ivar); 700 | var gen_v = 'gen_' + v; /* Use an extra "gen_" prefix 701 | * for variables that hold 702 | * generators. 703 | */ 704 | 705 | return 'var ' + v + ', ' + gen_v + ' = (' + compile_args(env2, expr.for[ivar]) + ');'; 706 | }).join('') 707 | + (expr.expr ? 'var __result = [];' : '') 708 | // No need to wrap into a function if calculating expression. 709 | + (expr.sync ? '' : ('\nfunction __body(' 710 | + newvars(envb, iters).join(',') 711 | + ') {' 712 | + (expr.expr 713 | ? ('__result.push((' + compile_args(envb, expr.expr) + '))') 714 | : ('(' + compile_args(envb, expr.body) + ')')) 715 | + '}\n')) 716 | + iters.map(function (ivar) { 717 | var v = varname(env2, ivar); 718 | var gen_v = 'gen_' + v; 719 | return '\nfor(' + v + ' = ' + gen_v + '(true);' 720 | + v + ' !== undefined; ' 721 | + v + ' = ' + gen_v + '()) {'; 722 | }).join('') 723 | + (expr.when 724 | ? ('if (' + compile_args(env2, expr.when) + ') {') 725 | : '') 726 | + (expr.sync 727 | ? (expr.expr 728 | ? ('__result.push((' + compile_args(env2, expr.expr) + '))') 729 | : ('(' + compile_args(env2, expr.body) + ')')) 730 | : ('__body(' + varnames(env2, iters).join(',') + ');')) 731 | + (expr.when ? '}' : '') 732 | + iters.map(function (ivar) { return '\n}'; }).join('') 733 | + (expr.expr ? '\nreturn __result;' : '') 734 | + '}())'; 735 | }); 736 | }); 737 | 738 | if (enable_tests) { 739 | Prim.run({for: {x: {from: 1, to: 4}, 740 | y: {from: 100, to: 104}}, 741 | body: [{display: {$_: [{_$: "x"}, {_$: "y"}]}}]}); 742 | } 743 | 744 | // ### Let's support some math as well. 745 | // {expr: "x + y", where: {x: val1, y: val2}} 746 | // The expression can only see the variables in the where clause. 747 | // UNSAFE! 748 | define(Prim, 'expr', function (env, expr) { 749 | if (!env.options.unsafe) { 750 | throw "Unsafe expression! " + JSON.stringify(expr); 751 | } 752 | if (expr.where) { 753 | var vars = Object.keys(expr.where); 754 | return '(function (' + vars.join(',') + ') {' 755 | + 'return (' + expr.expr + ');}' 756 | + '(' 757 | + vars.map(function (v) { return compile(env, expr.where[v]); }).join(',') 758 | + '))'; 759 | } else { 760 | return '(' + expr.expr + ')'; 761 | } 762 | }); 763 | 764 | // ### Some higher order functions? 765 | 766 | // {map: fn, list: listval} 767 | define(Prim, 'map', function (env, expr) { 768 | return '(' + compile(env, expr.list) + '.map(' + compile(env, expr.map) + '))'; 769 | }); 770 | 771 | // {reduce: fn, list: listval, init: value} 772 | define(Prim, 'reduce', function (env, expr) { 773 | return '(' + compile(env, expr.list) + '.reduce(' 774 | + compile(env, expr.reduce) + ', ' 775 | + compile(env, expr.init) 776 | + '))'; 777 | }); 778 | 779 | // {filter: fn, list: listval} 780 | define(Prim, 'filter', function (env, expr) { 781 | return '(' + compile(env, expr.list) + '.filter(' + compile(env, expr.filter) + '))'; 782 | }); 783 | 784 | // ### Dot notation 785 | // It is useful to refer to object parts directly using 786 | // the dot notation. Just change lookupSymbol to directly 787 | // support it. 788 | lookupSymbol = (function (lookup) { 789 | var forbiddenProperties = {}; 790 | return function (env, sym) { 791 | var parts = sym.split('.'); 792 | if (parts.length === 1) { 793 | return lookup(env, sym); 794 | } else { 795 | parts[0] = lookup(env, parts[0]); 796 | parts.forEach(function (p,i) { 797 | if (i > 0) { 798 | if (forbiddenProperties[p]) { 799 | throw "Forbidden javascript property '" + p + "' accessed!"; 800 | } 801 | } 802 | }); 803 | if (parts[0]) { 804 | return parts.join('.'); 805 | } else { 806 | return undefined; 807 | } 808 | } 809 | }; 810 | }(lookupSymbol)); 811 | 812 | if (enable_tests) { 813 | Prim.run({let: {x: {table: {cat: {$: "meow"}}}}, 814 | in: {display: "x.cat"}}); 815 | 816 | // Try the lambda example again with dot notation access. 817 | // Lets now try a lambda hello world. 818 | Prim.run({let: {greet: {lambda: ["msg"], 819 | body: [ 820 | {display: {$: "Hello lambda world ..."}}, 821 | {display: "msg"}, 822 | {display: "keywords.lockword"} 823 | ]}}, 824 | in: [{greet: [{$: "Planet earth rocks!"}], 825 | lockword: {$: "haha!"}}]}); 826 | } 827 | 828 | // ## Defines and blocks 829 | // It will certainly be convenient to be able to write do blocks 830 | // for walking through steps and introduce definitions along the way, 831 | // process them etc. A simple macro for that would work on -- 832 | // 833 | // {do: [stmt1, stmt2, ...], 834 | // where: {...}} 835 | // 836 | // and allow define statements in the mix, like this - 837 | // 838 | // {define: {name1: value1, name2, value2,...}} 839 | // 840 | // We translate such a "do" block into a 841 | // (function () {...}()) 842 | // form. 843 | define(Prim, 'do', function (env, expr) { 844 | return whereClause(env, expr, expr.where, function (env, expr) { 845 | var result = '(function () {'; 846 | var stmts = expr.do; 847 | if (stmts.constructor !== Array) { 848 | stmts = [stmts]; 849 | } 850 | 851 | stmts.forEach(function (stmt, i) { 852 | if (stmt && operatorName(stmt) === 'define') { 853 | env = subenv(env); /* It is a define statement. Make a new environment. 854 | * This is an important step to ensure that new 855 | * definitions don't override older ones. 856 | */ 857 | 858 | Object.keys(stmt.define).forEach(function (varname) { 859 | result += 'var ' + newvar(env, varname) + ' = '; 860 | result += compile(env, stmt.define[varname]) + ';'; 861 | }); 862 | } else { 863 | result += (i+1 < expr.do.length ? '' : 'return ') 864 | + compile(env, stmt) + ';'; 865 | } 866 | }); 867 | return result + '}())'; 868 | }); 869 | }); 870 | 871 | if (enable_tests) { 872 | Prim.run({do: [ 873 | {define: {x: 5}}, 874 | {define: {fn: {lambda: ["y"], body: [{table: {x: "x", y: "y"}}]}}}, 875 | {define: {x: 10}}, 876 | {display: {fn: [10]}}, 877 | {display: "x"} 878 | ]}); 879 | } 880 | 881 | // ### Accessors 882 | // We don't have any accessor functions for working with 883 | // object and array properties yet. Let's add a general purpose 884 | // "get" and "put". 885 | 886 | // {get: [obj, key1, key2, ...]} 887 | define(Prim, 'get', function (env, expr) { 888 | return expr.get.map(function (e, i) { 889 | var ce = compile(env, e); 890 | return (i > 0 ? ('['+ce+']') : ce); 891 | }).join(''); 892 | }); 893 | 894 | // {put: [obj, key1, key2, ...], value: val} 895 | define(Prim, 'put', function (env, expr) { 896 | if (expr.put.constructor === String) { 897 | return '(' + compile(env, expr.put) + ' = ' + compile(env, expr.value) + ')'; 898 | } else if (expr.put.constructor === Array) { 899 | return '(' 900 | + lookupSymbol(env, 'get')(env, expr.put) 901 | + ' = ' 902 | + compile(env, expr.value) 903 | + ')'; 904 | } 905 | }); 906 | 907 | // ### Resolving power differences 908 | // 909 | // There is a asymmetry between lambda and macro that is uncomfortable. 910 | // It is that using a lambda always requires its arguments to be 911 | // wrapped into an array (other than keywords) whereas macros are able 912 | // to work with free forms better. Ideally, they shouldn't have differences 913 | // in form at usage time and should be able to work with all forms. 914 | // One simple solution to this is to auto-promote single non-array 915 | // arguments into one-element arrays at call time. We patch compile_apply 916 | // to resolve this. 917 | // 918 | // With this patch, you can have the following lambda - 919 | // 920 | // {let: {ruler: {lambda: ["arg"], 921 | // body: [{if: "keywords.double_rule", 922 | // then: {display: {$: "================="}} 923 | // else: {display: {$: "-----------------"}}}, 924 | // {display: "arg"}]}} 925 | // ...} 926 | // 927 | // which can be called like this - 928 | // 929 | // {ruler: {$: "An important message"}, double_rule: true} 930 | // 931 | // and "applied" like this - 932 | // 933 | // {apply: "ruler", 934 | // args: {list: [{$: "An important message"}]}, 935 | // keywords: {table: {double_rule: true}}} 936 | // 937 | var compile_apply = (function (prevCompileApply) { 938 | return function (env, op, jexpr) { 939 | var opname = operatorName(jexpr); 940 | var head = jexpr[opname]; 941 | if (head && head.constructor === Array) { 942 | return prevCompileApply(env, op, jexpr); // Safe. Old behaviour applies. 943 | } else { 944 | jexpr[opname] = [jexpr[opname]]; /* Transform the main argument into a 945 | * one-element array. 946 | * HACK: We hack this by destructively modifying 947 | * jexpr since the next time around we won't then 948 | * get into this branch. 949 | */ 950 | return prevCompileApply(env, op, jexpr); 951 | } 952 | }; 953 | }(compile_apply)); 954 | 955 | if (enable_tests) { 956 | Prim.run({let: {ruler: {lambda: ["arg"], 957 | body: [{if: "keywords.double_rule", 958 | then: {display: {$: "======================="}}, 959 | else: {display: {$: "-----------------------"}}}, 960 | {display: "arg"}]}}, 961 | in: [{ruler: {$: "An important message!"}, double_rule: true}]}); 962 | } 963 | 964 | // This uniformity lets us turn 'display' into a function much more simply! 965 | define(Prim, 'display', 'console.log'); 966 | 967 | // Can we turn map/reduce/filter into functions as well? 968 | // This looks possible, but I'm not sure about the resulting 969 | // efficiency, so I'll leave them as macros for now and leave 970 | // it to YOU to figure that out. 971 | // 972 | // define(Prim, 'map', '(function (fn) { return this.list.map(fn); })'); 973 | // define(Prim, 'reduce', '(function (fn) { return this.list.reduce(fn, this.init); })'); 974 | // define(Prim, 'filter', '(function (fn) { return this.list.filter(fn); })'); 975 | // 976 | // Many others that we've written as macros should similarly be 977 | // expressed as functions .. except for such runtime performance considerations. 978 | // The disadvantage to how we've been doing this up to here, is 979 | // that we cannot use the macros with "apply" in a program. That's 980 | // a pretty BIG disadvantage, but I'm waving my hands and saying 981 | // "you can always wrap a lambda around it" :) 982 | // 983 | // Have fun! 984 | 985 | 986 | // ## A runtime environment? 987 | // So far, we don't have the notion of a runtime and all "functions" 988 | // are actually macros and all is not well in this world just yet. 989 | // We need some way to provide an environment that exposes symbol 990 | // bindings to some piece of compiled code that we then evaluate 991 | // using eval(). 992 | // 993 | // We use a very simple model of a language runtime - which is a 994 | // function that takes in a piece of compiled code and evaluates 995 | // it using eval! The function is free to introduce new bindings 996 | // in its local environment which then become accessible to eval. 997 | // In other words, we just treat "eval" itself as a runtime. 998 | // 999 | // Here is a sample runtime that redefines "map", "reduce" 1000 | // and "filter" as functions instead of the macros that we defined 1001 | // them to be. What is returned from a call to the runtime is 1002 | // a compiled Javascript function, which when you call will result 1003 | // in the expression being evaluated. This returned function is 1004 | // of the form - 1005 | // function (param) { return something; } 1006 | // and you can pass in any object for the "param". The expression 1007 | // you supply will be able to safely access this object as the 1008 | // direct symbol "param". If you omit this argument, then accessing 1009 | // "param" in your expression will result in "undefined". 1010 | // 1011 | // Take a look at the sample function definitions. They access 1012 | // the regular arguments through the usual JS arguments and access 1013 | // the keyword argument provided through "this". 1014 | function hofRT(parentEnv, expr) { 1015 | 1016 | var defs = { 1017 | map: function (fn) { 1018 | return this.list.map(fn); 1019 | }, 1020 | 1021 | reduce: function (fn) { 1022 | return this.list.reduce(fn, this.init); 1023 | }, 1024 | 1025 | filter: function (fn) { 1026 | return this.list.filter(fn); 1027 | } 1028 | }; 1029 | 1030 | var env = subenv(parentEnv); 1031 | 1032 | Object.keys(defs).forEach(function (fn) { 1033 | define(env, fn, "__runtime__." + fn); 1034 | }); 1035 | 1036 | return eval('(function (__runtime__) { return (function (' + newvar(env, 'param') + ') { ' 1037 | + 'return (' + compile(env, expr) + ');' 1038 | + '}); })')(defs); 1039 | } 1040 | 1041 | if (enable_tests) { 1042 | console.log("Testing map function in hofRT.."); 1043 | console.log(hofRT(Prim, {map: {lambda: ["x"], body: {table: {x: "x"}}}, 1044 | list: {list: [1,2,3]}})()); 1045 | } 1046 | 1047 | // The pattern expressed in hofRT can be encapsulated as a generic thing where 1048 | // you have a "runtime maker" function to which you pass in an object 1049 | // containing the definitions you want to make visible when running the 1050 | // code and you get back a function that can run expressions with those 1051 | // definitions. In this case, we make it so that calling the returned 1052 | // runtime function with an expression does not evaluate it like eval 1053 | // does, but compiles it and returns the compiled result as a function 1054 | // that you can then call as many times as you want. 1055 | // 1056 | // So the calling sequence goes like this -- 1057 | // 1058 | // var rt = J.runtime({...definitions...}); 1059 | // var proc = rt({...jexpr...}); 1060 | // proc(param1); 1061 | // proc(param2); 1062 | // ... 1063 | // 1064 | function runtime(env, definitions) { 1065 | 1066 | var rtenv = subenv(env); // New compiler env holds the definitions. 1067 | Object.keys(definitions).forEach(function (fn) { 1068 | define(rtenv, fn, 'runtime$' + rtenv.id + '$.' + fn); 1069 | }); 1070 | 1071 | return function (expr) { 1072 | var env = subenv(rtenv); // Make a new one so that each run is independent. 1073 | 1074 | return eval('(function (__runtime__) { return (function (' + newvar(env, 'param') + ') {' 1075 | + 'var runtime$' + rtenv.id + '$ = __runtime__;' 1076 | + 'return (' + compile(env, expr) + ');' 1077 | + '}); })')(definitions); 1078 | }; 1079 | } 1080 | 1081 | // ## Standard library 1082 | 1083 | // With the above notion of runtime, we can define a "standard library" 1084 | // that implements as functions some of what we wrote above as macros. 1085 | var standardLibrary = { 1086 | display: (function () { 1087 | var map = Array.prototype.map; 1088 | var stringify = JSON.stringify; 1089 | return function () { 1090 | console.log(map.call(arguments, stringify).join('')); 1091 | }; 1092 | }()), 1093 | 1094 | from: function (fromIx) { 1095 | var step = this.step === undefined ? 1 : this.step; 1096 | var toIx = this.to === undefined ? (fromIx + step * 1e16) : this.to; 1097 | var i = fromIx; 1098 | var index = 0; 1099 | 1100 | // We support an optional "when:" field using which 1101 | // the user can supply a predicate that filters the 1102 | // stream of results. The predicate has the signature - 1103 | // value -> index -> Bool 1104 | if (this.when && this.when.constructor === Function) { 1105 | var when = this.when; 1106 | return function (reset) { 1107 | var result, resultIx; 1108 | 1109 | if (reset) { 1110 | i = fromIx; 1111 | index = 0; 1112 | } 1113 | 1114 | while (step >= 0 ? (i < toIx) : (i > toIx)) { 1115 | result = i; 1116 | resultIx = index; 1117 | i += step; 1118 | index += 1; 1119 | 1120 | // Use the function to filter the result. 1121 | if (when(result, resultIx)) { 1122 | return result; 1123 | } 1124 | } 1125 | 1126 | return undefined; // Indicates end of iteration. 1127 | }; 1128 | } else { 1129 | return function (reset) { 1130 | var result; 1131 | 1132 | if (reset) { 1133 | i = fromIx; 1134 | } 1135 | 1136 | if (step >= 0 ? (i < toIx) : (i > toIx)) { 1137 | result = i; 1138 | i += step; 1139 | return result; 1140 | } 1141 | 1142 | return undefined; // Indicates end of iteration. 1143 | }; 1144 | } 1145 | }, 1146 | 1147 | in: function (arr) { 1148 | var fromIx = this.from === undefined ? 0 : this.from; 1149 | var step = this.step === undefined ? 1 : this.step; 1150 | var toIx = this.to === undefined ? (step >= 0 ? (fromIx + arr.length) : -1) : this.to; 1151 | var i = fromIx; 1152 | 1153 | // We support an optional "when:" field using which 1154 | // the user can supply a predicate that filters the 1155 | // stream of results. The predicate has the signature - 1156 | // value -> index -> array -> Bool 1157 | if (this.when && this.when.constructor === Function) { 1158 | var when = this.when; 1159 | return function (reset) { 1160 | var resultIx; 1161 | 1162 | if (reset) { 1163 | i = fromIx; 1164 | } 1165 | 1166 | while (step >= 0 ? (i < toIx) : (i > toIx)) { 1167 | resultIx = i; 1168 | i += step; 1169 | if (when(arr[resultIx], resultIx, arr)) { 1170 | return arr[resultIx]; 1171 | } 1172 | } 1173 | 1174 | return undefined; // Indicates end of iteration. 1175 | }; 1176 | } else { 1177 | return function (reset) { 1178 | var resultIx; 1179 | 1180 | if (reset) { 1181 | i = fromIx; 1182 | } 1183 | 1184 | if (step >= 0 ? (i < toIx) : (i > toIx)) { 1185 | resultIx = i; 1186 | i += step; 1187 | return arr[resultIx]; 1188 | } else { 1189 | return undefined; // Indicates end of iteration. 1190 | } 1191 | }; 1192 | } 1193 | }, 1194 | 1195 | map: function (fn) { 1196 | return this.list.map(fn); 1197 | }, 1198 | 1199 | reduce: function (fn) { 1200 | return this.list.reduce(fn, this.init); 1201 | }, 1202 | 1203 | filter: function (fn) { 1204 | return this.list.filter(fn); 1205 | } 1206 | }; 1207 | 1208 | // ... but then we'll need some way of combining multiple 1209 | // such definitions lists into a single one before we can use 1210 | // the Env.prototype.runtime call to make a runtime. We'll also 1211 | // need to insert the standard definitions before any custom 1212 | // definitions are loaded. Let's therefore patch the runtime function 1213 | // to accept multiple definitions objects and merge them all into a 1214 | // single pile before making a runtime. 1215 | runtime = (function (runtime) { 1216 | return function (env) { 1217 | var definitions = copyValues(standardLibrary, {}); 1218 | copyValues(Array.prototype.slice.call(arguments, 1), definitions); 1219 | return runtime(env, definitions); 1220 | }; 1221 | }(runtime)) 1222 | 1223 | function copyValues(source, target) { 1224 | if (source.constructor === Array) { 1225 | source.forEach(function (d) { 1226 | copyValues(d, target); 1227 | }); 1228 | } else if (source.constructor === Function) { 1229 | source(target); /* When a function is passed, I pass it the target 1230 | * and let it deal with inserting primitives. That 1231 | * way, the function can make use of what was defined 1232 | * before it was called, such as the standardLibrary. 1233 | */ 1234 | } else if (source instanceof Object) { 1235 | Object.keys(source).forEach(function (k) { 1236 | target[k] = source[k]; 1237 | }); 1238 | } 1239 | 1240 | return target; 1241 | } 1242 | 1243 | if (enable_tests) { 1244 | console.log("Testing standardLibrary.."); 1245 | runtime(Prim)( 1246 | {let: {}, 1247 | in: [{display: {map: {lambda: ["x"], body: {table: {x: "x"}}}, 1248 | list: {list: [1,2,3]}}}, 1249 | {display: {for: {x: {from: 1, to: 10}}, expr: {table: {x: "x"}}}}]} 1250 | )(); 1251 | } 1252 | 1253 | // ## A *different* model of a runtime ## 1254 | // 1255 | // Actually, I don't quite like the above model of the runtime, because 1256 | // I can't now compile code in one place and run it in 1257 | // another place. To fix that, I need some way to indicate that 1258 | // a symbol whose value is unknown at compile time is expected to 1259 | // be resolved at runtime. That's most easily done by patching 1260 | // lookupSymbol. Note that lookupSymbol will *always* succeed now 1261 | // for syntactically valid symbols. Also, we use the "R_" prefix just 1262 | // so we don't walk all over the JS proprietary properties. 1263 | lookupSymbol = (function (oldLookupSymbol) { 1264 | var symRE = /^[a-zA-Z_\$][a-zA-Z0-9_\.\$]*$/; 1265 | return function (env, sym) { 1266 | return oldLookupSymbol(env, sym) 1267 | || (symRE.test(sym) ? ('__jexpr_runtime__.R_' + sym) : undefined); 1268 | }; 1269 | }(lookupSymbol)); 1270 | 1271 | // ... then I need to define a top level block compiler that will 1272 | // do the necessary wrapping. 1273 | function compile_to_js(env, exprArr) { 1274 | return '(function (__jexpr_runtime__, ' + newvar(env, 'param') + ') {' 1275 | + 'return (' + compile_args(env, exprArr) + ');' 1276 | + '})'; 1277 | } 1278 | 1279 | // Now the runtime building can be independent of 1280 | // the compilation environment which may no longer exist. 1281 | // Much cleaner! 1282 | function makeRuntime() { 1283 | var definitions = copyValues(standardLibrary, {}); 1284 | copyValues(Array.prototype.slice.call(arguments, 0), definitions); 1285 | var prefixed = {}; 1286 | Object.keys(definitions).forEach(function (k) { 1287 | prefixed['R_' + k] = definitions[k]; 1288 | }); 1289 | return prefixed; 1290 | } 1291 | 1292 | // Now the calling sequence is - 1293 | // 1294 | // eval(compile_to_js(env, [expr..]))(makeRuntime(...), {...params...}) 1295 | // 1296 | // Though this DRAMATICALLY alters how a runtime is defined, the 1297 | // actual definition of the runtime such as `standardLibrary` remains 1298 | // the same. 1299 | 1300 | // Undefine the definitions moved to the standardLibrary so that 1301 | // they can be overridden by user runtime definitions. 1302 | Object.keys(standardLibrary).forEach(function (key) { 1303 | define(Prim, key, undefined); 1304 | }); 1305 | 1306 | // Math functions are safe. 1307 | // FIXME: ... but actually not. Math.constructor and such stuff 1308 | // is now exposed to the language! This is actually a general 1309 | // problem with allowing the dot syntax without restrictions. 1310 | standardLibrary.Math = Math; 1311 | 1312 | // ### Operators 1313 | // 1314 | // The language is pretty bare and we don't even have basic 1315 | // addition, subtraction, boolean operations, etc. within the 1316 | // language. We need to expose some JS functionality here. 1317 | // 1318 | // `n` is the arity of the operator, which can be 2 or undefined, 1319 | // for the moment. 1320 | function defineOperator(Prim, n, opjname, opname) { 1321 | opname = opname || opjname; 1322 | define(Prim, opjname, function (env, expr) { 1323 | var argv = expr[opjname]; 1324 | console.assert((n === undefined) || (argv.length <= n)); 1325 | return '(' + argv.slice(0, n).map(function (arg) { 1326 | return '(' + compile(env, arg) + ')'; 1327 | }).join(' ' + opname + ' ') + ')'; 1328 | }); 1329 | } 1330 | 1331 | defineOperator(Prim, 2, '<'); 1332 | defineOperator(Prim, 2, '<='); 1333 | defineOperator(Prim, 2, '>'); 1334 | defineOperator(Prim, 2, '>='); 1335 | defineOperator(Prim, 2, '>>'); 1336 | defineOperator(Prim, 2, '<<'); 1337 | defineOperator(Prim, undefined, '+'); 1338 | defineOperator(Prim, undefined, '-'); 1339 | defineOperator(Prim, undefined, '*'); 1340 | defineOperator(Prim, undefined, '/'); 1341 | defineOperator(Prim, undefined, '%'); 1342 | defineOperator(Prim, 2, 'is', '==='); 1343 | defineOperator(Prim, 2, 'isnot', '!=='); 1344 | defineOperator(Prim, 2, 'isin', 'in'); 1345 | 1346 | // Short circuiting 'and' and 'or' 1347 | defineOperator(Prim, undefined, 'and', '&&'); 1348 | defineOperator(Prim, undefined, 'or', '||'); 1349 | 1350 | define(Prim, 'not', function (env, expr) { 1351 | return '(!' + compile(env, expr.not) + ')'; 1352 | }); 1353 | 1354 | // We can add infix operator support by writing a macro. 1355 | // Since such infix is usually used only in math-y code, 1356 | // we'll just call the macro `math`. 1357 | 1358 | // This array defines the precedences sequence, from the highest 1359 | // to lowest precedence. 1360 | var operatorPrecedenceSeq = ['*', '%', '/', '+', '-', '<<', '>>', 'is', 'isnot', 'isin', '<', '<=', '>', '>=', 'and', 'or']; 1361 | 1362 | var operatorRE = /^(<<|>>|<=|>=|<|>|===|==|\+|\-|\*|\/|\%|\band\b|\bor\b|\bisnot\b|\bisin\b|\bis\b)$/; 1363 | 1364 | // Given a sequence of terms, processing the operators in them is 1365 | // a simple fold over the precedence sequence. 1366 | function processInfixOperators(seq) { 1367 | if (seq.constructor === Array) { 1368 | return operatorPrecedenceSeq.reduce(processInfixOperator, seq); 1369 | } else { 1370 | return seq; 1371 | } 1372 | } 1373 | 1374 | // Rewrites the `seq` so that infix usages of the operator `op` 1375 | // are rewritten so that the operator is at its rightful head 1376 | // position. 1377 | function processInfixOperator(seq, op) { 1378 | var i, j, N, args, result = [], opexpr, e, en; 1379 | for (i = 0, N = seq.length; i < N; ++i) { 1380 | e = seq[i]; 1381 | if (i > 0 && e === op) { // Collect arguments. 1382 | args = [result.pop()]; 1383 | for (j = i + 1; j < N; j += 2) { 1384 | if (seq[j-1] === op) { 1385 | args.push(seq[j]); 1386 | } else { 1387 | break; 1388 | } 1389 | } 1390 | 1391 | opexpr = {}; 1392 | opexpr[op] = args; 1393 | result.push(opexpr); 1394 | 1395 | i = j - 2; 1396 | } else if (e.constructor === Array) { 1397 | result.push(processInfixOperators(e)[0]); 1398 | } else if (e.constructor === Object && !(en = operatorName(e)).match(operatorRE)) { 1399 | // (a - b) will get parsed as {"a": ["-", "b"]), but 1400 | // within parens, you could have an operator at head position 1401 | // as well, like (- a b), or a processed one like {"-": ["a", "b"]} 1402 | // which should both be left alone. 1403 | result.push(processInfixOperators([en].concat(e[en]))[0]); 1404 | } else { 1405 | result.push(e); 1406 | } 1407 | } 1408 | return result; 1409 | } 1410 | 1411 | // Now for the actual `math` macro that can rewrite 1412 | // 1413 | // math a + b * (d - (c % d)) 1414 | // 1415 | // into 1416 | // 1417 | // {'+': ['a', {'*': ['b', {'-': ['d', {'%': ['c', 'd']}]}]}]} 1418 | // 1419 | // Note that `processInfixOperators`, when it succeeds, will yield an 1420 | // array of one expression, which we extract using the `[0]`. 1421 | define(Prim, 'math', function (env, expr) { 1422 | return compile(env, processInfixOperators(expr.math)[0]); 1423 | }); 1424 | 1425 | // ## Limiting exposure in the exports 1426 | // 1427 | // I'd like the ability to be very very selective about what 1428 | // gets exposed in the environment so that at some point I can 1429 | // safely expose compilation environments at runtime. To do this, 1430 | // I create a "frozen wrapper" around a given environment that poses 1431 | // no extra running overhead for the internal machinery. 1432 | // 1433 | // The exposed functionality is all here. 1434 | // 1435 | // `J` is the name of the exposed variable containing this API. 1436 | function freeze(env) { 1437 | return Object.freeze({ 1438 | 1439 | // `J.subenv()` makes a new environment with J as its parent. That new 1440 | // sub-environment also gets this very same API. 1441 | subenv: function () { 1442 | return freeze(subenv(env)); 1443 | }, 1444 | 1445 | // `J.option(name, [value])` gets/sets environment options. Currently the 1446 | // only option exposed is 'unsafe' which can be set to true/false to permit 1447 | // unsafe expressions at compilation time. 1448 | option: function (optName, optVal) { 1449 | return (arguments.length === 1 1450 | ? env.options[optName] 1451 | : (env.options[optName] = optVal)); 1452 | }, 1453 | 1454 | // `J.define(name, value)` puts a symbol definition into the compilation 1455 | // environment. Defining symbols in a sub-environment does not affect 1456 | // symbol lookup in parent environments. 1457 | define: function (name, value) { 1458 | return define(env, name, value); 1459 | }, 1460 | 1461 | // `J.compile_to_js(expr,...)` Returns the compiled Javascript source for the 1462 | // given expression as a string. Evaluating this string will give you a function 1463 | // of the form -- 1464 | // 1465 | // function (runtime, param) {..} 1466 | compile_to_js: function () { 1467 | return compile_to_js(env, Array.prototype.slice.call(arguments, 0)); 1468 | }, 1469 | 1470 | // `J.compile(expr,...)` Returns the compiled closure that you can pass to `J.eval`, 1471 | // or call yourself. 1472 | compile: function () { 1473 | return eval(compile_to_js(env, Array.prototype.slice.call(arguments, 0))); 1474 | }, 1475 | 1476 | // `J.runtime(defns...)` will collect all the supplied runtime definitions 1477 | // into a single object and return it. The `standardLibrary` is included 1478 | // by default. 1479 | // 1480 | // Specifying definitions has a lot of flexibility - 1481 | // 1482 | // 1. You can give a table of name->defn mappings, 1483 | // 2. You can give a function(table) which is then 1484 | // applied to the table of already loaded definitions 1485 | // so you can add new ones that make use of older ones. 1486 | // 3. You can pass an array of such tables or functions 1487 | // and it steps through such arrays recursively. This 1488 | // helps with "componentizing" the runtime. 1489 | runtime: makeRuntime, 1490 | 1491 | // `J.eval(expr, runtime, param)` wraps it all together. You can either 1492 | // pass in a compiled expression (as a closure in the form returned by 1493 | // `J.compile(expr)` or a j-expression which will then be compiled and 1494 | // evaluated. If given a j-expression, this is equivalent to - 1495 | // 1496 | // eval(J.compile(expr))(runtime, param) 1497 | eval: function (expr, rt, param) { 1498 | rt = rt || makeRuntime(); 1499 | param = param || {}; 1500 | if (expr.constructor === Function) { 1501 | return expr(rt, param); // Already a compiled expression. 1502 | } else { 1503 | return eval(compile_to_js(env, [expr]))(rt, param); // Need to compile. 1504 | } 1505 | }, 1506 | 1507 | // `J.parse(string)` will make a parser for the given string containig 1508 | // a jsonx expression (= JSON with unquoted identifier strings allowed). 1509 | // Each call of the resultant function will parse the next JSONx object in 1510 | // the string and return it, finally returning `undefined`. 1511 | parse: jsonx, 1512 | 1513 | // `J.runPageScripts(window, runtime)` will scan the given window's document 1514 | // for script tags with `type="application/x-jexpr"` attribute set and evaluate 1515 | // all of them. 1516 | runPageScripts: function (window, runtime) { 1517 | var scripts = window.document.querySelectorAll('script[type="application/x-jexpr"]'); 1518 | var scriptText = ''; 1519 | var i, N; 1520 | for (i = 0, N = scripts.length; i < N; ++i) { 1521 | scriptText += scripts[i].text + '\n'; 1522 | } 1523 | var e = this.parse(scriptText); 1524 | var rt = this.runtime(runtime || window); 1525 | var expr; 1526 | for (expr = e(); expr; expr = e()) { 1527 | this.eval(expr, rt); 1528 | } 1529 | } 1530 | }); 1531 | } 1532 | 1533 | // for debugging. 1534 | function show(label, x) { 1535 | console.log(label + ':\t' + JSON.stringify(x)); 1536 | return x; 1537 | } 1538 | 1539 | // ## JSONx parser 1540 | 1541 | // 1542 | // This is a parser for a (highly) modified JSON (called JSONx) where unquoted identifiers 1543 | // are automatically treated as strings. Identifiers begin within alphabetic 1544 | // or underscore or dollar and can contain alphanumeric or underscore or 1545 | // dollar or period in the middle. Consecutive periods are not allowed. 1546 | // It is the basis for the J "programming language" whose AST *is* a 1547 | // JSON-serializable form, as opposed to a language whose AST is *represented* 1548 | // in JSON-serializable form. (This property enables macros in the language.) 1549 | // 1550 | // I just took Douglas Crockford's reference implementation and modified it 1551 | // to parse JSONx. (So, thanks a mil for the basic JSON parser Douglas!) 1552 | // 1553 | // This is valid JSONx - `{one.two: [buckle, 'my', "shoe"]}` - and is equivalent 1554 | // to the pure JSON `{"one.two": ["buckle", "my", {"$": "shoe"}]}`. (Yes, now 1555 | // you know I have a kid!) 1556 | // 1557 | // This bit of code doesn't follow the "stream of thought" style and it evolved 1558 | // to a point where it now has support for optional tab-syntax. Here is a summary - 1559 | // 1560 | // A "term" is of the form - 1561 | // 1562 | // ... keyword1: keyword2: ... 1563 | // 1564 | // .. which can be written like this as well - 1565 | // 1566 | // ... 1567 | // keyword1: 1568 | // keyword2: 1569 | // 1570 | // The key syntax ideas are a) line breaks begin terms or continue keyword 1571 | // parts of a term (based on indentation >= term) and b) parentheses contain 1572 | // terms. See the "cases" directory for some ad hoc examples. 1573 | // 1574 | var jsonx = (function () { 1575 | 1576 | // Error object with some info about source context. 1577 | // We keep the current source parsing state in an object 1578 | // that is passed here as `state`. 1579 | function error(desc, state) { 1580 | var e = new Error(desc); 1581 | e.name = 'JSONx_SyntaxError'; 1582 | e.text = state.text; 1583 | e.at = state.at; 1584 | e.line = state.line.slice(0); 1585 | throw e; 1586 | } 1587 | 1588 | // Copying a parsing state is useful to do some kinds 1589 | // of look ahead in a recursive descent parser. 1590 | function clone(state) { 1591 | var copy = {}; 1592 | copy.text = state.text; 1593 | copy.at = state.at; 1594 | copy.ch = state.ch; 1595 | copy.line = [column(state)]; 1596 | copy.toString = state.toString; 1597 | return copy; 1598 | } 1599 | 1600 | // State copy is also useful for look ahead. 1601 | function copy(stateFrom, stateTo) { 1602 | stateTo.text = stateFrom.text; 1603 | stateTo.at = stateFrom.at; 1604 | stateTo.ch = stateFrom.ch; 1605 | stateTo.line.pop(); 1606 | stateTo.line.push.apply(stateTo.line, stateFrom.line); 1607 | return stateTo; 1608 | } 1609 | 1610 | // Make a state for parsing the `text` starting 1611 | // from `at`. Info about the lines are kept in 1612 | // an array. The values of the array give the column 1613 | // offset processed on that line. 1614 | function mkState(text, at) { 1615 | return { 1616 | text: text, 1617 | at: at || 0, 1618 | line: [0], 1619 | toString: function () { return this.text.substr(this.at, 10); } 1620 | }; 1621 | } 1622 | 1623 | // Advance the parse state by `cols` columns. 1624 | function advance(state, cols) { 1625 | state.line[state.line.length - 1] += cols; 1626 | } 1627 | 1628 | function resetcol(state) { 1629 | // state.line[state.line.length - 1] = 0; 1630 | } 1631 | 1632 | // Gets the current column. This is stored as the 1633 | // last element of the line array. 1634 | function column(state) { 1635 | return state.line[state.line.length - 1]; 1636 | } 1637 | 1638 | // Gets next character from current parse state 1639 | // and advances the parse state. 1640 | function getChar(state) { 1641 | state.ch = state.text.charAt(state.at++); 1642 | switch (state.ch) { 1643 | case '\t': advance(state, 4); break; 1644 | case '\n': state.line.push(0); break; 1645 | default: advance(state, 1); break; 1646 | } 1647 | return state.ch; 1648 | } 1649 | 1650 | // Ungets the last getChar(). 1651 | function ungetChar(state) { 1652 | switch (state.ch) { 1653 | case '\t': advance(state, -4); break; 1654 | case '\n': state.line.pop(); break; 1655 | default: advance(state, -1); break; 1656 | } 1657 | return state.ch = state.text.charAt((--state.at) - 1); 1658 | } 1659 | 1660 | // Peeks ahead. Cheap implementation using getChar() 1661 | // followed by ungetChar(). 1662 | function look(state) { 1663 | var ch = getChar(state); 1664 | ungetChar(state); 1665 | return ch; 1666 | } 1667 | 1668 | // Returns a substring starting from the current parse state 1669 | // - i.e. the "rest of the text". 1670 | function rest(state) { 1671 | return state.text.substr(state.at); 1672 | } 1673 | 1674 | // Takes `n` characters from the current parse state and returns 1675 | // them as a string, while advancing the parse state by `n` characters. 1676 | function take(state, n) { 1677 | var i = state.at; 1678 | state.at += n; 1679 | advance(state, n); 1680 | return state.text.substr(i, n); 1681 | } 1682 | 1683 | // Tells if the text is finished. 1684 | function end(state) { 1685 | return state.at >= state.text.length; 1686 | } 1687 | 1688 | // A parser for a regular expression `exp`. 1689 | // The result of the parse, if successful, is 1690 | // mapped using the optional `mapper` function 1691 | // provided. 1692 | function re(state, exp, mapper) { 1693 | var m = rest(state).match(exp), s; 1694 | if (m) { 1695 | s = take(state, m[0].length); 1696 | return mapper ? mapper(s) : s; 1697 | } else { 1698 | return ''; 1699 | } 1700 | } 1701 | 1702 | // A parser combinator that turns a parser into a 1703 | // logging parser. Inefficient, but this is only for 1704 | // internal debugging at the moment. 1705 | function logp(p) { 1706 | return function (state) { 1707 | var s = p(state); 1708 | console.log(p.name + '[' + state.at + '] :\t\t<<' + s + '>>'); 1709 | return s; 1710 | }; 1711 | } 1712 | 1713 | // Parses one character and succeeds if the character is 1714 | // the given `ch`. 1715 | function charp(state, ch) { 1716 | if (end(state)) { 1717 | return ''; 1718 | } 1719 | var c = getChar(state); 1720 | if (ch === c) { 1721 | return ch; 1722 | } else { 1723 | ungetChar(state); 1724 | return ''; 1725 | } 1726 | } 1727 | 1728 | // Parses an integer or floating point number. 1729 | function number(state) { 1730 | return re(state, /^\-?[0-9]+(\.[0-9]+)?([eE][\-\+]?[0-9]+)?/, 1731 | function (s) { return +s; }); 1732 | } 1733 | 1734 | // Parses a 4-digit hex code for unicode chars. 1735 | function hex4(state) { 1736 | return re(state, /^[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]/, 1737 | function (s) { 1738 | return parseInt(s, 16); 1739 | }); 1740 | } 1741 | 1742 | // List of escape codes and their values. 1743 | var escapees = { 1744 | '"': '"', 1745 | "'": "'", 1746 | '\\': '\\', 1747 | '/': '/', 1748 | b: '\b', 1749 | f: '\f', 1750 | n: '\n', 1751 | r: '\r', 1752 | t: '\t' 1753 | }; 1754 | 1755 | // Parses an escapee. 1756 | function escapee(state) { 1757 | var ch = getChar(state); 1758 | if (escapees[ch]) { 1759 | return escapees[ch]; 1760 | } else { 1761 | ungetChar(state); 1762 | error('Unknown escapee <<' + ch + '>>'); 1763 | } 1764 | } 1765 | 1766 | // Pases a JSON string, but can be surrounded by 1767 | // either '"' or '\''. 1768 | function string(state, surroundedBy) { 1769 | 1770 | surroundedBy = surroundedBy || '"'; 1771 | var string = ''; 1772 | 1773 | if (charp(state, surroundedBy)) { 1774 | while (!end(state)) { 1775 | if (charp(state, surroundedBy)) { 1776 | return string; 1777 | } else if (charp(state, '\\')) { 1778 | if (charp(state, 'u')) { 1779 | string += String.fromCharCode(hex4(state)); 1780 | } else { 1781 | string += escapee(state); 1782 | } 1783 | } else { 1784 | string += getChar(state); 1785 | } 1786 | } 1787 | } 1788 | 1789 | return string; 1790 | } 1791 | 1792 | // An "identistring" is an identifier-like string, but with 1793 | // allowance for period characters in the middle. 1794 | function identistring(state) { 1795 | return re(state, /^([a-zA-Z_\$][a-zA-Z0-9_\$]*)(\.[a-zA-Z_\$][a-zA-Z0-9_\$]*)*/); 1796 | } 1797 | 1798 | // Some operators we support. 1799 | function operatorp(state) { 1800 | // <, >, <=, >=, <<, >>, +, -, *, /, % 1801 | return re(state, /^(<<|>>|<=|>=|<|>|\+|\-|\*|\/|\%)/); 1802 | } 1803 | 1804 | // Support the standard '//' comment form. 1805 | var commentRE = /^\/\/[^\n]*/; 1806 | 1807 | // Parses that skips white space and comments. 1808 | function white(state) { 1809 | while (!end(state)) { 1810 | switch (state.text.charAt(state.at)) { 1811 | case ' ': state.at++; advance(state, 1); continue; 1812 | case '\t': state.at++; advance(state, 4); continue; 1813 | case '\n': state.at++; state.line.push(0); continue; 1814 | default: 1815 | if (re(state, commentRE)) { 1816 | continue; 1817 | } else { 1818 | return state; 1819 | } 1820 | } 1821 | } 1822 | return state; 1823 | } 1824 | 1825 | // Parses some known value key words of the JS language. 1826 | function word(state) { 1827 | return re(state, /^(true|false|null)\b/, 1828 | function (s) { 1829 | switch (s) { 1830 | case 'true': return true; 1831 | case 'false': return false; 1832 | case 'null': return null; 1833 | } 1834 | }); 1835 | } 1836 | 1837 | // Parses the array syntax. Note that the array contents 1838 | // are full JSONx "terms" and can therefore be expressions 1839 | // that are compiled instead of just values. 1840 | function array(state) { 1841 | var array = []; 1842 | if (charp(state, '[')) { 1843 | white(state); 1844 | if (charp(state, ']')) { 1845 | return array; 1846 | } 1847 | 1848 | while (!end(state)) { 1849 | resetcol(state); 1850 | array.push(term(state)); 1851 | white(state); 1852 | if (charp(state, ']')) { 1853 | return array; 1854 | } else if (charp(state, ',')) { 1855 | white(state); 1856 | } else { 1857 | error('Expecting ] or ,', state); 1858 | } 1859 | } 1860 | } 1861 | 1862 | error('Bad array', state); 1863 | } 1864 | 1865 | // Parses the object syntax. Object values can also be 1866 | // JSONx "terms". Support object keys with any string 1867 | // representation. 1868 | function object(state) { 1869 | var object = {}, key; 1870 | if (charp(state, '{')) { 1871 | white(state); 1872 | if (charp(state, '}')) { 1873 | return object; 1874 | } 1875 | 1876 | while (!end(state)) { 1877 | key = string(state, '"') || string(state, "'") || identistring(state); 1878 | white(state); 1879 | if (charp(state, ':')) { 1880 | if (Object.hasOwnProperty.call(object, key)) { 1881 | error('Duplicate key "' + key + '"', state); 1882 | } 1883 | resetcol(state); 1884 | object[key] = term(state); 1885 | white(state); 1886 | if (charp(state, '}')) { 1887 | return object; 1888 | } else if (charp(state, ',')) { 1889 | white(state); 1890 | } else { 1891 | error('Expecting } or ,', state); 1892 | } 1893 | } else { 1894 | error('Expecting :', state); 1895 | } 1896 | } 1897 | error('Bad object', state); 1898 | } else { 1899 | return ''; 1900 | } 1901 | } 1902 | 1903 | // A "term" is of one of the following forms - 1904 | // 1905 | // 1906 | // "string" 1907 | // 'multi-part identifier' 1908 | // identifier 1909 | // a.nested.reference 1910 | // [a,b,c] 1911 | // {"key":"val",...} 1912 | // (term) 1913 | // true|false|null 1914 | // head arg1 arg2 arg3 ... argN kw1: kwa1 kwa2 ... kwaI kw2: kwb1 kwb2 ... kwbJ ... 1915 | // -- The arg1 arg2 .. are not head terms whereas the keyword arguments are 1916 | // -- head terms. This means "head: arg1 arg2 kw1: v1 v2" translates to 1917 | // -- {head: [arg1, arg2], kw1: {v1: v2}} 1918 | // -- if v1 can be a head - i.e. is an identifier. 1919 | function term(state, head, kwterm) { 1920 | var ch, s, t, cs, cs1, cs2, a, t2, kw; 1921 | white(state); 1922 | if (head && column(state) <= column(head)) { 1923 | /* Indentation gone out of scope. */ 1924 | return undefined; 1925 | } 1926 | if (end(state)) { 1927 | return undefined; 1928 | } 1929 | ch = look(state); 1930 | switch (ch) { 1931 | case '{': return object(state); 1932 | case '[': return array(state); 1933 | case '"': return {$: string(state, '"')}; 1934 | case '\'': return string(state, '\''); 1935 | case '(': 1936 | cs = clone(state); 1937 | if (charp(cs, '(') && (resetcol(cs), (t = term(cs)) !== undefined)) { 1938 | if (white(cs), charp(cs, ')')) { 1939 | copy(cs, state); 1940 | return t; 1941 | } else { 1942 | /* Perhaps more arguments? If so, turn it into 1943 | a plain array for later processing, say, using 'math'. */ 1944 | t2 = argv(cs, state); 1945 | 1946 | /* process keywords. */ 1947 | kw = {}; 1948 | while (optKeywordPart(cs, kw, state)) {}; 1949 | 1950 | if (white(cs), charp(cs, ')')) { 1951 | t2.unshift(t); 1952 | t = t2; 1953 | t.keywords = kw; 1954 | } else { 1955 | error('Expecting ) in term', cs); 1956 | } 1957 | 1958 | copy(cs, state); 1959 | return t; 1960 | } 1961 | } else { 1962 | error('Bad term', state); 1963 | } 1964 | case ')': 1965 | case ',': 1966 | case '}': 1967 | case ']': 1968 | return undefined; 1969 | default: 1970 | if (ch === '-') { 1971 | s = number(state); 1972 | if (s) { return s; } 1973 | } 1974 | 1975 | if (ch >= '0' && ch <= '9') { 1976 | return number(state); 1977 | } 1978 | 1979 | cs = clone(state); 1980 | if (word(cs) !== '') { 1981 | return word(state); 1982 | } 1983 | 1984 | // A full term. Not a word. 1985 | cs = clone(state); 1986 | if (s = (identistring(cs) || operatorp(cs))) { 1987 | cs2 = clone(cs); 1988 | if (!charp(cs2, ':')) { 1989 | if (!head || state.line.length > 1 || kwterm) { 1990 | // This is a head term and we need to collect argv and keywords 1991 | // if there is no prior head, a line break has occurred after 1992 | // the previous term or this is a keyword term. 1993 | t = {}; 1994 | try { 1995 | t[s] = argv(cs2, state); 1996 | } catch (e) { 1997 | // Only argv throws error. optKeywordPart doesn't. 1998 | // A single term without argv is a value. (Going pure functional syntax here!) 1999 | t = s; 2000 | } 2001 | // process keywords. 2002 | while (optKeywordPart(cs2, t, state)) {}; 2003 | copy(cs2, state); 2004 | return t; 2005 | } else { 2006 | copy(cs2, state); 2007 | return s; 2008 | } 2009 | } else { 2010 | return undefined; 2011 | } 2012 | } else { 2013 | return undefined; 2014 | } 2015 | } 2016 | } 2017 | 2018 | // Parse the arguments of a term's head. The result is an array. 2019 | function argv(state, head, kwterm) { 2020 | var result = []; 2021 | var cs = clone(state); 2022 | var t; 2023 | while ((t = term(cs, head, kwterm)) !== undefined) { 2024 | result.push(t); 2025 | } 2026 | 2027 | if (result.length === 0) { 2028 | error('Term expected', state); 2029 | } else { 2030 | copy(cs, state); 2031 | return result.length === 1 ? result[0] : result; 2032 | } 2033 | } 2034 | 2035 | // Parse the optional keywords of a term. 2036 | function optKeywordPart(state, t, head) { 2037 | var cs = clone(state); 2038 | white(cs); 2039 | if (head && column(cs) < column(head)) { 2040 | // This keyword no longer applies to the given head 2041 | // due to the indentation going back to a shallower nesting. 2042 | return undefined; 2043 | } 2044 | var cs2 = clone(cs); 2045 | var kw = optKeyword(cs2), a; 2046 | if (kw !== undefined) { 2047 | a = argv(cs2, head, true); 2048 | t[kw] = (a.length === 1) ? a[0] : a; 2049 | copy(cs2, state); 2050 | return t; 2051 | } else { 2052 | return undefined; 2053 | } 2054 | } 2055 | 2056 | // Parse one keyword. 2057 | function optKeyword(state) { 2058 | var stateC = clone(state); 2059 | var str = identistring(stateC); 2060 | if (str && charp(stateC, ':')) { 2061 | copy(stateC, state); 2062 | return str; 2063 | } else { 2064 | return undefined; 2065 | } 2066 | } 2067 | 2068 | // Finally, the full parser. You pass in a text to be parsed 2069 | // as JSONx and you get a function back. Every time you call 2070 | // this function, you'll get the next term in the text, until 2071 | // there are no more, in which case the return value will be 2072 | // 'undefined'. 2073 | return function (text) { 2074 | var state = mkState(text, 0); 2075 | // You can call the returned function several 2076 | // times until you get a null. Multiple 2077 | // JSONx expressions in the string will be 2078 | // parsed and returned in sequence. 2079 | return function () { 2080 | white(state); 2081 | return end(state) ? undefined : term(state); 2082 | }; 2083 | }; 2084 | 2085 | }()); 2086 | 2087 | return freeze(Prim); 2088 | 2089 | }(function () { 2090 | // Check whether J_enable_tests has been set. If it is set, 2091 | // some stupid examples in the "stream of thought" code will be 2092 | // evaluated and the results will be printed out. 2093 | try { 2094 | return global.J_enable_tests || false; 2095 | } catch (e) { 2096 | } 2097 | 2098 | try { 2099 | return window.J_enable_tests || false; 2100 | } catch (e) { 2101 | } 2102 | 2103 | return false; 2104 | }())); 2105 | 2106 | // Are we in node.js? If so set the exports variable. 2107 | // Otherwise just shut up and return. 2108 | try { 2109 | module.exports = J; 2110 | } catch (e) { 2111 | } 2112 | 2113 | // Are we in a browser? If so execute any script tags 2114 | // with `type="application/x-jexpr"`. Permit the scripts 2115 | // access to the enumerable properties of the `window` 2116 | // object. 2117 | try { 2118 | window.document.querySelectorAll; 2119 | window.console = window.console; // Also give access to the console. 2120 | J.subenv().runPageScripts(window, window); // subenv() 'cos we don't want residues. 2121 | } catch (e) { 2122 | } 2123 | --------------------------------------------------------------------------------