diff --git a/.babelrc b/.babelrc new file mode 100644 index 00000000..002b4aa0 --- /dev/null +++ b/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["env"] +} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..2ca0133b --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +* text eol=lf +.* text eol=lf +dist/* binary diff --git a/.gitignore b/.gitignore index b25c15b8..95ea5187 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,8 @@ *~ +node_modules +coverage +npm-debug.log +\#* +.\#* +.DEV +.[0-9]* diff --git a/.npmignore b/.npmignore index d29dc586..fdca3a9e 100644 --- a/.npmignore +++ b/.npmignore @@ -1,2 +1,7 @@ spec *~ +covarage +Makefile +jest.config.js +version +templates diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..75affb3c --- /dev/null +++ b/.travis.yml @@ -0,0 +1,10 @@ +language: node_js +node_js: + - "node" +install: + - npm install +script: + - make lint + - make test +after_script: + - make coveralls diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..20f9c5a2 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,18 @@ +## 0.2.0 +### Features +* Add reduce and set functions +* add while, ++ and -- macros +* ignore comments everything after ; but not inside strings and regexes +* gensym and load functions +* better string function +* Pair methods for working with ALists + Pair::reduce +* throw exception on car/cdr with non list + +### Bugs +* fix parsing empty strings +* fix various errors catch by lint +* fix parsing ALists with list as keys and values +* fix parsing quasiquote that evaluate to single pair out if unquote + +## 0.1.0 +* Initial version diff --git a/Makefile b/Makefile index 0f29c3e1..c601b875 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,58 @@ -.PHONY publish +.PHONY: publish test coveralls lint + +VERSION=0.1.0 +BRANCH=`git branch | grep '^*' | sed 's/* //'` +DATE=`date -uR` +SPEC_CHECKSUM=`md5sum spec/lips.spec.js | cut -d' ' -f 1` +COMMIT=`git log -n 1 | grep commit | sed 's/commit //'` + +GIT=git +SED=sed +RM=rm +TEST=test +CAT=cat +NPM=npm +ESLINT=./node_modules/.bin/eslint +COVERALLS=./node_modules/.bin/coveralls +JEST=./node_modules/.bin/jest +UGLIFY=./node_modules/.bin/uglifyjs +BABEL=./node_modules/.bin/babel + + +ALL: Makefile .$(VERSION) dist/lips.js dist/lips.min.js README.md package.json + +dist/lips.js: src/lips.js .$(VERSION) + $(GIT) branch | grep '* devel' > /dev/null && $(SED) -e "s/{{VER}}/DEV/g" -e "s/{{DATE}}/$(DATE)/g" src/lips.js > dist/lips.tmp.js || $(SED) -e "s/{{VER}}/$(VERSION)/g" -e "s/{{DATE}}/$(DATE)/g" src/lips.js > dist/lips.tmp.js + $(BABEL) dist/lips.tmp.js > dist/lips.js + $(RM) dist/lips.tmp.js + +dist/lips.min.js: dist/lips.js .$(VERSION) + $(UGLIFY) -o dist/lips.min.js --comments --mangle -- dist/lips.js + +Makefile: templates/Makefile + $(SED) -e "s/{{VER""SION}}/"$(VERSION)"/" templates/Makefile > Makefile + +package.json: templates/package.json .$(VERSION) + $(SED) -e "s/{{VER}}/"$(VERSION)"/" templates/package.json > package.json || true + +README.md: templates/README.md + $(GIT) branch | grep '* devel' > /dev/null && $(SED) -e "s/{{VER}}/DEV/g" -e \ + "s/{{BRANCH}}/$(BRANCH)/g" -e "s/{{CHECKSUM}}/$(SPEC_CHECKSUM)/g" \ + -e "s/{{COMMIT}}/$(COMMIT)/g" < templates/README.md > README.md || \ + $(SED) -e "s/{{VER}}/$(VERSION)/g" -e "s/{{BRANCH}}/$(BRANCH)/g" -e \ + "s/{{CHECKSUM}}/$(SPEC_CHECKSUM)/g" -e "s/{{COMMIT}}/$(COMMIT)/g" < templates/README.md > README.md + +.$(VERSION): Makefile + touch .$(VERSION) publish: - npm publish --access=public + $(NPM) publish --access=public + +test: + $(JEST) + +coveralls: + $(CAT) ./coverage/lcov.info | $(COVERALLS) + +lint: + $(ESLINT) src/lips.js spec/lips.spec.js diff --git a/README.md b/README.md index e0a0cca1..df8c9884 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,12 @@ -## Lips is Pretty Simple +## LIPS is Pretty Simple -[![npm](https://img.shields.io/badge/npm-0.1.0-blue.svg)](https://www.npmjs.com/package/@jcubic/lips) +[![npm](https://img.shields.io/badge/npm-DEV-blue.svg)](https://www.npmjs.com/package/@jcubic/lips) +[![travis](https://travis-ci.org/jcubic/jquery.terminal.svg?branch=devel&acf06f4b5cae4b8fb9ca088cda6a89e805b48569)](https://travis-ci.org/jcubic/jquery.terminal) +[![Coverage Status](https://coveralls.io/repos/github/jcubic/lips/badge.svg?branch=devel&)](https://coveralls.io/github/jcubic/lips?branch=devel) -Lips is very simple Lisp, similar to Scheme writen in JavaScript. + + +LIPS is very simple Lisp, similar to Scheme writen in JavaScript. [Demo](https://codepen.io/jcubic/full/LQBaaV/) @@ -31,8 +35,8 @@ https://cdn.rawgit.com/jcubic/lips/master/index.js ```javascript var {parse, tokenize, evaluate} = require('@jcubic/lips'); -parse(tokenize(code)).forEach(function(code) { - evalute(code); +parse(tokenize(string)).forEach(function(code) { + evaluate(code); }); ``` @@ -46,7 +50,9 @@ You can create new environment using: var env = new Environment({}, lips.global_environment); ``` -You need to use global environment otherwise you will not have any functions. +First argument is an object with functions, macros and varibles (see Extending LIPS at the end). +Second argument is parent environment, you need to use global environment (or other that extend global) +otherwise you will not have any functions. ## What's in @@ -71,7 +77,7 @@ You need to use global environment otherwise you will not have any functions. (print (cadaddr lst))) ``` -all functions that match this regex `c[ad]{2,5}r` +all functions that match this regex `c[ad]{2,5}r` are defined. ### ALists @@ -124,7 +130,7 @@ then type S-Expression like `(print 10)`. If function return Promise the execution is paused and restored when Promise is resolved -### Access JavaScript functions +### Access JavaScript functions and objects ```scheme ((. window "alert") "hello") @@ -145,14 +151,14 @@ function `$` is available because it's in window object. or operate on strings -```scheme +``` ((. "foo bar baz" "replace") /^[a-z]+/g "baz") (let ((match (. "foo bar baz" "match"))) (array->list (match /([a-z]+)/g))) ``` -### Mapping and filtering +### Mapping, filtering and reducing ```scheme (map car (list @@ -164,13 +170,47 @@ or operate on strings (filter (lambda (x) (== (% x 2) 0)) (list 1 2 3 4 5)) + +(define (reverse list) + (reduce (lambda (list x) (cons x list)) list)) + +(reverse '(1 2 3 4)) +``` + +### Working with arrays + +You can modify array with `set` function and to get the value of the array you can use `.` dot function. + +```scheme +(let ((arr (list->array '(1 2 3 4)))) + (set arr 0 2) + (print (array->list arr))) + +(let* ((div ((. document "querySelectorAll") ".terminal-output > div")) + (len (. div "length")) + (i 0)) + (while (< i len) + (print (. (. div i) "innerHTML")) + (++ i))) +``` + +this equivalent of JavaScript code: + +```javascript +var div = document.querySelectorAll(".terminal div"); +var len = div.length; +var i = 0; +while (i < len) { + console.log(div[i].innerHTML); + ++i; +} ``` ### Math and boolean operators `< > => <= ++ -- + - * / % and or` -## Extending Lips +## Extending LIPS to create new function from JavaScript you can use: @@ -180,9 +220,9 @@ env.set('replace', function(re, sub, string) { }); ``` -then you can use it in lips: +then you can use it in LIPS: -```scheme +``` (replace /(foo|bar)/g "hello" "foo bar baz") ``` @@ -194,14 +234,11 @@ single function argument, that should return lisp code (instance of Pair) var {Macro, Pair, Symbol, nil} = lips; env.set('quote-car', new Macro(function(code) { - return new Pair( - new Symbol("quote"), - new Pair(code.car.car, nil) - ); + return Pair.fromArray([new Symbol('quote'), code.car.car]); })); ``` -and you can execute this macro in lips: +and you can execute this macro in LIPS: ```scheme (quote-car (foo bar baz)) diff --git a/demo.html b/demo.html index f87c73a0..b369d899 100644 --- a/demo.html +++ b/demo.html @@ -2,14 +2,33 @@ - Lips Demo + LIPS Demo - + + diff --git a/dist/lips.js b/dist/lips.js new file mode 100644 index 00000000..bda784f2 --- /dev/null +++ b/dist/lips.js @@ -0,0 +1,1185 @@ +/**@license + * LIPS is Pretty Simple - version DEV + * + * Copyright (c) 2018 Jakub Jankiewicz + * Released under the MIT license + * + * build: Sun, 04 Mar 2018 08:09:12 +0000 + */ +/* + * TODO: Pair.prototype.toObject = alist to Object + */ +"use strict"; +/* global define, module, setTimeout, jQuery */ + +var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; + +function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } + +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define([], function () { + return root.lips = factory(); + }); + } else if ((typeof module === 'undefined' ? 'undefined' : _typeof(module)) === 'object' && module.exports) { + // Node/CommonJS + module.exports = factory(); + } else { + root.lips = factory(); + } +})(typeof self !== 'undefined' ? self : undefined, function (undefined) { + // parse_argument based on function from jQuery Terminal + var re_re = /^\/((?:\\\/|[^/]|\[[^\]]*\/[^\]]*\])+)\/([gimy]*)$/; + var float_re = /^[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?$/; + // ---------------------------------------------------------------------- + function parse_argument(arg) { + function parse_string(string) { + // remove quotes if before are even number of slashes + // we don't remove slases becuase they are handled by JSON.parse + //string = string.replace(/([^\\])['"]$/, '$1'); + if (string.match(/^['"]/)) { + if (string === '""' || string === "''") { + return ''; + } + var quote = string[0]; + var re = new RegExp("((^|[^\\\\])(?:\\\\\\\\)*)" + quote, "g"); + string = string.replace(re, "$1"); + } + // use build in function to parse rest of escaped characters + return JSON.parse('"' + string + '"'); + } + var regex = arg.match(re_re); + if (regex) { + return new RegExp(regex[1], regex[2]); + } else if (arg.match(/['"]/)) { + return parse_string(arg); + } else if (arg.match(/^-?[0-9]+$/)) { + return parseInt(arg, 10); + } else if (arg.match(float_re)) { + return parseFloat(arg); + } else if (arg === 'nil') { + return nil; + } else { + return new _Symbol(arg); + } + } + // ---------------------------------------------------------------------- + /* eslint-disable */ + var tokens_re = /("[^"\\]*(?:\\[\S\s][^"\\]*)*"|\/[^\/\\]*(?:\\[\S\s][^\/\\]*)*\/[gimy]*(?=\s|\(|\)|$)|;.*|\(|\)|'|\.|,@|,|`|[^(\s)]+)/gi; + /* eslint-enable */ + // ---------------------------------------------------------------------- + function tokenize(str) { + return str.split('\n').map(function (line) { + return line.split(tokens_re).map(function (token) { + if (token.match(/^;/)) { + return null; + } + return token.trim(); + }).filter(Boolean); + }).reduce(function (arr, tokens) { + return arr.concat(tokens); + }, []); + } + // ---------------------------------------------------------------------- + var specials = { + "'": new _Symbol('quote'), + '`': new _Symbol('quasiquote'), + ',': new _Symbol('unquote'), + ',@': new _Symbol('unquote-splicing') + }; + // ---------------------------------------------------------------------- + // :: tokens are the array of strings from tokenizer + // :: the return value is lisp code created out of Pair class + // ---------------------------------------------------------------------- + function parse(tokens) { + var stack = []; + var result = []; + var special = null; + var special_tokens = Object.keys(specials); + var special_forms = special_tokens.map(function (s) { + return specials[s].name; + }); + var parents = 0; + var first_value = false; + tokens.forEach(function (token) { + var top = stack[stack.length - 1]; + if (special_tokens.indexOf(token) !== -1) { + special = token; + } else if (token === '(') { + first_value = true; + parents++; + if (special) { + stack.push([specials[special]]); + special = null; + } + stack.push([]); + } else if (token === '.' && !first_value) { + stack[stack.length - 1] = Pair.fromArray(top); + } else if (token === ')') { + parents--; + if (!stack.length) { + throw new Error('Unbalanced parenthesis'); + } + if (stack.length === 1) { + result.push(stack.pop()); + } else if (stack.length > 1) { + var list = stack.pop(); + top = stack[stack.length - 1]; + if (top instanceof Array) { + top.push(list); + } else if (top instanceof Pair) { + top.append(Pair.fromArray(list)); + } + if (top instanceof Array && top[0] instanceof _Symbol && special_forms.includes(top[0].name) && stack.length > 1) { + stack.pop(); + if (stack[stack.length - 1].length === 0) { + stack[stack.length - 1] = top; + } else if (stack[stack.length - 1] instanceof Pair) { + if (stack[stack.length - 1].cdr instanceof Pair) { + stack[stack.length - 1] = new Pair(stack[stack.length - 1], Pair.fromArray(top)); + } else { + stack[stack.length - 1].cdr = Pair.fromArray(top); + } + } else { + stack[stack.length - 1].push(top); + } + } + } + if (parents === 0 && stack.length) { + result.push(stack.pop()); + } + } else { + first_value = false; + var value = parse_argument(token); + if (special) { + value = [specials[special], value]; + special = false; + } + if (top instanceof Pair) { + var node = top; + while (true) { + if (node.cdr === nil) { + node.cdr = value; + break; + } else { + node = node.cdr; + } + } + } else if (!stack.length) { + result.push(value); + } else { + top.push(value); + } + } + }); + if (stack.length) { + throw new Error('Unbalanced parenthesis'); + } + return result.map(function (arg) { + if (arg instanceof Array) { + return Pair.fromArray(arg); + } + return arg; + }); + } + // ---------------------------------------------------------------------- + // :: Symbol constructor + // ---------------------------------------------------------------------- + function _Symbol(name) { + this.name = name; + } + _Symbol.is = function (symbol, name) { + return symbol instanceof _Symbol && typeof name === 'string' && symbol.name === name; + }; + _Symbol.prototype.toJSON = _Symbol.prototype.toString = function () { + //return '<#symbol \'' + this.name + '\'>'; + return this.name; + }; + // ---------------------------------------------------------------------- + // :: Nil constructor with only once instance + // ---------------------------------------------------------------------- + function Nil() {} + Nil.prototype.toString = function () { + return 'nil'; + }; + var nil = new Nil(); + // ---------------------------------------------------------------------- + // :: Pair constructor + // ---------------------------------------------------------------------- + function Pair(car, cdr) { + this.car = car; + this.cdr = cdr; + } + Pair.prototype.length = function () { + var len = 0; + var node = this; + while (true) { + if (node === nil) { + break; + } + len++; + node = node.cdr; + } + return len; + }; + Pair.prototype.clone = function () { + var cdr; + if (this.cdr === nil) { + cdr = nil; + } else { + cdr = this.cdr.clone(); + } + return new Pair(this.car, cdr); + }; + Pair.prototype.toArray = function () { + if (this.cdr === nil && this.car === nil) { + return []; + } + var result = []; + if (this.car instanceof Pair) { + result.push(this.car.toArray()); + } else { + result.push(this.car); + } + if (this.cdr instanceof Pair) { + result = result.concat(this.cdr.toArray()); + } + return result; + }; + Pair.fromArray = function (array) { + if (array instanceof Pair) { + return array; + } + if (array.length && !array instanceof Array) { + array = [].concat(_toConsumableArray(array)); + } + if (array.length === 0) { + return new Pair(nil, nil); + } else { + var car; + if (array[0] instanceof Array) { + car = Pair.fromArray(array[0]); + } else { + car = array[0]; + } + if (array.length === 1) { + return new Pair(car, nil); + } else { + return new Pair(car, Pair.fromArray(array.slice(1))); + } + } + }; + Pair.prototype.toObject = function () { + var node = this; + var result = {}; + while (true) { + if (node instanceof Pair && node.car instanceof Pair) { + var pair = node.car; + var name = pair.car; + if (name instanceof _Symbol) { + name = name.name; + } + result[name] = pair.cdr; + node = node.cdr; + } else { + break; + } + } + return result; + }; + Pair.fromPairs = function (array) { + return array.reduce(function (list, pair) { + return new Pair(new Pair(new _Symbol(pair[0]), pair[1]), list); + }, nil); + }; + Pair.fromObject = function (obj) { + var array = Object.keys(obj).map(function (key) { + return [key, obj[key]]; + }); + return Pair.fromPairs(array); + }; + Pair.prototype.reduce = function (fn) { + var node = this; + var result = nil; + while (true) { + if (node !== nil) { + result = fn(result, node.car); + node = node.cdr; + } else { + break; + } + } + return result; + }; + Pair.prototype.reverse = function () { + var node = this; + var prev = nil; + while (node !== nil) { + var next = node.cdr; + node.cdr = prev; + prev = node; + node = next; + } + return prev; + }; + Pair.prototype.transform = function (fn) { + var visited = []; + function recur(pair) { + if (pair instanceof Pair) { + if (pair.replace) { + delete pair.replace; + return pair; + } + var car = fn(pair.car); + if (car instanceof Pair) { + car = recur(car); + visited.push(car); + } + var cdr = fn(pair.cdr); + if (cdr instanceof Pair) { + cdr = recur(cdr); + visited.push(cdr); + } + return new Pair(car, cdr); + } + return pair; + } + return recur(this); + }; + Pair.prototype.toString = function () { + var arr = ['(']; + if (typeof this.car === 'string') { + arr.push(JSON.stringify(this.car)); + } else if (typeof this.car !== 'undefined') { + arr.push(this.car); + } + if (this.cdr instanceof Pair) { + arr.push(' '); + arr.push(this.cdr.toString().replace(/^\(|\)$/g, '')); + } else if (typeof this.cdr !== 'undefined' && this.cdr !== nil) { + if (typeof this.cdr === 'string') { + arr = arr.concat([' . ', JSON.stringify(this.cdr)]); + } else { + arr = arr.concat([' . ', this.cdr]); + } + } + arr.push(')'); + return arr.join(''); + }; + Pair.prototype.append = function (pair) { + if (pair instanceof Array) { + return this.append(Pair.fromArray(pair)); + } + var p = this; + while (true) { + if (p instanceof Pair && p.cdr !== nil) { + p = p.cdr; + } else { + break; + } + } + p.cdr = pair; + return this; + }; + + // ---------------------------------------------------------------------- + // :: Macro constructor + // ---------------------------------------------------------------------- + function Macro(fn) { + this.fn = fn; + } + Macro.prototype.invoke = function (code, env) { + return this.fn.call(env, code); + }; + + // ---------------------------------------------------------------------- + // :: Environment constructor (parent argument is optional) + // ---------------------------------------------------------------------- + function Environment(obj, parent) { + this.env = obj; + this.parent = parent; + } + Environment.prototype.get = function (symbol) { + if (symbol instanceof _Symbol) { + if (typeof this.env[symbol.name] !== 'undefined') { + return this.env[symbol.name]; + } + } else if (typeof symbol === 'string') { + if (typeof this.env[symbol] !== 'undefined') { + return this.env[symbol]; + } + } + + if (this.parent instanceof Environment) { + return this.parent.get(symbol); + } else if (symbol instanceof _Symbol) { + if (typeof window[symbol.name] !== 'undefined') { + return window[symbol.name]; + } + } else if (typeof symbol === 'string') { + if (typeof window[symbol] !== 'undefined') { + return window[symbol]; + } + } + }; + Environment.prototype.set = function (name, value) { + this.env[name] = value; + }; + // ---------------------------------------------------------------------- + // :: Quote constructor used to pause evaluation from Macro + // ---------------------------------------------------------------------- + function Quote(value) { + this.value = value; + } + // ---------------------------------------------------------------------- + // :: function that return macro for let and let* + // ---------------------------------------------------------------------- + function let_macro(asterisk) { + return new Macro(function (code) { + var _this = this; + + var args = this.get('list->array')(code.car); + var env = new Environment({}, this); + args.forEach(function (pair) { + env.set(pair.car, evaluate(pair.cdr.car, asterisk ? env : _this)); + }); + var output = new Pair(new _Symbol('begin'), code.cdr); + return new Quote(evaluate(output, env)); + }); + } + var gensym = function () { + var count = 0; + return function () { + count++; + return new _Symbol('#' + count); + }; + }(); + function request(url) { + var method = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'GET'; + var headers = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + var data = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : null; + + var xhr = new XMLHttpRequest(); + xhr.open(method, url, true); + Object.keys(headers).forEach(function (name) { + xhr.setRequestHeader(name, headers[name]); + }); + return new Promise(function (resolve) { + xhr.onreadystatechange = function () { + if (xhr.readyState === 4 && xhr.status === 200) { + resolve(xhr.responseText); + } + }; + if (data !== null) { + xhr.send(data); + } else { + xhr.send(); + } + }); + } + var global_env = new Environment({ + nil: nil, + window: window, + 'true': true, + 'false': false, + stdout: { + write: function write() { + var _console; + + (_console = console).log.apply(_console, arguments); + } + }, + stdin: { + read: function read() { + return new Promise(function (resolve) { + resolve(prompt('')); + }); + } + }, + cons: function cons(car, cdr) { + return new Pair(car, cdr); + }, + car: function car(list) { + if (list instanceof Pair) { + return list.car; + } else { + throw new Error('argument to car need to be a list'); + } + }, + cdr: function cdr(list) { + if (list instanceof Pair) { + return list.cdr; + } else { + throw new Error('argument to cdr need to be a list'); + } + }, + 'set-car': function setCar(slot, value) { + slot.car = value; + }, + 'set-cdr': function setCdr(slot, value) { + slot.cdr = value; + }, + assoc: function assoc(list, key) { + var node = list; + var name = key instanceof _Symbol ? key.name : key; + while (true) { + var car = node.car.car; + if (car instanceof _Symbol && car.name === name || car.name === name) { + return node.car; + } else { + node = node.cdr; + } + } + }, + gensym: gensym, + load: function load(file) { + var _this2 = this; + + request(file).then(function (code) { + _this2.get('eval')(_this2.get('read')(code)); + }); + }, + 'while': new Macro(function (code) { + var self = this; + var begin = new Pair(new _Symbol('begin'), code.cdr); + return new Promise(function (resolve) { + var result; + (function loop() { + function next(cond) { + if (cond) { + var value = evaluate(begin, self); + if (value instanceof Promise) { + value.then(function (value) { + result = value; + loop(); + }); + } else { + result = value; + loop(); + } + } else { + resolve(result); + } + } + var cond = evaluate(code.car, self); + if (cond instanceof Promise) { + cond.then(next); + } else { + next(cond); + } + })(); + }); + }), + 'if': new Macro(function (code) { + var _this3 = this; + + var resolve = function resolve(cond) { + if (cond) { + var true_value = evaluate(code.cdr.car, _this3); + if (typeof true_value === 'undefined') { + return; + } + return true_value; + } else if (code.cdr.cdr.car instanceof Pair) { + var false_value = evaluate(code.cdr.cdr.car, _this3); + if (typeof false_value === 'undefined') { + return false; + } + return false_value; + } else { + return false; + } + }; + var cond = evaluate(code.car, this); + if (cond instanceof Promise) { + return cond.then(resolve); + } else { + return resolve(cond); + } + }), + 'let*': let_macro(true), + 'let': let_macro(false), + 'begin': new Macro(function (code) { + var _this4 = this; + + var arr = this.get('list->array')(code); + return arr.reduce(function (_, code) { + return evaluate(code, _this4); + }, 0); + }), + timer: new Macro(function (code) { + var _this5 = this; + + return new Promise(function (resolve) { + setTimeout(function () { + resolve(new Quote(evaluate(code.cdr, _this5))); + }, code.car); + }); + }), + define: new Macro(function (code) { + if (code.car instanceof Pair && code.car.car instanceof _Symbol) { + var new_code = new Pair(new _Symbol("define"), new Pair(code.car.car, new Pair(new Pair(new _Symbol("lambda"), new Pair(code.car.cdr, code.cdr))))); + return new_code; + } + var value = code.cdr.car; + if (value instanceof Pair) { + value = evaluate(value, this); + } + if (code.car instanceof _Symbol) { + this.env[code.car.name] = value; + } + }), + set: function set(obj, key, value) { + obj[key] = value; + }, + 'eval': function _eval(code) { + var _this6 = this; + + if (code instanceof Pair) { + return evaluate(code, this); + } + if (code instanceof Array) { + var result; + code.forEach(function (code) { + result = evaluate(code, _this6); + }); + return result; + } + }, + lambda: new Macro(function (code) { + var _this7 = this; + + return function () { + var env = new Environment({}, _this7); + var name = code.car; + var i = 0; + var value; + while (true) { + if (name.car !== nil) { + if (typeof (arguments.length <= i ? undefined : arguments[i]) === 'undefined') { + value = nil; + } else { + value = arguments.length <= i ? undefined : arguments[i]; + } + env.env[name.car.name] = value; + } + if (name.cdr === nil) { + break; + } + i++; + name = name.cdr; + } + return evaluate(code.cdr.car, env); + }; + }), + defmacro: new Macro(function (macro) { + if (macro.car.car instanceof _Symbol) { + this.env[macro.car.car.name] = new Macro(function (code) { + var env = new Environment({}, this); + var name = macro.car.cdr; + var arg = code; + while (true) { + if (name.car !== nil && arg.car !== nil) { + env.env[name.car.name] = arg.car; + } + if (name.cdr === nil) { + break; + } + arg = arg.cdr; + name = name.cdr; + } + return evaluate(macro.cdr.car, env); + }); + } + }), + quote: new Macro(function (arg) { + return new Quote(arg.car); + }), + quasiquote: new Macro(function (arg) { + var self = this; + function recur(pair) { + if (pair instanceof Pair) { + var eval_pair; + if (_Symbol.is(pair.car.car, 'unquote-splicing')) { + eval_pair = evaluate(pair.car.cdr.car, self); + if (!eval_pair instanceof Pair) { + throw new Error('Value of unquote-splicing need' + ' to be pair'); + } + if (pair.cdr instanceof Pair) { + if (eval_pair instanceof Pair) { + eval_pair.cdr.append(recur(pair.cdr)); + } else { + eval_pair = new Pair(eval_pair, recur(pair.cdr)); + } + } + return eval_pair; + } + if (_Symbol.is(pair.car, 'unquote-splicing')) { + eval_pair = evaluate(pair.cdr.car, self); + if (!eval_pair instanceof Pair) { + throw new Error('Value of unquote-splicing' + ' need to be pair'); + } + return eval_pair; + } + if (_Symbol.is(pair.car, 'unquote')) { + if (pair.cdr.cdr !== nil) { + return new Pair(evaluate(pair.cdr.car, self), pair.cdr.cdr); + } else { + return evaluate(pair.cdr.car, self); + } + } + var car = pair.car; + if (car instanceof Pair) { + car = recur(car); + } + var cdr = pair.cdr; + if (cdr instanceof Pair) { + cdr = recur(cdr); + } + return new Pair(car, cdr); + } + return pair; + } + return new Quote(recur(arg.car)); + }), + clone: function clone(list) { + return list.clone(); + }, + append: function append(list, item) { + return this.get('append!')(list.clone(), item); + }, + 'append!': function append(list, item) { + var node = list; + while (true) { + if (node.cdr === nil) { + node.cdr = item; + break; + } + node = node.cdr; + } + return list; + }, + list: function list() { + return Pair.fromArray([].slice.call(arguments)); + }, + concat: function concat() { + return [].join.call(arguments, ''); + }, + string: function string(obj) { + if (typeof jQuery !== 'undefined' && obj instanceof jQuery.fn.init) { + return '<#jQuery>'; + } + if (obj instanceof Macro) { + //return '<#Macro>'; + } + if (typeof obj === 'undefined') { + return '<#undefined>'; + } + if (typeof obj === 'function') { + return '<#function>'; + } + if (obj === nil) { + return 'nil'; + } + if (obj instanceof Array || obj === null) { + return JSON.stringify(obj); + } + if (obj instanceof Pair) { + return obj.toString(); + } + if ((typeof obj === 'undefined' ? 'undefined' : _typeof(obj)) === 'object') { + var name = obj.constructor.name; + if (name !== '') { + return '<#' + name + '>'; + } + return '<#Object>'; + } + if (typeof obj !== 'string') { + return obj.toString(); + } + return obj; + }, + env: function env(_env) { + _env = _env || this; + var names = Object.keys(_env.env); + var result; + if (names.length) { + result = Pair.fromArray(names); + } else { + result = nil; + } + if (_env.parent !== undefined) { + return this.get('env').call(this, _env.parent).append(result); + } + return result; + }, + '.': function _(obj, arg) { + var name = arg instanceof _Symbol ? arg.name : arg; + var value = obj[name]; + if (typeof value === 'function') { + return value.bind(obj); + } + return value; + }, + read: function read(arg) { + var _this8 = this; + + if (typeof arg === 'string') { + return parse(tokenize(arg)); + } + return this.get('stdin').read().then(function (text) { + return _this8.get('read').call(_this8, text); + }); + }, + print: function print() { + var _get, + _this9 = this; + + for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { + args[_key] = arguments[_key]; + } + + (_get = this.get('stdout')).write.apply(_get, _toConsumableArray(args.map(function (arg) { + return _this9.get('string')(arg); + }))); + }, + 'array->list': function arrayList(array) { + return Pair.fromArray(array); + }, + 'list->array': function listArray(list) { + var result = []; + var node = list; + while (true) { + if (node instanceof Pair) { + result.push(node.car); + node = node.cdr; + } else { + break; + } + } + return result; + }, + filter: function filter(fn, list) { + return Pair.fromArray(this.get('list->array')(list).filter(fn)); + }, + odd: function odd(num) { + return num % 2 === 1; + }, + even: function even(num) { + return num % 2 === 0; + }, + apply: function apply(fn, list) { + var args = this.get('list->array')(list); + return fn.apply(null, args); + }, + map: function map(fn, list) { + var result = this.get('list->array')(list).map(fn); + if (result.length) { + return Pair.fromArray(result); + } else { + return nil; + } + }, + reduce: function reduce(fn, list) { + var arr = this.get('list->array')(list); + return arr.reduce(function (list, item) { + return fn(list, item); + }, nil); + }, + // math functions + '*': function _() { + for (var _len2 = arguments.length, args = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { + args[_key2] = arguments[_key2]; + } + + return args.reduce(function (a, b) { + return a * b; + }); + }, + '+': function _() { + for (var _len3 = arguments.length, args = Array(_len3), _key3 = 0; _key3 < _len3; _key3++) { + args[_key3] = arguments[_key3]; + } + + return args.reduce(function (a, b) { + return a + b; + }); + }, + '-': function _() { + for (var _len4 = arguments.length, args = Array(_len4), _key4 = 0; _key4 < _len4; _key4++) { + args[_key4] = arguments[_key4]; + } + + return args.reduce(function (a, b) { + return a - b; + }); + }, + '/': function _() { + for (var _len5 = arguments.length, args = Array(_len5), _key5 = 0; _key5 < _len5; _key5++) { + args[_key5] = arguments[_key5]; + } + + return args.reduce(function (a, b) { + return a / b; + }); + }, + '%': function _(a, b) { + return a % b; + }, + // Booleans + "==": function _(a, b) { + return a === b; + }, + '>': function _(a, b) { + return a > b; + }, + '<': function _(a, b) { + return a < b; + }, + '<=': function _(a, b) { + return a <= b; + }, + '>=': function _(a, b) { + return a >= b; + }, + or: new Macro(function (code) { + var args = this.get('list->array')(code); + var self = this; + return new Promise(function (resolve) { + var result; + (function loop() { + function next(value) { + result = value; + if (result) { + resolve(value); + } + loop(); + } + var arg = args.shift(); + if (typeof arg === 'undefined') { + if (result) { + resolve(result); + } else { + resolve(false); + } + } else { + var value = evaluate(arg, self); + if (value instanceof Promise) { + value.then(next); + } else { + next(value); + } + } + })(); + }); + }), + and: new Macro(function (code) { + var args = this.get('list->array')(code); + var self = this; + return new Promise(function (resolve) { + var result; + (function loop() { + function next(value) { + result = value; + if (!result) { + resolve(false); + } + loop(); + } + var arg = args.shift(); + if (typeof arg === 'undefined') { + if (result) { + resolve(result); + } else { + resolve(false); + } + } else { + var value = evaluate(arg, self); + if (value instanceof Promise) { + value.then(next); + } else { + next(value); + } + } + })(); + }); + }), + '++': new Macro(function (code) { + var value = this.get(code.car) + 1; + this.set(code.car, value); + return value; + }), + '--': new Macro(function (code) { + var value = this.get(code.car) - 1; + this.set(code.car, value); + return value; + }) + }); + + // ---------------------------------------------------------------------- + // source: https://stackoverflow.com/a/4331218/387194 + function allPossibleCases(arr) { + if (arr.length === 1) { + return arr[0]; + } else { + var result = []; + // recur with the rest of array + var allCasesOfRest = allPossibleCases(arr.slice(1)); + for (var i = 0; i < allCasesOfRest.length; i++) { + for (var j = 0; j < arr[0].length; j++) { + result.push(arr[0][j] + allCasesOfRest[i]); + } + } + return result; + } + } + + // ---------------------------------------------------------------------- + function combinations(input, start, end) { + var result = []; + for (var i = start; i <= end; ++i) { + var input_arr = []; + for (var j = 0; j < i; ++j) { + input_arr.push(input); + } + result = result.concat(allPossibleCases(input_arr)); + } + return result; + } + // ---------------------------------------------------------------------- + // cadr caddr cadadr etc. + combinations(['d', 'a'], 2, 5).forEach(function (spec) { + var chars = spec.split('').reverse(); + global_env.set('c' + spec + 'r', function (arg) { + return chars.reduce(function (list, type) { + if (type === 'a') { + return list.car; + } else { + return list.cdr; + } + }, arg); + }); + }); + + // ---------------------------------------------------------------------- + function evaluate(code, env) { + env = env || global_env; + var value; + if (typeof code === 'undefined') { + return; + } + var first = code.car; + var rest = code.cdr; + if (first instanceof Pair) { + value = evaluate(first, env); + if (typeof value !== 'function') { + throw new Error(env.get('string')(value) + ' is not a function'); + } + } + if (typeof first === 'function') { + value = first; + } + if (first instanceof _Symbol) { + value = env.get(first); + if (value instanceof Macro) { + value = value.invoke(rest, env); + if (value instanceof Quote) { + return value.value; + } + return evaluate(value, env); + } else if (typeof value !== 'function') { + throw new Error('Unknown function `' + first.name + '\''); + } + } + if (typeof value === 'function') { + var args = []; + var node = rest; + while (true) { + if (node instanceof Pair) { + args.push(evaluate(node.car, env)); + node = node.cdr; + } else { + break; + } + } + var promises = args.filter(function (arg) { + return arg instanceof Promise; + }); + if (promises.length) { + return Promise.all(args).then(function (args) { + return value.apply(env, args); + }); + } + return value.apply(env, args); + } else if (code instanceof _Symbol) { + value = env.get(code); + if (value === 'undefined') { + throw new Error('Unbound variable `' + code.name + '\''); + } + return value; + } else { + return code; + } + } + // ---------------------------------------------------------------------- + + function balanced(code) { + var re = /[()]/; + var parenthesis = tokenize(code).filter(function (token) { + return token.match(re); + }); + var open = parenthesis.filter(function (p) { + return p === ')'; + }); + var close = parenthesis.filter(function (p) { + return p === '('; + }); + return open.length === close.length; + } + // -------------------------------------- + Pair.unDry = function (value) { + return new Pair(value.car, value.cdr); + }; + Pair.prototype.toDry = function () { + return { + value: { + car: this.car, + cdr: this.cdr + } + }; + }; + Nil.prototype.toDry = function () { + return { + value: null + }; + }; + Nil.unDry = function () { + return nil; + }; + _Symbol.prototype.toDry = function () { + return { + value: { + name: this.name + } + }; + }; + _Symbol.unDry = function (value) { + return new _Symbol(value.name); + }; + return { + version: 'DEV', + parse: parse, + tokenize: tokenize, + evaluate: evaluate, + Environment: Environment, + global_environment: global_env, + balanced_parenthesis: balanced, + Macro: Macro, + Quote: Quote, + Pair: Pair, + nil: nil, + Symbol: _Symbol + }; +}); + diff --git a/dist/lips.min.js b/dist/lips.min.js new file mode 100644 index 00000000..89bf112a --- /dev/null +++ b/dist/lips.min.js @@ -0,0 +1,9 @@ +/**@license + * LIPS is Pretty Simple - version DEV + * + * Copyright (c) 2018 Jakub Jankiewicz + * Released under the MIT license + * + * build: Sun, 04 Mar 2018 08:09:12 +0000 + */ +"use strict";var _typeof=typeof Symbol==="function"&&typeof Symbol.iterator==="symbol"?function(n){return typeof n}:function(n){return n&&typeof Symbol==="function"&&n.constructor===Symbol&&n!==Symbol.prototype?"symbol":typeof n};function _toConsumableArray(n){if(Array.isArray(n)){for(var r=0,e=Array(n.length);r1){var p=r.pop();d=r[r.length-1];if(d instanceof Array){d.push(p)}else if(d instanceof l){d.append(l.fromArray(p))}if(d instanceof Array&&d[0]instanceof c&&o.includes(d[0].name)&&r.length>1){r.pop();if(r[r.length-1].length===0){r[r.length-1]=d}else if(r[r.length-1]instanceof l){if(r[r.length-1].cdr instanceof l){r[r.length-1]=new l(r[r.length-1],l.fromArray(d))}else{r[r.length-1].cdr=l.fromArray(d)}}else{r[r.length-1].push(d)}}}if(u===0&&r.length){e.push(r.pop())}}else{h=false;var v=t(n);if(i){v=[f[i],v];i=false}if(d instanceof l){var y=d;while(true){if(y.cdr===s){y.cdr=v;break}else{y=y.cdr}}}else if(!r.length){e.push(v)}else{d.push(v)}}});if(r.length){throw new Error("Unbalanced parenthesis")}return e.map(function(n){if(n instanceof Array){return l.fromArray(n)}return n})}function c(n){this.name=n}c.is=function(n,r){return n instanceof c&&typeof r==="string"&&n.name===r};c.prototype.toJSON=c.prototype.toString=function(){return this.name};function u(){}u.prototype.toString=function(){return"nil"};var s=new u;function l(n,r){this.car=n;this.cdr=r}l.prototype.length=function(){var n=0;var r=this;while(true){if(r===s){break}n++;r=r.cdr}return n};l.prototype.clone=function(){var n;if(this.cdr===s){n=s}else{n=this.cdr.clone()}return new l(this.car,n)};l.prototype.toArray=function(){if(this.cdr===s&&this.car===s){return[]}var n=[];if(this.car instanceof l){n.push(this.car.toArray())}else{n.push(this.car)}if(this.cdr instanceof l){n=n.concat(this.cdr.toArray())}return n};l.fromArray=function(n){if(n instanceof l){return n}if(n.length&&!n instanceof Array){n=[].concat(_toConsumableArray(n))}if(n.length===0){return new l(s,s)}else{var r;if(n[0]instanceof Array){r=l.fromArray(n[0])}else{r=n[0]}if(n.length===1){return new l(r,s)}else{return new l(r,l.fromArray(n.slice(1)))}}};l.prototype.toObject=function(){var n=this;var r={};while(true){if(n instanceof l&&n.car instanceof l){var e=n.car;var t=e.car;if(t instanceof c){t=t.name}r[t]=e.cdr;n=n.cdr}else{break}}return r};l.fromPairs=function(n){return n.reduce(function(n,r){return new l(new l(new c(r[0]),r[1]),n)},s)};l.fromObject=function(n){var r=Object.keys(n).map(function(r){return[r,n[r]]});return l.fromPairs(r)};l.prototype.reduce=function(n){var r=this;var e=s;while(true){if(r!==s){e=n(e,r.car);r=r.cdr}else{break}}return e};l.prototype.reverse=function(){var n=this;var r=s;while(n!==s){var e=n.cdr;n.cdr=r;r=n;n=e}return r};l.prototype.transform=function(n){var r=[];function e(t){if(t instanceof l){if(t.replace){delete t.replace;return t}var i=n(t.car);if(i instanceof l){i=e(i);r.push(i)}var a=n(t.cdr);if(a instanceof l){a=e(a);r.push(a)}return new l(i,a)}return t}return e(this)};l.prototype.toString=function(){var n=["("];if(typeof this.car==="string"){n.push(JSON.stringify(this.car))}else if(typeof this.car!=="undefined"){n.push(this.car)}if(this.cdr instanceof l){n.push(" ");n.push(this.cdr.toString().replace(/^\(|\)$/g,""))}else if(typeof this.cdr!=="undefined"&&this.cdr!==s){if(typeof this.cdr==="string"){n=n.concat([" . ",JSON.stringify(this.cdr)])}else{n=n.concat([" . ",this.cdr])}}n.push(")");return n.join("")};l.prototype.append=function(n){if(n instanceof Array){return this.append(l.fromArray(n))}var r=this;while(true){if(r instanceof l&&r.cdr!==s){r=r.cdr}else{break}}r.cdr=n;return this};function h(n){this.fn=n}h.prototype.invoke=function(n,r){return this.fn.call(r,n)};function d(n,r){this.env=n;this.parent=r}d.prototype.get=function(n){if(n instanceof c){if(typeof this.env[n.name]!=="undefined"){return this.env[n.name]}}else if(typeof n==="string"){if(typeof this.env[n]!=="undefined"){return this.env[n]}}if(this.parent instanceof d){return this.parent.get(n)}else if(n instanceof c){if(typeof window[n.name]!=="undefined"){return window[n.name]}}else if(typeof n==="string"){if(typeof window[n]!=="undefined"){return window[n]}}};d.prototype.set=function(n,r){this.env[n]=r};function p(n){this.value=n}function v(n){return new h(function(r){var e=this;var t=this.get("list->array")(r.car);var i=new d({},this);t.forEach(function(r){i.set(r.car,A(r.cdr.car,n?i:e))});var a=new l(new c("begin"),r.cdr);return new p(A(a,i))})}var y=function(){var n=0;return function(){n++;return new c("#"+n)}}();function w(r){var e=arguments.length>1&&arguments[1]!==n?arguments[1]:"GET";var t=arguments.length>2&&arguments[2]!==n?arguments[2]:{};var i=arguments.length>3&&arguments[3]!==n?arguments[3]:null;var a=new XMLHttpRequest;a.open(e,r,true);Object.keys(t).forEach(function(n){a.setRequestHeader(n,t[n])});return new Promise(function(n){a.onreadystatechange=function(){if(a.readyState===4&&a.status===200){n(a.responseText)}};if(i!==null){a.send(i)}else{a.send()}})}var g=new d({nil:s,window:window,true:true,false:false,stdout:{write:function n(){var r;(r=console).log.apply(r,arguments)}},stdin:{read:function n(){return new Promise(function(n){n(prompt(""))})}},cons:function n(r,e){return new l(r,e)},car:function n(r){if(r instanceof l){return r.car}else{throw new Error("argument to car need to be a list")}},cdr:function n(r){if(r instanceof l){return r.cdr}else{throw new Error("argument to cdr need to be a list")}},"set-car":function n(r,e){r.car=e},"set-cdr":function n(r,e){r.cdr=e},assoc:function n(r,e){var t=r;var i=e instanceof c?e.name:e;while(true){var a=t.car.car;if(a instanceof c&&a.name===i||a.name===i){return t.car}else{t=t.cdr}}},gensym:y,load:function n(r){var e=this;w(r).then(function(n){e.get("eval")(e.get("read")(n))})},while:new h(function(n){var r=this;var e=new l(new c("begin"),n.cdr);return new Promise(function(t){var i;(function a(){function f(n){if(n){var f=A(e,r);if(f instanceof Promise){f.then(function(n){i=n;a()})}else{i=f;a()}}else{t(i)}}var o=A(n.car,r);if(o instanceof Promise){o.then(f)}else{f(o)}})()})}),if:new h(function(n){var r=this;var e=function e(t){if(t){var i=A(n.cdr.car,r);if(typeof i==="undefined"){return}return i}else if(n.cdr.cdr.car instanceof l){var a=A(n.cdr.cdr.car,r);if(typeof a==="undefined"){return false}return a}else{return false}};var t=A(n.car,this);if(t instanceof Promise){return t.then(e)}else{return e(t)}}),"let*":v(true),let:v(false),begin:new h(function(n){var r=this;var e=this.get("list->array")(n);return e.reduce(function(n,e){return A(e,r)},0)}),timer:new h(function(n){var r=this;return new Promise(function(e){setTimeout(function(){e(new p(A(n.cdr,r)))},n.car)})}),define:new h(function(n){if(n.car instanceof l&&n.car.car instanceof c){var r=new l(new c("define"),new l(n.car.car,new l(new l(new c("lambda"),new l(n.car.cdr,n.cdr)))));return r}var e=n.cdr.car;if(e instanceof l){e=A(e,this)}if(n.car instanceof c){this.env[n.car.name]=e}}),set:function n(r,e,t){r[e]=t},eval:function n(r){var e=this;if(r instanceof l){return A(r,this)}if(r instanceof Array){var t;r.forEach(function(n){t=A(n,e)});return t}},lambda:new h(function(r){var e=this;return function(){var t=new d({},e);var i=r.car;var a=0;var f;while(true){if(i.car!==s){if(typeof(arguments.length<=a?n:arguments[a])==="undefined"){f=s}else{f=arguments.length<=a?n:arguments[a]}t.env[i.car.name]=f}if(i.cdr===s){break}a++;i=i.cdr}return A(r.cdr.car,t)}}),defmacro:new h(function(n){if(n.car.car instanceof c){this.env[n.car.car.name]=new h(function(r){var e=new d({},this);var t=n.car.cdr;var i=r;while(true){if(t.car!==s&&i.car!==s){e.env[t.car.name]=i.car}if(t.cdr===s){break}i=i.cdr;t=t.cdr}return A(n.cdr.car,e)})}}),quote:new h(function(n){return new p(n.car)}),quasiquote:new h(function(n){var r=this;function e(n){if(n instanceof l){var t;if(c.is(n.car.car,"unquote-splicing")){t=A(n.car.cdr.car,r);if(!t instanceof l){throw new Error("Value of unquote-splicing need"+" to be pair")}if(n.cdr instanceof l){if(t instanceof l){t.cdr.append(e(n.cdr))}else{t=new l(t,e(n.cdr))}}return t}if(c.is(n.car,"unquote-splicing")){t=A(n.cdr.car,r);if(!t instanceof l){throw new Error("Value of unquote-splicing"+" need to be pair")}return t}if(c.is(n.car,"unquote")){if(n.cdr.cdr!==s){return new l(A(n.cdr.car,r),n.cdr.cdr)}else{return A(n.cdr.car,r)}}var i=n.car;if(i instanceof l){i=e(i)}var a=n.cdr;if(a instanceof l){a=e(a)}return new l(i,a)}return n}return new p(e(n.car))}),clone:function n(r){return r.clone()},append:function n(r,e){return this.get("append!")(r.clone(),e)},"append!":function n(r,e){var t=r;while(true){if(t.cdr===s){t.cdr=e;break}t=t.cdr}return r},list:function n(){return l.fromArray([].slice.call(arguments))},concat:function n(){return[].join.call(arguments,"")},string:function n(r){if(typeof jQuery!=="undefined"&&r instanceof jQuery.fn.init){return"<#jQuery>"}if(r instanceof h){}if(typeof r==="undefined"){return"<#undefined>"}if(typeof r==="function"){return"<#function>"}if(r===s){return"nil"}if(r instanceof Array||r===null){return JSON.stringify(r)}if(r instanceof l){return r.toString()}if((typeof r==="undefined"?"undefined":_typeof(r))==="object"){var e=r.constructor.name;if(e!==""){return"<#"+e+">"}return"<#Object>"}if(typeof r!=="string"){return r.toString()}return r},env:function r(e){e=e||this;var t=Object.keys(e.env);var i;if(t.length){i=l.fromArray(t)}else{i=s}if(e.parent!==n){return this.get("env").call(this,e.parent).append(i)}return i},".":function n(r,e){var t=e instanceof c?e.name:e;var i=r[t];if(typeof i==="function"){return i.bind(r)}return i},read:function n(r){var e=this;if(typeof r==="string"){return o(a(r))}return this.get("stdin").read().then(function(n){return e.get("read").call(e,n)})},print:function n(){var r,e=this;for(var t=arguments.length,i=Array(t),a=0;alist":function n(r){return l.fromArray(r)},"list->array":function n(r){var e=[];var t=r;while(true){if(t instanceof l){e.push(t.car);t=t.cdr}else{break}}return e},filter:function n(r,e){return l.fromArray(this.get("list->array")(e).filter(r))},odd:function n(r){return r%2===1},even:function n(r){return r%2===0},apply:function n(r,e){var t=this.get("list->array")(e);return r.apply(null,t)},map:function n(r,e){var t=this.get("list->array")(e).map(r);if(t.length){return l.fromArray(t)}else{return s}},reduce:function n(r,e){var t=this.get("list->array")(e);return t.reduce(function(n,e){return r(n,e)},s)},"*":function n(){for(var r=arguments.length,e=Array(r),t=0;t":function n(r,e){return r>e},"<":function n(r,e){return r=":function n(r,e){return r>=e},or:new h(function(n){var r=this.get("list->array")(n);var e=this;return new Promise(function(n){var t;(function i(){function a(r){t=r;if(t){n(r)}i()}var f=r.shift();if(typeof f==="undefined"){if(t){n(t)}else{n(false)}}else{var o=A(f,e);if(o instanceof Promise){o.then(a)}else{a(o)}}})()})}),and:new h(function(n){var r=this.get("list->array")(n);var e=this;return new Promise(function(n){var t;(function i(){function a(r){t=r;if(!t){n(false)}i()}var f=r.shift();if(typeof f==="undefined"){if(t){n(t)}else{n(false)}}else{var o=A(f,e);if(o instanceof Promise){o.then(a)}else{a(o)}}})()})}),"++":new h(function(n){var r=this.get(n.car)+1;this.set(n.car,r);return r}),"--":new h(function(n){var r=this.get(n.car)-1;this.set(n.car,r);return r})});function m(n){if(n.length===1){return n[0]}else{var r=[];var e=m(n.slice(1));for(var t=0;t a + b, + f2: (a, b) => new Pair(a, new Pair(b, nil)) + }, global_environment); + it('should return value', function() { + expect(exec('value', env)).toEqual(rand); + }); + it('should call function', function() { + expect(exec('(fun 1 2)', env)).toEqual(3); + expect(exec('(fun "foo" "bar")', env)).toEqual("foobar"); + }); + it('should set environment', function() { + exec('(define x "foobar")', env); + expect(exec('x', env)).toEqual("foobar"); + expect(exec('x')).toEqual(undefined); + }); + it('should create list', function() { + expect(exec('(cons 1 (cons 2 (cons 3 nil)))')) + .toEqual(Pair.fromArray([1, 2, 3])); + }); + describe('quote', function() { + it('should return literal list', function() { + expect(exec(`'(1 2 3 (4 5))`)).toEqual( + Pair.fromArray([1, 2, 3, [4, 5]]) + ); + }); + it('should return alist', function() { + expect(exec(`'((foo . 1) + (bar . 2.1) + (baz . "string") + (quux . /foo./g))`)).toEqual( + new Pair( + new Pair( + new Symbol('foo'), + 1 + ), + new Pair( + new Pair( + new Symbol('bar'), + 2.1 + ), + new Pair( + new Pair( + new Symbol('baz'), + "string" + ), + new Pair( + new Pair( + new Symbol('quux'), + /foo./g + ), + nil + ) + ) + ) + ) + ); + }); + }); + describe('quasiquote', function() { + it('should create list with function call', function() { + expect(exec('`(1 2 3 ,(fun 2 2) 5)', env)).toEqual( + Pair.fromArray([1, 2, 3, 4, 5]) + ); + }); + it('should create list with value', function() { + expect(exec('`(1 2 3 ,value 4)', env)).toEqual( + Pair.fromArray([1, 2, 3, rand, 4]) + ); + }); + it('should create single list using uquote-splice', function() { + expect(exec('`(1 2 3 ,@(f2 4 5) 6)', env)).toEqual( + Pair.fromArray([1, 2, 3, 4, 5, 6]) + ); + }); + it('should create single pair', function() { + [ + '`(1 . 2)', + '`(,(car (list 1 2 3)) . 2)', + '`(1 . ,(cadr (list 1 2 3)))', + '`(,(car (list 1 2 3)) . ,(cadr (list 1 2 3)))' + ].forEach((code) => { + expect(exec(code)).toEqual(new Pair(1, 2)); + }); + }); + it('should create list from pair syntax', function() { + expect(exec('`(,(car (list 1 2 3)) . (1 2 3))')).toEqual( + Pair.fromArray([1, 1, 2, 3]) + ); + }); + it('should create alist with values', function() { + expect(exec(`\`((1 . ,(car (list 1 2))) + (2 . ,(cadr (list 1 "foo"))))`)).toEqual( + new Pair( + new Pair(1, 1), + new Pair(new Pair(2, "foo"), nil)) + ); + expect(exec(`\`((,(car (list "foo")) . ,(car (list 1 2))) + (2 . ,(cadr (list 1 "foo"))))`)) + .toEqual(new Pair( + new Pair("foo", 1), + new Pair( + new Pair(2, "foo"), + nil + ))); + }); + it('should process nested backquote', function() { + expect(exec('`(1 2 3 ,(cadr `(1 ,(+ "foo" "bar") 3)) 4)')).toEqual( + Pair.fromArray([1, 2, 3, "foobar", 4]) + ); + }); + }); +}); + + +/* + +var code = parse(tokenize(` + (print (cons 1 (cons 2 (cons 3 nil)))) + (print (list 1 2 3 4)) + (print (car (list 1 2 3))) + (print (concat "hello" " " "world")) + (print (append (list 1 2 3) (list 10))) + (print nil) + (define x 10) + (print (* x x)) + (print (/ 1 2)) + (define l1 (list 1 2 3 4)) + (define l2 (append l1 (list 5 6))) + (print l1) + (print l2) + (defmacro (foo code) \`(print ,(string (car code)))) + (foo (name baz)) + (print \`(,(car (list "a" "b" "c")) 2 3)) + (print \`(,@(list 1 2 3))) + (print \`(1 2 3 ,@(list 4 5) 6)) + (defmacro (xxx code) \`(list 1 ,(car (cdr code)) 2)) + (print (xxx ("10" "20"))) + (if (== 10 20) (print "1 true") (print "1 false")) + (if (== 10 10) (print "2 true") (print "2 false")) + (print (concat "if " (if (== x 10) "10" "20"))) +`)); + +(function() { + var env = new Environment({}, global_environment); + var c = parse(tokenize([ + "(define x '(1 2 3))", + "(print x)", + "(print `(1 2 ,@x 4 5))", + "(print `(foo bar ,@x 4 5))" + ].join(' '))); + c.forEach(function(code) { + console.log(code.toString()); + try { + evaluate(code, env); + } catch (e) { + console.error(e.message); + } + }) +})(); +*/ diff --git a/index.js b/src/lips.js similarity index 74% rename from index.js rename to src/lips.js index 8fcb9f50..4d7342f0 100644 --- a/index.js +++ b/src/lips.js @@ -1,15 +1,20 @@ -/** - * Lips is Pretty Simple - version 0.1.0 +/**@license + * LIPS is Pretty Simple - version {{VER}} * * Copyright (c) 2018 Jakub Jankiewicz * Released under the MIT license * + * build: {{DATE}} */ +/* + * TODO: Pair.prototype.toObject = alist to Object + */ +"use strict"; /* global define, module, setTimeout, jQuery */ (function(root, factory) { if (typeof define === 'function' && define.amd) { // AMD. Register as an anonymous module. - define([], function () { + define([], function() { return (root.lips = factory()); }); } else if (typeof module === 'object' && module.exports) { @@ -25,7 +30,6 @@ // ---------------------------------------------------------------------- function parse_argument(arg) { function parse_string(string) { - console.log(JSON.stringify(string)); // remove quotes if before are even number of slashes // we don't remove slases becuase they are handled by JSON.parse //string = string.replace(/([^\\])['"]$/, '$1'); @@ -49,7 +53,7 @@ return parseInt(arg, 10); } else if (arg.match(float_re)) { return parseFloat(arg); - } else if (arg == 'nil') { + } else if (arg === 'nil') { return nil; } else { return new Symbol(arg); @@ -57,13 +61,20 @@ } // ---------------------------------------------------------------------- /* eslint-disable */ - var tokens_re = /("[^"\\]*(?:\\[\S\s][^"\\]*)*"|\/[^\/\\]*(?:\\[\S\s][^\/\\]*)*\/[gimy]*(?=\s|\(|\)|$)|\(|\)|'|\.|,@|,|`|[^(\s)]+)/gi; + var tokens_re = /("[^"\\]*(?:\\[\S\s][^"\\]*)*"|\/[^\/\\]*(?:\\[\S\s][^\/\\]*)*\/[gimy]*(?=\s|\(|\)|$)|;.*|\(|\)|'|\.|,@|,|`|[^(\s)]+)/gi; /* eslint-enable */ // ---------------------------------------------------------------------- function tokenize(str) { - return str.split(tokens_re).map(function(token) { - return token.trim(); - }).filter(Boolean); + return str.split('\n').map(function(line) { + return line.split(tokens_re).map(function(token) { + if (token.match(/^;/)) { + return null; + } + return token.trim(); + }).filter(Boolean); + }).reduce(function(arr, tokens) { + return arr.concat(tokens); + }, []); } // ---------------------------------------------------------------------- var specials = { @@ -79,15 +90,14 @@ function parse(tokens) { var stack = []; var result = []; - var list; var special = null; var special_tokens = Object.keys(specials); var special_forms = special_tokens.map(s => specials[s].name); var parents = 0; var first_value = false; - tokens.forEach(function(token, i) { - var top = stack[stack.length-1]; - if (special_tokens.indexOf(token) != -1) { + tokens.forEach(function(token) { + var top = stack[stack.length - 1]; + if (special_tokens.indexOf(token) !== -1) { special = token; } else if (token === '(') { first_value = true; @@ -98,30 +108,43 @@ } stack.push([]); } else if (token === '.' && !first_value) { - stack[stack.length-1] = Pair.fromArray(top); + stack[stack.length - 1] = Pair.fromArray(top); } else if (token === ')') { parents--; if (!stack.length) { - throw new Error('Unbalanced parenthesis 1'); + throw new Error('Unbalanced parenthesis'); } if (stack.length === 1) { result.push(stack.pop()); } else if (stack.length > 1) { var list = stack.pop(); - top = stack[stack.length-1]; - top.push(list); + top = stack[stack.length - 1]; + if (top instanceof Array) { + top.push(list); + } else if (top instanceof Pair) { + top.append(Pair.fromArray(list)); + } if (top instanceof Array && top[0] instanceof Symbol && special_forms.includes(top[0].name) && stack.length > 1) { stack.pop(); - if (stack[stack.length-1].length == 0) { - stack[stack.length-1] = top; + if (stack[stack.length - 1].length === 0) { + stack[stack.length - 1] = top; + } else if (stack[stack.length - 1] instanceof Pair) { + if (stack[stack.length - 1].cdr instanceof Pair) { + stack[stack.length - 1] = new Pair( + stack[stack.length - 1], + Pair.fromArray(top) + ); + } else { + stack[stack.length - 1].cdr = Pair.fromArray(top); + } } else { - stack[stack.length-1].push(top); + stack[stack.length - 1].push(top); } } } - if (parents == 0 && stack.length) { + if (parents === 0 && stack.length) { result.push(stack.pop()); } } else { @@ -133,7 +156,7 @@ } if (top instanceof Pair) { var node = top; - while(true) { + while (true) { if (node.cdr === nil) { node.cdr = value; break; @@ -149,7 +172,7 @@ } }); if (stack.length) { - throw new Error('Unbalanced parenthesis 2'); + throw new Error('Unbalanced parenthesis'); } return result.map((arg) => { if (arg instanceof Array) { @@ -167,19 +190,39 @@ Symbol.is = function(symbol, name) { return symbol instanceof Symbol && typeof name === 'string' && - symbol.name == name; + symbol.name === name; }; Symbol.prototype.toJSON = Symbol.prototype.toString = function() { //return '<#symbol \'' + this.name + '\'>'; return this.name; }; // ---------------------------------------------------------------------- + // :: Nil constructor with only once instance + // ---------------------------------------------------------------------- + function Nil() {} + Nil.prototype.toString = function() { + return 'nil'; + }; + var nil = new Nil(); + // ---------------------------------------------------------------------- // :: Pair constructor // ---------------------------------------------------------------------- function Pair(car, cdr) { this.car = car; this.cdr = cdr; } + Pair.prototype.length = function() { + var len = 0; + var node = this; + while (true) { + if (node === nil) { + break; + } + len++; + node = node.cdr; + } + return len; + }; Pair.prototype.clone = function() { var cdr; if (this.cdr === nil) { @@ -190,7 +233,7 @@ return new Pair(this.car, cdr); }; Pair.prototype.toArray = function() { - if (this.cdr === nil && this.car == nil) { + if (this.cdr === nil && this.car === nil) { return []; } var result = []; @@ -208,7 +251,10 @@ if (array instanceof Pair) { return array; } - if (array.length == 0) { + if (array.length && !array instanceof Array) { + array = [...array]; + } + if (array.length === 0) { return new Pair(nil, nil); } else { var car; @@ -224,6 +270,63 @@ } } }; + Pair.prototype.toObject = function() { + var node = this; + var result = {}; + while (true) { + if (node instanceof Pair && node.car instanceof Pair) { + var pair = node.car; + var name = pair.car; + if (name instanceof Symbol) { + name = name.name; + } + result[name] = pair.cdr; + node = node.cdr; + } else { + break; + } + } + return result; + }; + Pair.fromPairs = function(array) { + return array.reduce((list, pair) => { + return new Pair( + new Pair( + new Symbol(pair[0]), + pair[1] + ), + list + ); + }, nil); + }; + Pair.fromObject = function(obj) { + var array = Object.keys(obj).map((key) => [key, obj[key]]); + return Pair.fromPairs(array); + }; + Pair.prototype.reduce = function(fn) { + var node = this; + var result = nil; + while (true) { + if (node !== nil) { + result = fn(result, node.car); + node = node.cdr; + } else { + break; + } + } + return result; + }; + Pair.prototype.reverse = function() { + var node = this; + var prev = nil; + while (node !== nil) { + var next = node.cdr; + node.cdr = prev; + prev = node; + node = next; + } + return prev; + }; Pair.prototype.transform = function(fn) { var visited = []; function recur(pair) { @@ -250,7 +353,7 @@ }; Pair.prototype.toString = function() { var arr = ['(']; - if (typeof this.car == 'string') { + if (typeof this.car === 'string') { arr.push(JSON.stringify(this.car)); } else if (typeof this.car !== 'undefined') { arr.push(this.car); @@ -259,14 +362,21 @@ arr.push(' '); arr.push(this.cdr.toString().replace(/^\(|\)$/g, '')); } else if (typeof this.cdr !== 'undefined' && this.cdr !== nil) { - arr = arr.concat([' . ', this.cdr]); + if (typeof this.cdr === 'string') { + arr = arr.concat([' . ', JSON.stringify(this.cdr)]); + } else { + arr = arr.concat([' . ', this.cdr]); + } } arr.push(')'); return arr.join(''); }; Pair.prototype.append = function(pair) { + if (pair instanceof Array) { + return this.append(Pair.fromArray(pair)); + } var p = this; - while(true) { + while (true) { if (p instanceof Pair && p.cdr !== nil) { p = p.cdr; } else { @@ -277,13 +387,6 @@ return this; }; - // ---------------------------------------------------------------------- - // :: Nil constructor with only once instance - // ---------------------------------------------------------------------- - function Nil() {} - Nil.prototype.toString = function() { return 'nil'; }; - var nil = new Nil(); - // ---------------------------------------------------------------------- // :: Macro constructor // ---------------------------------------------------------------------- @@ -306,7 +409,7 @@ if (typeof this.env[symbol.name] !== 'undefined') { return this.env[symbol.name]; } - } else if (typeof symbol == 'string') { + } else if (typeof symbol === 'string') { if (typeof this.env[symbol] !== 'undefined') { return this.env[symbol]; } @@ -318,7 +421,7 @@ if (typeof window[symbol.name] !== 'undefined') { return window[symbol.name]; } - } else if (typeof symbol == 'string') { + } else if (typeof symbol === 'string') { if (typeof window[symbol] !== 'undefined') { return window[symbol]; } @@ -347,6 +450,32 @@ return new Quote(evaluate(output, env)); }); } + var gensym = (function() { + var count = 0; + return function() { + count++; + return new Symbol('#' + count); + }; + })(); + function request(url, method = 'GET', headers = {}, data = null) { + var xhr = new XMLHttpRequest(); + xhr.open(method, url, true); + Object.keys(headers).forEach(name => { + xhr.setRequestHeader(name, headers[name]); + }); + return new Promise((resolve) => { + xhr.onreadystatechange = function() { + if (xhr.readyState === 4 && xhr.status === 200) { + resolve(xhr.responseText); + } + }; + if (data !== null) { + xhr.send(data); + } else { + xhr.send(); + } + }); + } var global_env = new Environment({ nil: nil, window: window, @@ -358,7 +487,7 @@ } }, stdin: { - read: function(arg) { + read: function() { return new Promise((resolve) => { resolve(prompt('')); }); @@ -370,14 +499,17 @@ car: function(list) { if (list instanceof Pair) { return list.car; + } else { + throw new Error('argument to car need to be a list'); } }, cdr: function(list) { if (list instanceof Pair) { return list.cdr; + } else { + throw new Error('argument to cdr need to be a list'); } }, - 'set-car': function(slot, value) { slot.car = value; }, @@ -387,7 +519,7 @@ assoc: function(list, key) { var node = list; var name = key instanceof Symbol ? key.name : key; - while(true) { + while (true) { var car = node.car.car; if (car instanceof Symbol && car.name === name || car.name === name) { @@ -397,24 +529,62 @@ } } }, + gensym: gensym, + load: function(file) { + request(file).then((code) => { + this.get('eval')(this.get('read')(code)); + }); + }, + 'while': new Macro(function(code) { + var self = this; + var begin = new Pair( + new Symbol('begin'), + code.cdr + ); + return new Promise((resolve) => { + var result; + (function loop() { + function next(cond) { + if (cond) { + var value = evaluate(begin, self); + if (value instanceof Promise) { + value.then((value) => { + result = value; + loop(); + }); + } else { + result = value; + loop(); + } + } else { + resolve(result); + } + } + var cond = evaluate(code.car, self); + if (cond instanceof Promise) { + cond.then(next); + } else { + next(cond); + } + })(); + }); + }), 'if': new Macro(function(code) { var resolve = (cond) => { if (cond) { var true_value = evaluate(code.cdr.car, this); - if (typeof true_value === 'undefiend') { + if (typeof true_value === 'undefined') { return; } return true_value; - } else { - if (code.cdr.cdr.car instanceof Pair) { - var false_value = evaluate(code.cdr.cdr.car, this); - if (typeof false_value === 'udefined') { - return false; - } - return false_value; - } else { + } else if (code.cdr.cdr.car instanceof Pair) { + var false_value = evaluate(code.cdr.cdr.car, this); + if (typeof false_value === 'undefined') { return false; } + return false_value; + } else { + return false; } }; var cond = evaluate(code.car, this); @@ -465,6 +635,9 @@ this.env[code.car.name] = value; } }), + set: function(obj, key, value) { + obj[key] = value; + }, 'eval': function(code) { if (code instanceof Pair) { return evaluate(code, this); @@ -481,7 +654,6 @@ return (...args) => { var env = new Environment({}, this); var name = code.car; - var arg = code; var i = 0; var value; while (true) { @@ -504,13 +676,12 @@ }), defmacro: new Macro(function(macro) { if (macro.car.car instanceof Symbol) { - var this_env = this; this.env[macro.car.car.name] = new Macro(function(code) { var env = new Environment({}, this); var name = macro.car.cdr; var arg = code; while (true) { - if (name.car !== nil && arg.car != nil) { + if (name.car !== nil && arg.car !== nil) { env.env[name.car.name] = arg.car; } if (name.cdr === nil) { @@ -524,30 +695,15 @@ } }), quote: new Macro(function(arg) { - var env = this; - function recur(pair) { - if (pair instanceof Pair) { - var car = pair.car; - if (car instanceof Pair) { - car = recur(car); - } - var cdr = pair.cdr; - if (cdr instanceof Pair) { - cdr = recur(cdr); - } - return new Pair(car, cdr); - } - return pair; - } return new Quote(arg.car); }), quasiquote: new Macro(function(arg) { - var env = this; + var self = this; function recur(pair) { if (pair instanceof Pair) { var eval_pair; if (Symbol.is(pair.car.car, 'unquote-splicing')) { - eval_pair = evaluate(pair.car.cdr.car, env); + eval_pair = evaluate(pair.car.cdr.car, self); if (!eval_pair instanceof Pair) { throw new Error('Value of unquote-splicing need' + ' to be pair'); @@ -565,7 +721,7 @@ return eval_pair; } if (Symbol.is(pair.car, 'unquote-splicing')) { - eval_pair = evaluate(pair.cdr.car, env); + eval_pair = evaluate(pair.cdr.car, self); if (!eval_pair instanceof Pair) { throw new Error('Value of unquote-splicing' + ' need to be pair'); @@ -573,7 +729,14 @@ return eval_pair; } if (Symbol.is(pair.car, 'unquote')) { - return evaluate(pair.cdr.car, env); + if (pair.cdr.cdr !== nil) { + return new Pair( + evaluate(pair.cdr.car, self), + pair.cdr.cdr + ); + } else { + return evaluate(pair.cdr.car, self); + } } var car = pair.car; if (car instanceof Pair) { @@ -596,7 +759,6 @@ return this.get('append!')(list.clone(), item); }, 'append!': function(list, item) { - var parent; var node = list; while (true) { if (node.cdr === nil) { @@ -618,12 +780,31 @@ obj instanceof jQuery.fn.init) { return '<#jQuery>'; } - if (typeof obj == 'undefined') { + if (obj instanceof Macro) { + //return '<#Macro>'; + } + if (typeof obj === 'undefined') { return '<#undefined>'; } - if (typeof obj == 'function') { + if (typeof obj === 'function') { return '<#function>'; } + if (obj === nil) { + return 'nil'; + } + if (obj instanceof Array || obj === null) { + return JSON.stringify(obj); + } + if (obj instanceof Pair) { + return obj.toString(); + } + if (typeof obj === 'object') { + var name = obj.constructor.name; + if (name !== '') { + return '<#' + name + '>'; + } + return '<#Object>'; + } if (typeof obj !== 'string') { return obj.toString(); } @@ -645,7 +826,7 @@ }, '.': function(obj, arg) { var name = arg instanceof Symbol ? arg.name : arg; - var value = obj[arg]; + var value = obj[name]; if (typeof value === 'function') { return value.bind(obj); } @@ -701,24 +882,30 @@ return nil; } }, + reduce: function(fn, list) { + var arr = this.get('list->array')(list); + return arr.reduce((list, item) => { + return fn(list, item); + }, nil); + }, // math functions - '*': function() { - return [].reduce.call(arguments, function(a, b) { + '*': function(...args) { + return args.reduce(function(a, b) { return a * b; - }, 1); + }); }, - '+': function() { - return [].reduce.call(arguments, function(a, b) { + '+': function(...args) { + return args.reduce(function(a, b) { return a + b; - }, 0); + }); }, - '-': function() { - return [].reduce.call(arguments, function(a, b) { + '-': function(...args) { + return args.reduce(function(a, b) { return a - b; }); }, - '/': function() { - return [].reduce.call(arguments, function(a, b) { + '/': function(...args) { + return args.reduce(function(a, b) { return a / b; }); }, @@ -727,7 +914,7 @@ }, // Booleans "==": function(a, b) { - return a == b; + return a === b; }, '>': function(a, b) { return a > b; @@ -743,7 +930,7 @@ }, or: new Macro(function(code) { var args = this.get('list->array')(code); - var env = this; + var self = this; return new Promise(function(resolve) { var result; (function loop() { @@ -762,7 +949,7 @@ resolve(false); } } else { - var value = evaluate(arg, env); + var value = evaluate(arg, self); if (value instanceof Promise) { value.then(next); } else { @@ -774,7 +961,7 @@ }), and: new Macro(function(code) { var args = this.get('list->array')(code); - var env = this; + var self = this; return new Promise(function(resolve) { var result; (function loop() { @@ -793,7 +980,7 @@ resolve(false); } } else { - var value = evaluate(arg, env); + var value = evaluate(arg, self); if (value instanceof Promise) { value.then(next); } else { @@ -802,13 +989,23 @@ } })(); }); + }), + '++': new Macro(function(code) { + var value = this.get(code.car) + 1; + this.set(code.car, value); + return value; + }), + '--': new Macro(function(code) { + var value = this.get(code.car) - 1; + this.set(code.car, value); + return value; }) }); // ---------------------------------------------------------------------- // source: https://stackoverflow.com/a/4331218/387194 function allPossibleCases(arr) { - if (arr.length == 1) { + if (arr.length === 1) { return arr[0]; } else { var result = []; @@ -840,11 +1037,10 @@ combinations(['d', 'a'], 2, 5).forEach((spec) => { var chars = spec.split('').reverse(); global_env.set('c' + spec + 'r', function(arg) { - var result = arg; return chars.reduce(function(list, type) { if (type === 'a') { return list.car; - } else if (type === 'd') { + } else { return list.cdr; } }, arg); @@ -916,9 +1112,9 @@ function balanced(code) { var re = /[()]/; var parenthesis = tokenize(code).filter((token) => token.match(re)); - var open = parenthesis.filter(p => p == ')'); - var close = parenthesis.filter(p => p == '('); - return open.length == close.length; + var open = parenthesis.filter(p => p === ')'); + var close = parenthesis.filter(p => p === '('); + return open.length === close.length; } // -------------------------------------- Pair.unDry = function(value) { @@ -927,8 +1123,8 @@ Pair.prototype.toDry = function() { return { value: { - car : this.car, - cdr : this.cdr + car: this.car, + cdr: this.cdr } }; }; @@ -951,6 +1147,7 @@ return new Symbol(value.name); }; return { + version: '{{VER}}', parse: parse, tokenize: tokenize, evaluate: evaluate, diff --git a/templates/Makefile b/templates/Makefile new file mode 100644 index 00000000..d1ca57da --- /dev/null +++ b/templates/Makefile @@ -0,0 +1,58 @@ +.PHONY: publish test coveralls lint + +VERSION={{VERSION}} +BRANCH=`git branch | grep '^*' | sed 's/* //'` +DATE=`date -uR` +SPEC_CHECKSUM=`md5sum spec/lips.spec.js | cut -d' ' -f 1` +COMMIT=`git log -n 1 | grep commit | sed 's/commit //'` + +GIT=git +SED=sed +RM=rm +TEST=test +CAT=cat +NPM=npm +ESLINT=./node_modules/.bin/eslint +COVERALLS=./node_modules/.bin/coveralls +JEST=./node_modules/.bin/jest +UGLIFY=./node_modules/.bin/uglifyjs +BABEL=./node_modules/.bin/babel + + +ALL: Makefile .$(VERSION) dist/lips.js dist/lips.min.js README.md package.json + +dist/lips.js: src/lips.js .$(VERSION) + $(GIT) branch | grep '* devel' > /dev/null && $(SED) -e "s/{{VER}}/DEV/g" -e "s/{{DATE}}/$(DATE)/g" src/lips.js > dist/lips.tmp.js || $(SED) -e "s/{{VER}}/$(VERSION)/g" -e "s/{{DATE}}/$(DATE)/g" src/lips.js > dist/lips.tmp.js + $(BABEL) dist/lips.tmp.js > dist/lips.js + $(RM) dist/lips.tmp.js + +dist/lips.min.js: dist/lips.js .$(VERSION) + $(UGLIFY) -o dist/lips.min.js --comments --mangle -- dist/lips.js + +Makefile: templates/Makefile + $(SED) -e "s/{{VER""SION}}/"$(VERSION)"/" templates/Makefile > Makefile + +package.json: templates/package.json .$(VERSION) + $(SED) -e "s/{{VER}}/"$(VERSION)"/" templates/package.json > package.json || true + +README.md: templates/README.md + $(GIT) branch | grep '* devel' > /dev/null && $(SED) -e "s/{{VER}}/DEV/g" -e \ + "s/{{BRANCH}}/$(BRANCH)/g" -e "s/{{CHECKSUM}}/$(SPEC_CHECKSUM)/g" \ + -e "s/{{COMMIT}}/$(COMMIT)/g" < templates/README.md > README.md || \ + $(SED) -e "s/{{VER}}/$(VERSION)/g" -e "s/{{BRANCH}}/$(BRANCH)/g" -e \ + "s/{{CHECKSUM}}/$(SPEC_CHECKSUM)/g" -e "s/{{COMMIT}}/$(COMMIT)/g" < templates/README.md > README.md + +.$(VERSION): Makefile + touch .$(VERSION) + +publish: + $(NPM) publish --access=public + +test: + $(JEST) + +coveralls: + $(CAT) ./coverage/lcov.info | $(COVERALLS) + +lint: + $(ESLINT) src/lips.js spec/lips.spec.js diff --git a/templates/README.md b/templates/README.md new file mode 100644 index 00000000..3f7300a5 --- /dev/null +++ b/templates/README.md @@ -0,0 +1,259 @@ +## LIPS is Pretty Simple + +[![npm](https://img.shields.io/badge/npm-{{VER}}-blue.svg)](https://www.npmjs.com/package/@jcubic/lips) +[![travis](https://travis-ci.org/jcubic/jquery.terminal.svg?branch={{BRANCH}}&{{COMMIT}})](https://travis-ci.org/jcubic/jquery.terminal) +[![Coverage Status](https://coveralls.io/repos/github/jcubic/lips/badge.svg?branch={{BRANCH}}&{{CHECKSUM}})](https://coveralls.io/github/jcubic/lips?branch={{BRANCH}}) + + + +LIPS is very simple Lisp, similar to Scheme writen in JavaScript. + +[Demo](https://codepen.io/jcubic/full/LQBaaV/) + +## Installation + +use npm + +``` +npm install @jcubic/lips +``` + +then include the file in script tag, You can grab the version from unpkg.com + +``` +https://unpkg.com/@jcubic/lips +``` + +or from rawgit + +``` +https://cdn.rawgit.com/jcubic/lips/master/index.js +``` + +## Usage + +```javascript +var {parse, tokenize, evaluate} = require('@jcubic/lips'); + +parse(tokenize(string)).forEach(function(code) { + evaluate(code); +}); +``` + +`evaluate` function also accept second argument, which is Environment. +By default it's `lips.global_environment`. You can use it if you want to +have separated instances of the interpreter. + +You can create new environment using: + +```javascript +var env = new Environment({}, lips.global_environment); +``` + +First argument is an object with functions, macros and varibles (see Extending LIPS at the end). +Second argument is parent environment, you need to use global environment (or other that extend global) +otherwise you will not have any functions. + +## What's in + +### variables and functions + +```scheme +(define x 10) +(define square (lambda (x) (* x x))) +(define (square x) (* x x)) +``` + +### List operications + +```scheme +(cons 1 2) +(cons 1 (cons 2 nil)) +(list 1 2 3 4) +'(1 2 3 4) + +(let ((lst '(1 2 (3 4 5 6)))) + (print (car lst)) + (print (cadaddr lst))) +``` + +all functions that match this regex `c[ad]{2,5}r` are defined. + +### ALists + +```scheme +(let ((l '((foo . "lorem") (bar "ipsum")))) + (set-cdr (assoc l 'foo) "hello") + (set-cdr (assoc l 'bar) "world") + (print l)) +``` + +### Flow constructs + +```scheme +(let ((x 5)) + (while (> (-- x) 0) (print x))) +``` + +same as in JS + +```scheme +(if (== "10" 10) + (print "equal")) + +(let ((x 10)) + (if (and (> x 1) (< x 20)) + (begin + (print "this is x > 1") + (print "and x < 20")))) +``` + +### eval + +```scheme +(eval (read "(print \"hello\")")) +``` + +### Lisp Macros + +```scheme +(defmacro (foo x) `(1 2 ,@(car x) 3 4)) +``` + +### Async code + +```scheme +(eval (read)) +``` + +then type S-Expression like `(print 10)`. If function return Promise +the execution is paused and restored when Promise is resolved + + +### Access JavaScript functions and objects + +```scheme +((. window "alert") "hello") +((. console "log") "hello") +``` + +If object is not found in environment, then window object is tested for +presense of the element. + +You can execute jQuery functions + +```scheme +(let* ((term ($ ".terminal"))) + ((. term "css") "background" "red")) +``` + +function `$` is available because it's in window object. + +or operate on strings + +``` +((. "foo bar baz" "replace") /^[a-z]+/g "baz") + +(let ((match (. "foo bar baz" "match"))) + (array->list (match /([a-z]+)/g))) +``` + +### Mapping, filtering and reducing + +```scheme +(map car (list + (cons "a" 2) + (cons "b" 3))) + +(filter odd (list 1 2 3 4 5)) + +(filter (lambda (x) + (== (% x 2) 0)) + (list 1 2 3 4 5)) + +(define (reverse list) + (reduce (lambda (list x) (cons x list)) list)) + +(reverse '(1 2 3 4)) +``` + +### Working with arrays + +You can modify array with `set` function and to get the value of the array you can use `.` dot function. + +```scheme +(let ((arr (list->array '(1 2 3 4)))) + (set arr 0 2) + (print (array->list arr))) + +(let* ((div ((. document "querySelectorAll") ".terminal-output > div")) + (len (. div "length")) + (i 0)) + (while (< i len) + (print (. (. div i) "innerHTML")) + (++ i))) +``` + +this equivalent of JavaScript code: + +```javascript +var div = document.querySelectorAll(".terminal div"); +var len = div.length; +var i = 0; +while (i < len) { + console.log(div[i].innerHTML); + ++i; +} +``` + +### Math and boolean operators + +`< > => <= ++ -- + - * / % and or` + +## Extending LIPS + +to create new function from JavaScript you can use: + +```javascript +env.set('replace', function(re, sub, string) { + return string.replace(re, sub); +}); +``` + +then you can use it in LIPS: + +``` +(replace /(foo|bar)/g "hello" "foo bar baz") +``` + +To define a macro in javascript you can use Macro constructor that accept +single function argument, that should return lisp code (instance of Pair) + +```javascript + +var {Macro, Pair, Symbol, nil} = lips; + +env.set('quote-car', new Macro(function(code) { + return Pair.fromArray([new Symbol('quote'), code.car.car]); +})); +``` + +and you can execute this macro in LIPS: + +```scheme +(quote-car (foo bar baz)) +``` + +it will return first symbol and not execute it as function foo. + +if you want to create macro like quasiquote, the returned code need to be wrapped with +Quote instance. + +When creating macros in JavaScript you can use helper `Pair.fromArray()` +and `code.toArray()`. + +## License + +Released under [MIT](http://opensource.org/licenses/MIT) license + +Copyright (c) 2018 [Jakub Jankiewicz](http://jcubic.pl/jakub-jankiewicz) diff --git a/templates/package.json b/templates/package.json new file mode 100644 index 00000000..a5e0c2b2 --- /dev/null +++ b/templates/package.json @@ -0,0 +1,195 @@ +{ + "name": "@jcubic/lips", + "version": "{{VER}}", + "description": "Simple Scheme Like Lisp in JavaScript", + "main": "dist/lips.js", + "scripts": {}, + "repository": { + "type": "git", + "url": "git+https://github.com/jcubic/lips.git" + }, + "keywords": [ + "lisp", + "scheme", + "language", + "interpreter" + ], + "author": "Jakub Jankiewicz (http://jcubic.pl/jakub-jankiewicz/)", + "license": "MIT", + "bugs": { + "url": "https://github.com/jcubic/lips/issues" + }, + "homepage": "https://github.com/jcubic/lips#readme", + "eslintConfig": { + "parserOptions": { + "ecmaVersion": 6, + "sourceType": "script", + "ecmaFeatures": {} + }, + "env": { + "browser": true, + "jest": true, + "node": true + }, + "globals": { + "Promise": true + }, + "rules": { + "eqeqeq": "error", + "curly": "error", + "no-unreachable": "error", + "valid-typeof": "error", + "no-unexpected-multiline": "error", + "no-regex-spaces": "error", + "no-irregular-whitespace": "error", + "no-invalid-regexp": "error", + "no-inner-declarations": "error", + "no-func-assign": "error", + "no-extra-semi": "error", + "no-extra-boolean-cast": "error", + "no-debugger": "error", + "no-dupe-args": "error", + "no-dupe-keys": "error", + "no-duplicate-case": "error", + "no-empty-character-class": "error", + "no-ex-assign": "error", + "array-callback-return": "error", + "no-case-declarations": "error", + "guard-for-in": "error", + "no-caller": "error", + "no-eval": "error", + "no-extend-native": "error", + "no-extra-bind": "error", + "no-fallthrough": "error", + "no-global-assign": "error", + "no-implicit-globals": "error", + "no-implied-eval": "error", + "no-multi-spaces": "error", + "no-new-wrappers": "error", + "no-redeclare": "error", + "no-self-assign": "error", + "no-return-assign": "error", + "no-self-compare": "error", + "no-throw-literal": "error", + "no-unused-labels": "error", + "no-useless-call": "error", + "no-useless-escape": "error", + "no-void": "error", + "no-with": "error", + "radix": "error", + "wrap-iife": [ + "error", + "inside" + ], + "yoda": [ + "error", + "never" + ], + "no-catch-shadow": "error", + "no-delete-var": "error", + "no-label-var": "error", + "no-undef-init": "error", + "no-unused-vars": "error", + "no-undef": "error", + "comma-style": [ + "error", + "last" + ], + "comma-dangle": [ + "error", + "never" + ], + "comma-spacing": [ + "error", + { + "before": false, + "after": true + } + ], + "computed-property-spacing": [ + "error", + "never" + ], + "eol-last": [ + "error", + "always" + ], + "func-call-spacing": [ + "error", + "never" + ], + "key-spacing": [ + "error", + { + "beforeColon": false, + "afterColon": true, + "mode": "strict" + } + ], + "max-len": [ + "error", + 85 + ], + "max-statements-per-line": "error", + "new-parens": "error", + "no-array-constructor": "error", + "no-lonely-if": "error", + "no-mixed-spaces-and-tabs": "error", + "no-multiple-empty-lines": "error", + "no-new-object": "error", + "no-tabs": "error", + "no-trailing-spaces": "error", + "no-whitespace-before-property": "error", + "object-curly-spacing": [ + "error", + "never" + ], + "space-before-blocks": "error", + "keyword-spacing": [ + "error", + { + "before": true, + "after": true + } + ], + "space-in-parens": [ + "error", + "never" + ], + "space-infix-ops": "error", + "space-before-function-paren": [ + "error", + "never" + ], + "complexity": [ + "error", + { + "max": 26 + } + ], + "indent": [ + "error", + 4, + { + "SwitchCase": 1 + } + ], + "linebreak-style": [ + "error", + "unix" + ], + "semi": [ + "error", + "always" + ] + } + }, + "devDependencies": { + "babel-cli": "^6.26.0", + "babel-preset-env": "^1.6.1", + "coveralls": "^3.0.0", + "eslint": "^4.18.1", + "jest": "^22.4.2", + "uglify-js": "^3.3.12" + } +} diff --git a/version b/version new file mode 100755 index 00000000..c0677b4b --- /dev/null +++ b/version @@ -0,0 +1,10 @@ +#!/bin/bash + +# Display current version or update version if used with version as argument + +VERSION=`grep VERSION= Makefile | sed -e 's/VERSION=\(.*\)/\1/'` +if [ -z "$1" ]; then + echo $VERSION +elif [ "$1" != "$VERSION" ]; then + sed -e "s/{{VERSION}}/"$1"/" templates/Makefile > Makefile +fi