├── Examples ├── ex0_hello_world.js ├── ex1_objs_with_functions.js ├── ex2_math_functions_with_std_lib.js ├── ex3_math_functions_without_lib.js ├── ex4_trusting_user_defined_libs.js ├── ex5_readme_examples.js ├── ex6_bounded_loops.js └── ex7_null_bool_undefined.js ├── LICENSE ├── LJSON.js ├── README.md ├── package.json └── parsenhora.js /Examples/ex0_hello_world.js: -------------------------------------------------------------------------------- 1 | var LJSON = require("./../LJSON.js"); 2 | 3 | // Defines a JS function as usual 4 | var helloWorld = function(x){ 5 | return "Hello, world!"; 6 | }; 7 | 8 | // Serializes the function as a string with `stringify` 9 | var helloWorldStr = LJSON.stringify(helloWorld); 10 | 11 | // Gets the value back using `parse` 12 | var helloWorldVal = LJSON.parse(helloWorldStr); 13 | 14 | // Test it: 15 | console.log(helloWorldVal(0)); 16 | 17 | 18 | -------------------------------------------------------------------------------- /Examples/ex1_objs_with_functions.js: -------------------------------------------------------------------------------- 1 | var LJSON = require("./../LJSON.js"); 2 | 3 | // Suppose you have the following JSON object: 4 | 5 | var player = { 6 | name : "John", 7 | atk : 17, 8 | def : 18, 9 | letter : function (msg){ 10 | return { 11 | name : "john", 12 | message : msg}; 13 | } 14 | }; 15 | 16 | // What happens if you use JSON.stringify on it? 17 | 18 | var playerStr = JSON.stringify(player); 19 | var playerVal = JSON.parse(playerStr); 20 | // console.log(playerVal.letter("Hello.")) 21 | 22 | // This would be an error, because JSON doesn't define functions, so the field 23 | // `letter` is stripped away from `playerVal`. In other words, the parsed value 24 | // doesn't behave the same as the stringified value. But since `letter` is a 25 | // pure function, you can use LJSON on it: 26 | 27 | var playerStr = LJSON.stringify(player); 28 | var playerVal = LJSON.parse(playerStr); 29 | console.log(playerVal.letter("Hello.")); 30 | 31 | // That outputs: { name: 'john', message: 'Hello.' } 32 | // As expected. 33 | -------------------------------------------------------------------------------- /Examples/ex2_math_functions_with_std_lib.js: -------------------------------------------------------------------------------- 1 | var LJSON = require("./../LJSON.js"); 2 | 3 | // LJSON doesn't define any primitive - just functions and function application. 4 | // That means you can't do anything intersting with JS numbers, arrays, etc. - 5 | // for that, you need to manually provide the primitivies yourself. Since that 6 | // would be way too cumbersome and repetitive, there is a convenient helper that 7 | // enables the common primitives such as addition, multiplication, array access 8 | // etc. after you parse a function. It works as follows: 9 | 10 | // First, you define your functions with an extra argument, "$". 11 | // You can use `$` inside the function to access the common primitives. 12 | var square = function($,x){ 13 | return $("*",x,x); 14 | }; 15 | 16 | // You stringify your function as usual: 17 | var squareStr = LJSON.stringify(square); 18 | 19 | // Now, instead of using `LJSON.parse`, you use `LJSON.parseWithLib`: 20 | var squareVal = LJSON.parseWithStdLib(squareStr); 21 | 22 | console.log(squareVal(4)); // outputs 16, as expected 23 | -------------------------------------------------------------------------------- /Examples/ex3_math_functions_without_lib.js: -------------------------------------------------------------------------------- 1 | // That is just a note to say that even without primitives to deal with JS values, you 2 | // can still do a lot in LJSON alone. Indeed, any computable function is expressible 3 | // in LJSON. For example, with some λ-fu, we can define `fibonacci` with church numbers: 4 | 5 | var LJSON = require("./../LJSON.js"); 6 | 7 | fib = LJSON.parse("(v0)=>(v0((v1)=>(v1((v2)=>((v3)=>((v4)=>(v4(v3)((v5)=>((v6)=>(v2(v5)(v3(v5)(v6)))))))))))((v7)=>(v7((v8)=>((v9)=>(v9)))((v10)=>((v11)=>(v10(v11))))))((v12)=>((v13)=>(v12))))") 8 | seven = LJSON.parse("(f)=>((x)=>(f(f(f(f(f(f(f(x)))))))))"); 9 | logChu = (lam) => console.log(lam ((a)=>a+1) (0)) // Prints a church number on JavaScript. 10 | 11 | // Prints 13, because `fib(7) == 13`. 12 | logChu(fib(seven)); 13 | 14 | // Of course, you'd not use that over the std lib - but that is to say the power is there. 15 | -------------------------------------------------------------------------------- /Examples/ex4_trusting_user_defined_libs.js: -------------------------------------------------------------------------------- 1 | var LJSON = require("./../LJSON.js"); 2 | 3 | // Warning: this is a very complex example. 4 | 5 | // Suppose you want your users to define an array library for you, but you 6 | // don't trust them. As a good programmer, you know that using `foldr` and 7 | // `cons` as primitives, one is able to encode any array-manipulating 8 | // algorithm. So, you enable those primitives and nothing else. 9 | 10 | // A creative user, then, writes this function that, given the promised 11 | // primitives, returns 3 new functions: `foldl`, `reverse` and `concat`. 12 | 13 | function userLib(foldr,cons){ 14 | function id(x){ 15 | return x; 16 | }; 17 | function foldl(snoc,nil,array){ 18 | function cons(head,tail){ 19 | return function(result){ 20 | return tail(snoc(result,head)); 21 | }; 22 | }; 23 | return foldr(cons, id, array)(nil); 24 | }; 25 | function reverse(array){ 26 | return foldl(function(head,tail){return cons(tail,head)}, [], array); 27 | }; 28 | function concat(a,b){ 29 | return foldr(cons,foldr(cons,[],b),a); 30 | }; 31 | return { 32 | foldl : foldl, 33 | reverse : reverse, 34 | concat : concat}; 35 | }; 36 | 37 | // That user stringifies his library and sends it to you via sockets: 38 | var userLibStr = LJSON.stringify(userLib); 39 | //console.log(userLibStr); 40 | 41 | // For the curious, this is the stringification of `userLibStr`: 42 | // (function(v0, v1) { 43 | // return { 44 | // foldl: (function(v2, v3, v4) { 45 | // return v0((function(v5, v6) { 46 | // return (function(v7) { 47 | // return v6(v2(v7, v5)) 48 | // }) 49 | // }), (function(v8) { 50 | // return v8 51 | // }), v4)(v3) 52 | // }), 53 | // reverse: (function(v9) { 54 | // return v0((function(v10, v11) { 55 | // return (function(v12) { 56 | // return v11(v1(v10, v12)) 57 | // }) 58 | // }), (function(v13) { 59 | // return v13 60 | // }), v9)([]) 61 | // }), 62 | // concat: (function(v13, v15) { 63 | // return v0(v1, v0(v1, [], v15), v14) 64 | // }) 65 | // } 66 | // }) 67 | 68 | // You recover his original code by parsing that string: 69 | var userLib = LJSON.unsafeParse(userLibStr); 70 | 71 | // Now, to get the actual lib, you need to give it the promised primitives: 72 | function foldr(cons, nil, array){ 73 | var result = nil; 74 | for (var i=array.length-1; i>=0; --i) 75 | result = cons(array[i],result); 76 | return result; 77 | }; 78 | function cons(head, tail){ 79 | return [head].concat(tail); 80 | }; 81 | var lib = userLib(foldr, cons); 82 | 83 | // You can then use his code safely, knowing he won't steal your password or 84 | // mine bitcoins, because you gave him no power other than array manipulation. 85 | // On this particular case, you can also be sure his programs will halt. 86 | console.log(lib.reverse([1,2,3])); 87 | console.log(lib.concat([1,2,3],[4,5,6])); 88 | -------------------------------------------------------------------------------- /Examples/ex5_readme_examples.js: -------------------------------------------------------------------------------- 1 | var LJSON = require("./../LJSON.js"); 2 | 3 | // First example 4 | 5 | // A random JS object with a pure function inside. 6 | var person = { 7 | name : "John", 8 | mail : function(msg){ return { author : "John", message : msg}; } 9 | }; 10 | 11 | // If JSON was used, the `mail` field would be stripped from `personVal`. 12 | var personStr = LJSON.stringify(person); 13 | var personVal = LJSON.parse(personStr); 14 | var mail = personVal.mail("hello"); // would crash with JSON 15 | 16 | // But, since `mail` is pure, LJSON can deal with it correctly: 17 | console.log("Serialized value : " + personStr); 18 | console.log("Calling mail : " + LJSON.stringify(mail)); 19 | 20 | 21 | // Hypotenuse example 22 | 23 | hypotenuse = function($,a,b){ 24 | return $("sqrt",$("+",$("*",a,a),$("*",b,b))); 25 | }; 26 | var hypotenuseStr = LJSON.stringify(hypotenuse); 27 | var hypotenuseVal = LJSON.parseWithStdLib(hypotenuseStr); 28 | console.log(hypotenuseVal(3,4)); 29 | 30 | 31 | // Crazy example 32 | 33 | console.log(LJSON.stringify(function(a){ 34 | // Things known at compile time are evaluated. 35 | var arr = []; 36 | for (var i=0; i<10; ++i) 37 | arr.push(i*10); 38 | 39 | // Even inside functions. 40 | var foo = function(x){ 41 | if (arr[5] < 10) 42 | var bird = "pidgey"; 43 | else 44 | var bird = "pidgeott"; 45 | return bird; 46 | }; 47 | 48 | // Even λ-calculus expressions! 49 | var C3 = function(f){return function(x){return f(f(f(x)))}}; 50 | var C27 = C3(C3); // church number exponentiation of 3^3 51 | 52 | return [ 53 | arr, 54 | foo, 55 | C27]; 56 | })); 57 | -------------------------------------------------------------------------------- /Examples/ex6_bounded_loops.js: -------------------------------------------------------------------------------- 1 | var LJSON = require("./../LJSON.js"); 2 | 3 | // Lets create an environment with only 2 functions, 4 | // inc (adds 1 to a number) 5 | // loop (bounded for-loop with max depth of 1000) 6 | var lib = { 7 | inc : function(a){ return a + 1; }, 8 | loop : function(limit,state,update){ 9 | limit = Math.min(limit, 1000); // No more than 1000 loops 10 | for (var i=0; i String 7 | // Stringifies a LJSON value. 8 | function stringify(value){ 9 | var nextVarId = 0; 10 | return (function normalize(value){ 11 | // This is responsible for collecting the argument list of a bound 12 | // variable. For example, in `function(x){return x(a,b)(b)(c)}`, it 13 | // collects `(a,b)`, `(b)`, `(c)` as the arguments of `x`. For 14 | // that, it creates a variadic argumented function that is applied 15 | // to many arguments, collecting them in a closure, until it is 16 | // applied to `null`. When it is, it returns the JS source string 17 | // for the application of the collected argument list. 18 | function application(varName, argList){ 19 | var app = function(arg){ 20 | if (arg === null) { 21 | function stringifyCall(args){ 22 | return "("+args.join(",")+")" 23 | } 24 | return varName + (argList.length===0 ? "" : argList.map(stringifyCall).join("")); 25 | } else { 26 | var args = [].slice.call(arguments,0); 27 | var newArgList = argList.concat([args.map(normalize)]); 28 | return application(varName, newArgList); 29 | }; 30 | }; 31 | app.isApplication = true; 32 | return app; 33 | }; 34 | // For unit types, we just return. 35 | if (value === undefined || value === null){ 36 | return value; 37 | } 38 | // For unit types, we just delegate to JSON.stringify. 39 | else if (typeof value === "string" 40 | || typeof value === "number" 41 | || typeof value === "boolean" 42 | ) { 43 | return JSON.stringify(value); 44 | } 45 | // If we try to normalize an application, we apply 46 | // it to `null` to stop the argument-collecting. 47 | else if (value.isApplication) { 48 | return value(null); 49 | } 50 | // If it is a function, we need to create an application for its 51 | // variable, and call the function on it, so its variable can start 52 | // collecting the argList for the places where it is called on the 53 | // body. We then normalize the resulting body and return the JS source 54 | // for the function. 55 | else if (typeof value === "function") { 56 | var argNames = []; 57 | var argApps = []; 58 | for (var i=0, l=value.length; i("+body+")"; 66 | } 67 | // For container types (objects and arrays), it is just a matter 68 | // of calling stringify on the contained values recursively. 69 | else if (typeof value === "object") { 70 | if (value instanceof Array){ 71 | var source = "["; 72 | for (var i=0, l=value.length; i LJSON 89 | // Parses a LJSON String into a LJSON value. 90 | // The value returned is guaranteed to be safe - i.e., all functions 91 | // inside it are pure and have no access to the global scope. 92 | // TODO : tests, add more comments. 93 | function parse(str){ 94 | // Note: Unfortunatelly, JavaScript doesn't provide any way to create 95 | // functions dynamically other than using `eval` on strings, so LJSON's 96 | // parser can't return JS values directly and needs to instead produce 97 | // a string to use `eval` on. This is perfectly safe as LJSON's 98 | // functions are pure and any unbound variable (i.e, reference to the 99 | // global scope) is detected here as parse error - yet, it could be a 100 | // bit faster if ECMAs provided a better way to build functions. 101 | return eval("("+parsenhora(function(P){ 102 | // insert :: forall a . Object -> String -> a -> Object 103 | // Inserts a keyVal pair into an object (purely). 104 | function insert(newKey,newVal,object){ 105 | var result = {}; 106 | for (var key in object) 107 | result[key] = object[key]; 108 | result[newKey] = newVal; 109 | return result; 110 | }; 111 | 112 | // A LJSON value is one of those: 113 | // - a JSON bool (ex: true); 114 | // - a JSON number (ex: `7.5e10`); 115 | // - a JSON string (ex: `"sdfak"`); 116 | // - a JSON array (ex: `[1,2,"aff"]`); 117 | // - a JSON object (ex: `{"a":1, "b":"ghost"}`); 118 | // - the JSON null (ex: null); 119 | // - a LJSON undefined (ex: undefined); 120 | // - a LJSON function following the grammar: 121 | // (var0, var1, varN) => body 122 | // - a LJSON variable following the grammar: 123 | // variable(arg0a,arg1a,argNa)(arg0b,arg1b,argNb)... 124 | function LJSON_value(binders,scope){ 125 | return function(){ 126 | return P.choice([ 127 | LJSON_boolean(binders,scope), 128 | LJSON_number(binders,scope), 129 | LJSON_string(binders,scope), 130 | LJSON_array(binders,scope), 131 | LJSON_object(binders,scope), 132 | LJSON_null(binders,scope), 133 | LJSON_undefined(binders,scope), 134 | LJSON_application(binders,scope), 135 | LJSON_lambda(binders,scope)])(); 136 | }; 137 | }; 138 | function LJSON_boolean(binders,scope){ 139 | return P.choice([P.string("true"),P.string("false")]); 140 | }; 141 | function LJSON_undefined(binders,scope){ 142 | return P.string("undefined"); 143 | }; 144 | function LJSON_null(binders,scope){ 145 | return P.string("null"); 146 | }; 147 | function LJSON_number(binders,scope){ 148 | return function(){ 149 | // Parses a LJSON Number, following 150 | // the same grammar as JSON. 151 | var result = ""; 152 | 153 | // Optional leading minus signal. 154 | var minus = P.chr("-")(); 155 | if (minus !== null) 156 | result += "-"; 157 | 158 | // First character must be 0~9. 159 | var leadingDigit = P.digit(); 160 | if (leadingDigit === null) 161 | return null; 162 | result = result + leadingDigit; 163 | 164 | // If the leading digit isn't 0, 165 | // the number could have more digits. 166 | if (leadingDigit !== "0") 167 | result += P.many(P.digit)().join(""); 168 | 169 | // Optionally, we can have a dot 170 | // followed by more digits. 171 | var dot = P.chr(".")(); 172 | if (dot !== null) 173 | result = result + "." + P.many(P.digit)().join(""); 174 | 175 | // Optionally, we can have the exp letter 176 | // followed by an optional sign and more 177 | // digits, for scientific notation. 178 | var e = P.choice([P.chr("e"),P.chr("E")])(); 179 | if (e !== null){ 180 | result += "e"; 181 | if (P.chr("+")() !== null) 182 | result += "+"; 183 | else if (P.chr("-")() !== null) 184 | result += "-"; 185 | result += P.many(P.digit)().join(""); 186 | }; 187 | 188 | // All that satisfied, that is a valid 189 | // JSON and, thus, LJSON Number, and can 190 | // be parsed to a JavaScript Number. 191 | return result; 192 | }; 193 | }; 194 | function LJSON_string(binders,scope){ 195 | return function(){ 196 | function isStringCharacter(c){ 197 | return c !== '"' && c !== '\\'; 198 | }; 199 | 200 | var openQuote = P.chr('"')(); 201 | if (openQuote === null) 202 | return null; 203 | 204 | for (var result = "", c = P.get(); c !== null && c !== '"'; c = P.get()){ 205 | if (isStringCharacter(c)){ 206 | result += c; 207 | } 208 | else if (c === "\\"){ 209 | var slashed = P.get(); 210 | switch (slashed){ 211 | case '"' : result += '\\"'; break; 212 | case "\\": result += "\\"; break; 213 | case "/" : result += "\\/"; break; 214 | case "b" : result += "\\b"; break; 215 | case "f" : result += "\\f"; break; 216 | case "n" : result += "\\n"; break; 217 | case "r" : result += "\\r"; break; 218 | case "t" : result += "\\t"; break; 219 | case "u" : 220 | var hexs = P.sequence([hex,hex,hex,hex])(); 221 | if (hexs === null) 222 | return null; 223 | result += "\\"+hexs.join(""); 224 | break; 225 | default: return null; 226 | }; 227 | }; 228 | }; 229 | return '"'+result+'"'; 230 | }; 231 | }; 232 | function LJSON_array(binders,scope){ 233 | return function(){ 234 | var result = P.betweenSpaced( 235 | P.chr("["), 236 | P.intercalatedSpaced( 237 | LJSON_value(binders,scope), 238 | P.chr(",")), 239 | P.chr("]"))(); 240 | if (result === null) 241 | return null; 242 | return "["+result.join(",")+"]"; 243 | }; 244 | }; 245 | function LJSON_object(binders,scope){ 246 | return function(){ 247 | function keyVals(){ 248 | var keyVals = P.intercalatedSpaced( 249 | P.pairSpaced( 250 | LJSON_string(binders,scope), 251 | P.chr(":"), 252 | LJSON_value (binders,scope)), 253 | P.chr(","))(); 254 | 255 | if (keyVals === null) 256 | return null; 257 | 258 | var result = "{"; 259 | for (var i=0, l=keyVals.length; i0?",":"") + [keyVals[i][0]] + ":" + keyVals[i][1]; 261 | result += "}"; 262 | 263 | return result; 264 | }; 265 | 266 | return P.betweenSpaced(P.chr("{"),keyVals,P.chr("}"))(); 267 | }; 268 | }; 269 | function LJSON_lambda(binders,scope){ 270 | return function(){ 271 | var varList = P.betweenSpaced( 272 | P.chr("("), 273 | P.intercalatedSpaced(P.word,P.chr(",")), 274 | P.chr(")"))(); 275 | 276 | if (varList === null) 277 | return null; 278 | 279 | var arrow = P.sequence([ 280 | P.skipSpaces, 281 | P.string("=>"), 282 | P.skipSpaces])(); 283 | 284 | if (arrow === null) 285 | return null; 286 | 287 | var newScope = {}; 288 | for (var key in scope) 289 | newScope[key] = scope[key]; 290 | for (var i=0, l=varList.length; i LJSON 331 | // Parses an arbitrary String into a LJSON value. 332 | // This function is faster than `parse`, but unsafe - the code you call 333 | // this on will be executed and could do every kind of harm. 334 | function unsafeParse(a){ 335 | return eval(a); 336 | }; 337 | 338 | // withLib :: Function -> Function 339 | // LJSON defines no primitives, so you can't do anything with JS values 340 | // from inside LJSON functions. For example, you are able to receive 341 | // numbers as arguments of, but not sum them. In order to do that, you need 342 | // to manually give the LJSON function the primitives it needs to operate. 343 | // That is cumbersome and repetitive, so `withLib` is just a convenient 344 | // utility to enable a LJSON function to access your own set of primitives. 345 | // It works by reserving the first argument as an accessor to your library: 346 | // Example: 347 | // 348 | // // Your own lib defining the multiplication operation, "*": 349 | // var myLib = {"*" : function(a,b){ return a*b; }}; 350 | // 351 | // // Doubles a JS number using your lib's "*": 352 | // function double(L,a){ 353 | // return L("*",a,2); 354 | // }; 355 | // var doubleStr = LJSON.stringify(double); // stringifies to send/store 356 | // var doubleVal = withLib(myLib,LJSON.parse(double)); // parses into a JS value and enables your lib 357 | // 358 | // console.log(double(4)); // output: 8 359 | function withLib(lib,fn){ 360 | return function(){ 361 | var args = [].slice.call(arguments,0); 362 | var call = function(primName){ 363 | var args = [].slice.call(arguments,1); 364 | return lib[primName].apply(null,args); 365 | }; 366 | return fn.apply(null, [call].concat(args)); 367 | }; 368 | }; 369 | 370 | // withStrLib :: Function -> Function 371 | // A standard lib with the set of common JavaScript functions. 372 | // TODO: complete this. 373 | function withStdLib(fn){ 374 | return withLib({ 375 | "+" : function(a,b){return a+b}, 376 | "-" : function(a,b){return a-b}, 377 | "*" : function(a,b){return a*b}, 378 | "/" : function(a,b){return a/b}, 379 | "sqrt" : function(x){return Math.sqrt(x)} 380 | },fn); 381 | }; 382 | 383 | // parseWithLib :: Object of functions -> String -> Function 384 | // Convenient function to parse and use `withLib`. 385 | function parseWithLib(lib,src){ 386 | return withLib(lib,parse(src)); 387 | }; 388 | 389 | // parseWithLib :: String -> Function 390 | // Convenient function to parse and use `withStdLib`. 391 | function parseWithStdLib(src){ 392 | return withStdLib(parse(src)); 393 | }; 394 | 395 | // toName :: Number -> String 396 | // Turns a number into a var name (a, b, c... aa, ab...). 397 | function toName(nat){ 398 | var alphabet = "abcdefghijklmnopqrstuvwxyz"; 399 | var name = ""; 400 | do { 401 | name += alphabet[nat % alphabet.length]; 402 | nat = Math.floor(nat / alphabet.length); 403 | } while (nat > 0); 404 | return name; 405 | }; 406 | 407 | return { 408 | stringify : stringify, 409 | parse : parse, 410 | unsafeParse : unsafeParse, 411 | withLib : withLib, 412 | withStdLib : withStdLib, 413 | parseWithLib : parseWithLib, 414 | parseWithStdLib : parseWithStdLib}; 415 | })(); 416 | if (typeof module !== "undefined") module.exports = LJSON; 417 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## LJSON 2 | 3 | LJSON is a drop-in replacement for [JSON](http://www.json.org) which also allows you to parse and stringify pure functions and their contents. There are good security reasons for functions to be out of the JSON specs, but most of those are only significant when you allow arbitrary, side-effective programs. With pure functions, one is able to interchange code while still being as safe as with regular JSON. 4 | 5 | ```JavaScript 6 | var LJSON = require("./LJSON.js"); 7 | 8 | // A random JS object with a pure function inside. 9 | var person = { 10 | name : "John", 11 | mail : function(msg){ return { author : "John", message : msg}; } 12 | }; 13 | 14 | // If JSON was used, the `mail` field would be stripped from `personVal`. 15 | var personStr = LJSON.stringify(person); 16 | var personVal = LJSON.parse(personStr); 17 | var mail = personVal.mail("hello"); // would crash with JSON 18 | 19 | // But, since `mail` is pure, LJSON can deal with it correctly: 20 | console.log("Serialized value : " + personStr); 21 | console.log("Calling mail : " + LJSON.stringify(mail)); 22 | ``` 23 | 24 | Output: 25 | 26 | ```JavaScript 27 | Serialized value : {"name":"John","mail":(v0)=>({"author":"John","message":v0})} 28 | Calling mail : {"author":"John","message":"hello"} 29 | ``` 30 | 31 | See this and more examples on the [`Examples`](https://github.com/MaiaVictor/LJSON/tree/master/Examples) directory. 32 | 33 | Also check [Moon-lang](https://github.com/MaiaVictor/moon-lang), the spiritual successor of LJSON which includes primitive operations. 34 | 35 | ## More info 36 | 37 | - [Installing](#installing) 38 | - [Other languages](#other-languages) 39 | - [Why?](#why?) 40 | - [Using primitives](#using-primitives) 41 | - [Safety](#safety) 42 | - [TODO](#todo) 43 | 44 | ## Installing 45 | 46 | npm install ljson 47 | 48 | Or just download `LJSON.js` and `parsinhora.js` and import directly. 49 | 50 | ## Other languages 51 | 52 | Currently, there is a port to [PHP](https://github.com/Kanti/LJSON) kindly made by [Kanti](https://github.com/Kanti). 53 | 54 | ## Why? 55 | 56 | Other than convenience, there are times when you simply can't avoid running user code. For example, if a feature in your online game requires players to define scripts to control their ingame characters, you could implement it by receiving their code as strings, and using `eval`: 57 | 58 | ```javascript 59 | // client-side 60 | script = ["function playerScript(player){", 61 | "if (player.targetInRange('poring'))", 62 | "player.castSpell('fire bolt',player.findTarget('poring'));", 63 | "}"].join("\n"); 64 | server.send(script); 65 | 66 | // server-side: 67 | player.onSend("script",function(script){ 68 | player.installScript(eval(script)); // what could go wrong 69 | }); 70 | ``` 71 | 72 | Except that is probably the worst idea ever. Trusting user defined code is a security-person's worst nightmare. Workarounds include sandboxes and defining your own safe DSL - but those solutions can be overkill. There is a simpler way: pure functions. Instead of starting with power to do anything (arbitrary functions) and struggling to control it, you start with no power at all (pure functions) and add only the right primitives to do what your app requires. The code above could be rewritten as: 73 | 74 | ```javascript 75 | // client-side 76 | function playerScript($,player){ 77 | return $("if", $("targetInRange", player, "poring"), 78 | $("castSpell", player, "fire bolt", $("findTarget", player, "poring"))); 79 | }; 80 | var script = LJSON.stringify(playerScript); 81 | server.send(script); 82 | 83 | // server-side: 84 | player.onSend("script", function(script){ 85 | player.installScript(LJSON.parseWithLib(safeLib, script)); // not so bad 86 | }); 87 | ``` 88 | 89 | Where the `$` is an environment with the set of primitives your players are allowed to use, including things such as math operators, flow control and in-game commands. Of course, that lispy-like code isn't nearly as good looking as the former version, but is completely safe and pure. Functions defined that way can be stringified, communicated and parsed securely - just like JSON. 90 | 91 | ## Using primitives 92 | 93 | LJSON defines functions and function application only - no primitives such as numeric addition. So, for example, this is undefined behavior: 94 | 95 | LJSON.stringify(function(x){ return x+1; }); 96 | 97 | Because the `+1` bit isn't defined on LJSON. To actually do things with JS numbers, arrays, etc., you need to enable the proper primitives. You can do that either manually or by using LJSON's primitive decorators: 98 | 99 | withLib(lib, fn) 100 | withStdLib(fn) 101 | parseWithLib(lib, source) 102 | parseWithStdLib(source) 103 | 104 | `withLib` uses the first argument of a pure function as a way to access the primitives defined on the `lib` object. For example: 105 | 106 | ```javascript 107 | var lib = {triple:function(x){return x*3}}; 108 | nineTimes = LJSON.withLib(lib, function ($, x){ 109 | return $("triple", $("triple",x)); 110 | }); 111 | console.log(nineTimes(9)); 112 | ``` 113 | 114 | Here, `$` can be understood as "apply function from environment". Since our environment only defines one function, `triple`, that's the only thing `nineTimes` can do. That is, it could multiply a number by 3, by 9, by 27, by 81, etc. - but it couldn't multiply a number by 2. That's how restricted your environment is! Of course, defining your own environment would be cumbersome if you just want to use JS's common functions. For that, there is `LJSON.withStdLib`, which enables an standard environment with most common (pure/safe) functions such as math operators and strings: 115 | 116 | ```javascript 117 | hypotenuse = function($,a,b){ 118 | return $("sqrt",$("+",$("*",a,a),$("*",b,b))); 119 | }; 120 | var hypotenuseStr = LJSON.stringify(hypotenuse); 121 | var hypotenuseVal = LJSON.parseWithStdLib(hypotenuseStr); 122 | console.log(hypotenuseVal(3,4)); // output: 5 123 | ``` 124 | 125 | Remember you have to enable a lib **after** stringifying, communicating/storing and parsing the function. It is the last step. After you call `withStdLib`, the function gains access to primitives outside of the LJSON specs, so `LJSON.stringify` will not work on it anymore. 126 | 127 | ## Safety 128 | 129 | The fact you have to explicitly provide primitives to LJSON functions is what gives you confidence they won't do any nasty thing such as stealing your password, mining bitcoins or launching missiles. LJSON functions can only do what you give them power to. You are still able to serialize side-effective functions, but the side effects will happen on the act of the serialization and get stripped from the serialized output. 130 | 131 | ```JavaScript 132 | function nastyPair(a,b){ 133 | console.log("booom"); 134 | return { 135 | fst : a, 136 | snd : (function nastyId(x){ 137 | for (var i=0; i<3; ++i) 138 | console.log("mwahahhahha"); 139 | return x; 140 | })(b)}; 141 | }; 142 | console.log(LJSON.stringify(nastyPair)); 143 | 144 | // output: 145 | // booom 146 | // mwahahhahha 147 | // mwahahhahha 148 | // mwahahhahha 149 | // (v0,v1)=>({fst:v0,snd:v1}) 150 | ``` 151 | 152 | As a cool side effect of this, you can actually use JS primitives inside functions - as long as they can be eliminated at compile time. In other words, `LJSON.stringify` also works very well as a λ-calculator (due to JS engines speed): 153 | 154 | ```javascript 155 | console.log(LJSON.stringify(function(a){ 156 | // Things known at compile time are evaluated. 157 | var arr = []; 158 | for (var i=0; i<10; ++i) 159 | arr.push(i*10); 160 | 161 | // Even inside functions. 162 | var foo = function(x){ 163 | if (arr[5] < 10) 164 | var value = "I'm never returned"; 165 | else 166 | var value = "I'm always returned"; 167 | return value; 168 | }; 169 | 170 | // Even λ-calculus expressions! 171 | var C3 = (f)=>(x)=>f(f(f(x))); 172 | var C27 = C3(C3); // church number exponentiation of 3^3 173 | 174 | return [ 175 | arr, 176 | foo, 177 | C27]; 178 | })); 179 | ``` 180 | 181 | That outputs: 182 | 183 | ```JavaScript 184 | (v0)=>([ 185 | [0,10,20,30,40,50,60,70,80,90], 186 | (v1)=>("I'm always returned"), 187 | (v2)=>((v3)=>(v2(v2(v2(v2(v2(v2(v2(v2(v2(v2(v2(v2(v2(v2(v2(v2(v2(v2(v2(v2(v2(v2(v2(v2(v2(v2(v2(v3)))))))))))))))))))))))))))))]) 188 | ``` 189 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": { 3 | "name": "Victor Maia" 4 | }, 5 | "bugs": { 6 | "url": "https://github.com/MaiaVictor/LJSON/issues" 7 | }, 8 | "dependencies": {}, 9 | "description": "JSON extended with pure functions.", 10 | "devDependencies": {}, 11 | "directories": {}, 12 | "dist": { 13 | "shasum": "1a513a16e078103a7f7e56c10d3b6a0c7065675f", 14 | "tarball": "https://registry.npmjs.org/ljson/-/ljson-2.0.0.tgz" 15 | }, 16 | "gitHead": "534041f7af54f075fe5721c9de4d85f86421a919", 17 | "homepage": "https://github.com/MaiaVictor/LJSON#readme", 18 | "keywords": [ 19 | "JSON", 20 | "lambda-calculus", 21 | "functional-programming", 22 | "functions" 23 | ], 24 | "license": "MIT", 25 | "main": "LJSON.js", 26 | "maintainers": [ 27 | { 28 | "name": "maiavictor", 29 | "email": "srvictormaia@gmail.com" 30 | } 31 | ], 32 | "name": "ljson", 33 | "optionalDependencies": {}, 34 | "readme": "ERROR: No README data found!", 35 | "repository": { 36 | "type": "git", 37 | "url": "git+https://github.com/MaiaVictor/LJSON.git" 38 | }, 39 | "scripts": { 40 | "test": "echo \"Error: no test specified\" && exit 1" 41 | }, 42 | "version": "2.1.0" 43 | } 44 | -------------------------------------------------------------------------------- /parsenhora.js: -------------------------------------------------------------------------------- 1 | function parsenhora(parser){ 2 | return function(str){ 3 | var index = 0; 4 | function get(){ 5 | return index !== str.length ? str[index++] : null; 6 | }; 7 | function chr(c){ 8 | return function(){ 9 | return str[index] === c ? str[index++] : null; 10 | }; 11 | }; 12 | function isDigit(chr){ 13 | var ASCII = chr.charCodeAt(0); 14 | return ASCII >= 48 && ASCII <= 57; // 0-9 15 | }; 16 | function isHex(chr){ 17 | var ASCII = chr.charCodeAt(0); 18 | return isDigit(chr) 19 | || ASCII >= 65 && ASCII <= 70 // A-Z 20 | || ASCII >= 97 && ASCII <= 102; // a-z 21 | }; 22 | function satisfy(test){ 23 | return function(){ 24 | return index !== str.length && test(str[index]) ? str[index++] : null; 25 | }; 26 | }; 27 | function digit(){ 28 | return satisfy(isDigit)(); 29 | }; 30 | function hex(){ 31 | return satisfy(isHex)(); 32 | }; 33 | function string(s){ 34 | return function(){ 35 | for (var i=0, l=s.length; i= 97 && ASCII <= 122 // a-z 48 | || ASCII >= 65 && ASCII <= 90 // A-Z 49 | || ASCII >= 48 && ASCII <= 57; // 0-9 50 | }; 51 | function wordChar(){ 52 | return satisfy(isWordChar)(); 53 | }; 54 | function wordCharExceptDigits(){ 55 | return satisfy(function(c){ 56 | return isWordChar(c) && !isDigit(c); 57 | })(); 58 | }; 59 | var head = wordCharExceptDigits(); 60 | if (head === null) 61 | return null; 62 | var tail = many(wordChar)(); 63 | if (tail === null) 64 | return null; 65 | return [head].concat(tail).join(""); 66 | }; 67 | function choice(options){ 68 | return function(){ 69 | for (var i=0, l=options.length; i