Created
May 25, 2017 10:08
-
-
Save OrangeBacon/10777fedd052d61cc08163e76d170bbc to your computer and use it in GitHub Desktop.
A Programming Language
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<div id='editor'></div> | |
<div id='output'></div> | |
<div id='run'>run</div> | |
<div id="example" hidden> | |
#comment | |
/* | |
this is also a comment | |
*/ | |
# all function arguments are required //TODO | |
def print-range = fn(start,stop) -> { | |
if start <= stop then { | |
print(start); | |
if start + 1 <= stop then { | |
print(", "); | |
sleep(250); | |
print-range(start + 1, stop); | |
} else print("\n"); | |
}; | |
}; | |
print-range(1,5); | |
def max; | |
/* | |
everything is an expression | |
this returns 3.14 | |
*/ | |
def pi = 3.14; | |
def function = fn(x) -> { | |
# return = last expression | |
x; | |
}; | |
def string = "hello world" | |
|> capitalise | |
|> increment | |
|> reverse; | |
/* | |
equivalent to: | |
String.fromCharCode(...[...string.toUpperCase()].map(x=>x.charCodeAt(0)+1).reverse()) | |
in JavaScript since strings act as arrays | |
*/ | |
/* | |
closure | |
fn -> {} is short for fn() -> {} | |
variables in closure are not accessible outside | |
*/ | |
(fn->{ | |
# iife | |
# code | |
})(); | |
# or simpler | |
{ | |
# code | |
}; | |
/* | |
To make a vaiable mutable you must add the keyword mut; | |
by default let is immutable. | |
*/ | |
def mut level = 0; | |
def with-yield = fn(mut func) -> { | |
let (mut yield) -> { | |
yield = fn(val) -> { | |
shift(fn(SK) -> { | |
func = SK; | |
val; ## return val | |
}); | |
}; | |
fn(val) -> { | |
reset( fn() -> func(val || yield) ); | |
}; | |
} | |
}; | |
def foo = with-yield(fn(yield) -> { | |
yield(1); | |
yield(2); | |
yield(3); | |
"DONE"; | |
}); | |
time(fn->{ | |
print(foo()); | |
print(foo()); | |
print(foo()); | |
print(foo()); | |
print(foo()); | |
}); | |
</div> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* jshint esversion:6 */ | |
/**\ | |
|**| TODO | |
|**| | |
|**| Macros | |
|**| Function/operator overloading | |
|**| arrays | |
|**| objects | |
|**| type checking | |
|**| modules | |
|**| jscodegen | |
|**| standard library | |
|**| rest parameters | |
|**| proper editor | |
|**| import/export | |
|**| optional arguments | |
\**/ | |
// parsing | |
function InputStream(input, editor = false) { | |
let pos = 0; | |
let line = 1; | |
let col = 0; | |
return { | |
next, | |
peek, | |
eof, | |
err, | |
peekData | |
}; | |
function next() { | |
let ch = input.charAt(pos); | |
let val = { | |
value: ch, | |
line, | |
col, | |
pos | |
}; | |
pos++; | |
if (ch == "\n") { | |
line++; | |
col = 0; | |
} else { | |
col++; | |
} | |
return val; | |
} | |
function peek(x = 0) { | |
return input.charAt(pos + x); | |
} | |
function peekData(x = 0) { | |
let ch = input.charAt(pos + x); | |
let val = { | |
value: ch, | |
line, | |
col, | |
pos | |
}; | |
for (let i = 0; i < x; i++) { | |
if (input.charAt(val.pos) == "\n") { | |
val.line++; | |
val.col = 0; | |
} else { | |
val.col++; | |
} | |
val.pos++; | |
} | |
return val; | |
} | |
function eof() { | |
return peek() === ""; | |
} | |
function err(type, msg) { | |
if (editor) { | |
let el = document.createElement("span"); | |
el.setAttribute('class', 'cm-error'); | |
el.style.display = 'block'; | |
el.innerText = `${type}: ${msg} at line ${line}:${col}`; | |
editor.widgets.push(editor.addLineWidget(line - 1, el, { | |
noHScroll: true | |
})); | |
} | |
throw new Error(`${type}: ${msg} at line ${line}:${col}`); | |
} | |
} | |
let TokenStreamCommon = { | |
keywords: ["if", "then", "else", "fn", "->", "true", "false", "null", "let", "mut", "fn->", "def"], | |
is_whitespace(ch) { | |
return " \t\n".indexOf(ch) >= 0; | |
}, | |
is_digit(ch) { | |
return /[0-9]/i.test(ch); | |
}, | |
is_id_start(ch) { | |
return /[a-z_]/i.test(ch); | |
}, | |
is_id(ch) { | |
return TokenStreamCommon.is_id_start(ch) || "?!-<>=0123456789".indexOf(ch) >= 0; | |
}, | |
is_keyword(x) { | |
return TokenStreamCommon.keywords.indexOf(x) >= 0; | |
}, | |
is_punc(ch) { | |
return ",;[]{}()".indexOf(ch) >= 0; | |
}, | |
is_op(ch) { | |
return "+-*/%=&|<>!^".indexOf(ch) >= 0; | |
} | |
}; | |
function TokenStream(input) { | |
let current = null; | |
let c = TokenStreamCommon; | |
return { | |
next, | |
peek, | |
eof, | |
err: input.err | |
}; | |
function read_while(pred) { | |
let str = ''; | |
while (pred(input.peek(), input.peek(1)) && !input.eof()) { | |
str += input.next().value; | |
} | |
return str; | |
} | |
function read_comment() { | |
let next = input.next(); | |
let varient; | |
if (next.value == "/") { | |
input.next(); | |
varient = "//"; | |
} else { | |
varient = "#"; | |
} | |
let val = read_while(ch => ch !== '\n'); | |
return { | |
type: "comment", | |
varient: varient, | |
value: val, | |
line: next.line, | |
col: next.col | |
}; | |
} | |
function read_comment_m() { | |
let next = input.next(); | |
input.next(); | |
let val = read_while((ch1, ch2) => ch1 !== '*' && ch2 !== '/'); | |
input.next(); | |
input.next(); | |
return { | |
type: "comment", | |
varient: "multiline", | |
value: val, | |
line: next.line, | |
col: next.col | |
}; | |
} | |
function read_escaped(end) { | |
let escaped = false; | |
let str = ""; | |
while (!input.eof()) { | |
let next = input.next(); | |
let ch = next.value; | |
if (escaped) { | |
if (ch == "n") { | |
str += "\n"; | |
} else { | |
str += ch; | |
} | |
escaped = false; | |
} else if (ch == "\\") { | |
escaped = true; | |
} else if (ch == end) { | |
break; | |
} else { | |
str += ch; | |
} | |
} | |
return str; | |
} | |
function read_string() { | |
let next = input.next(); | |
let varient = next.value == "'" ? "'" : '"'; | |
let val = read_escaped(varient); | |
return { | |
type: "string", | |
varient: varient, | |
value: val, | |
line: next.line, | |
col: next.col | |
}; | |
} | |
function read_number() { | |
let next = input.peekData(); | |
let has_dot = false; | |
let number = read_while(function(ch) { | |
if (ch == ".") { | |
if (has_dot) return false; | |
has_dot = true; | |
return true; | |
} | |
return c.is_digit(ch); | |
}); | |
return { | |
type: "number", | |
value: number, | |
line: next.line, | |
col: next.col, | |
}; | |
} | |
function read_ident() { | |
let next = input.peekData(); | |
let id = read_while(c.is_id); | |
return { | |
type: c.is_keyword(id) ? "kw" : "var", | |
value: id, | |
line: next.line, | |
col: next.col | |
}; | |
} | |
function read_punc() { | |
let next = input.next(); | |
return { | |
type: "punc", | |
value: next.value, | |
line: next.line, | |
col: next.col | |
}; | |
} | |
function read_op() { | |
let next = input.peekData(); | |
return { | |
type: "op", | |
value: read_while(c.is_op), | |
line: next.line, | |
col: next.col | |
}; | |
} | |
function read_mod() { | |
let next = input.next(); | |
return { | |
type: "mod", | |
value: next.value, | |
line: next.line, | |
col: next.col | |
}; | |
} | |
function read_next() { | |
read_while(c.is_whitespace); | |
if (input.eof()) return null; | |
var ch = input.peek(); | |
var next = input.peek(1); | |
if (ch == "#") { | |
read_comment(); | |
return read_next(); | |
} | |
if (ch == "/" && next == "*") { | |
read_comment_m(); | |
return read_next(); | |
} | |
if (ch == "/" && next == "/") { | |
read_comment(); | |
return read_next(); | |
} | |
if (ch == "'" || ch == '"') return read_string(); | |
if (c.is_digit(ch)) return read_number(); | |
if (c.is_id_start(ch) || (ch == "-" && next == ">")) return read_ident(); | |
if (c.is_punc(ch)) return read_punc(); | |
if (c.is_op(ch)) return read_op(); | |
if (ch == "?") return read_mod(); | |
input.err("Lexing Error", `Unexpected character "${ch}"`); | |
} | |
function peek() { | |
return current || (current = read_next()); | |
} | |
function next() { | |
var tok = current; | |
current = null; | |
return tok || read_next(); | |
} | |
function eof() { | |
return peek() === null; | |
} | |
} | |
function parse(input) { | |
let FALSE = { | |
type: "bool", | |
value: false | |
}; | |
let PRECEDENCE = { | |
"=": 1, | |
"*=": 1, | |
"/=": 1, | |
"+=": 1, | |
"-=": 1, | |
"||": 2, | |
"&&": 3, | |
"<": 7, | |
">": 7, | |
"<=": 7, | |
">=": 7, | |
"==": 7, | |
"!=": 7, | |
"+": 10, | |
"-": 10, | |
"*": 20, | |
"/": 20, | |
"%": 20, | |
"|>": 30, | |
}; | |
let is = type => ch => { | |
let tok = input.peek(); | |
return tok && tok.type == type && (!ch || tok.value == ch) && tok; | |
}; | |
let is_punc = is("punc"); | |
let is_kw = is("kw"); | |
let is_op = is("op"); | |
let is_var = is("var"); | |
let skip = type => ch => { | |
if (is(type)(ch)) { | |
input.next(); | |
} else { | |
input.err("Parse Error", `Expecting ${type}: "${ch}"`); | |
} | |
}; | |
let skip_punc = skip("punc"); | |
let skip_kw = skip("kw"); | |
let skip_op = skip("op"); | |
let skip_var = skip("var"); | |
function parse_call(func) { | |
let peek = input.peek(); | |
return { | |
type: "call", | |
func: func, | |
line: peek.line, | |
col: peek.col, | |
args: delimited("(", ")", ",", parse_expression), | |
}; | |
} | |
function maybe_call(expr) { | |
expr = expr(); | |
return is_punc("(") ? parse_call(expr) : expr; | |
} | |
function delimited(start, stop, separator, parser) { | |
var a = [], | |
first = true; | |
skip_punc(start); | |
while (!input.eof()) { | |
if (is_punc(stop)) break; | |
if (first) first = false; | |
else skip_punc(separator); | |
if (is_punc(stop)) break; | |
a.push(parser()); | |
} | |
skip_punc(stop); | |
return a; | |
} | |
function parse_block() { | |
let peek = input.peek(); | |
var prog = delimited("{", "}", ";", parse_expression); | |
if (prog.length === 0) return FALSE; | |
if (prog.length == 1) return prog[0]; | |
return { | |
type: "block", | |
prog: prog, | |
line: peek.line, | |
col: peek.col | |
}; | |
} | |
function parse_not(){ | |
let next = input.next(); | |
return { | |
type: 'not', | |
value: parse_expression(), | |
line: next.line, | |
col: next.col, | |
}; | |
} | |
function parse_bool() { | |
let next = input.next(); | |
return { | |
type: "bool", | |
value: next.value == "true", | |
line: next.line, | |
col: next.col | |
}; | |
} | |
function parse_if() { | |
let peek = input.peek(); | |
skip_kw("if"); | |
var cond = parse_expression(); | |
if (!is_punc("{")) skip_kw("then"); | |
var then = parse_expression(); | |
var ret = { | |
type: "if", | |
cond: cond, | |
then: then, | |
line: peek.line, | |
col: peek.col | |
}; | |
if (is_kw("else")) { | |
input.next(); | |
ret.else = parse_expression(); | |
} | |
return ret; | |
} | |
function parse_varname() { | |
let name = input.next(); | |
let mod = ""; | |
let immutable = true; | |
if (name.type == "mod") { | |
mod = '?'; | |
name = input.next(); | |
} | |
if(name.type =="kw" && name.value == "mut"){ | |
immutable = false; | |
name = input.next(); | |
} | |
if (name.type != "var") input.err("Parse Error", "Expecting variable name"); | |
return { | |
value: name.value, | |
mod: mod, | |
immutable:immutable, | |
line: name.line, | |
col: name.col | |
}; | |
} | |
function parse_fn() { | |
let next = input.next(); | |
let vars, name, ret; | |
if (next.value == "fn") { | |
if (input.peek().type == "var") name = input.next(); | |
vars = delimited("(", ")", ",", parse_varname); | |
skip_kw("->"); | |
} else { | |
vars = []; | |
name = null; | |
} | |
return { | |
type: "function", | |
name: name, | |
vars: vars, | |
body: parse_expression(), | |
line: next.line, | |
col: next.col | |
}; | |
} | |
function parse_vardef() { | |
let peek = input.peek(); | |
let name = parse_varname(); | |
let def; | |
if (is_op("=")) { | |
skip_op("="); | |
def = parse_expression(); | |
} | |
return { | |
name, | |
def, | |
line: peek.line, | |
col: peek.col | |
}; | |
} | |
function parse_let() { | |
skip_kw("let"); | |
let peek = input.peek(); | |
if (peek.type == "var") { | |
let name = input.next().value; | |
let defs = delimited("(", ")", ",", parse_vardef); | |
skip_kw("->"); | |
return { | |
type: "call", | |
func: { | |
type: "function", | |
name: name, | |
vars: defs.map(def => def.name), | |
body: parse_expression() | |
}, | |
args: defs.map(def => def.def || FALSE), | |
line: peek.line, | |
col: peek.col | |
}; | |
} | |
let vars = delimited("(", ")", ",", parse_vardef); | |
skip_kw("->"); | |
return { | |
type: "let", | |
vars: vars, | |
body: parse_expression(), | |
line: peek.line, | |
col: peek.col | |
}; | |
} | |
function parse_define() { | |
let next = input.next(); | |
let varient = "immutable"; | |
if (is_kw("mut")) { | |
varient = "mutable"; | |
skip_kw("mut"); | |
} | |
let left = is_var(); | |
if (left) { | |
skip_var(left.value); | |
} else { | |
input.err("Parse Error", `Expecting variable name after ${varient=="immutable"?"def":"def mut"}`); | |
} | |
let right = { | |
type: "number", | |
value: 0 | |
}; | |
if (is_op("=")) { | |
skip_op("="); | |
right = parse_expression(); | |
} | |
return { | |
type: "define", | |
varient, | |
left, | |
right, | |
line: next.line, | |
col:next.col | |
}; | |
} | |
function parse_atom() { | |
return maybe_call(function() { | |
if (is_punc("(")) { | |
input.next(); | |
let exp = parse_expression(); | |
skip_punc(")"); | |
return exp; | |
} | |
if (is_punc("{")) return parse_block(); | |
if(is_op("!"))return parse_not(); | |
if (is_kw("if")) return parse_if(); | |
if (is_kw("true") || is_kw("false")) return parse_bool(); | |
if (is_kw("fn") || is_kw("fn->")) return parse_fn(); | |
if (is_kw("def")) return parse_define(); | |
if (is_kw("let")) return parse_let(); | |
let tok = input.next(); | |
if (tok.type == "var" || tok.type == "number" || tok.type == "string") | |
return tok; | |
input.err("Parse Error", `Unexpected token: ${JSON.stringify(tok)}`); | |
}); | |
} | |
function maybe_binary(left, my_prec) { | |
let tok = is_op(); | |
if (tok) { | |
let his_prec = PRECEDENCE[tok.value]; | |
if (his_prec > my_prec) { | |
input.next(); | |
let type; | |
let assign = ["=", "*=", "+=", "-=", "/="]; | |
if (assign.indexOf(tok.value) >= 0) { | |
type = "assign"; | |
} else { | |
type = "binary"; | |
} | |
return maybe_binary({ | |
type: type, | |
operator: tok.value, | |
left: left, | |
right: maybe_binary(parse_atom(), his_prec), | |
line: left.line, | |
col: left.col | |
}, my_prec); | |
} | |
} | |
return left; | |
} | |
function maybe_pipe(expr){ | |
if(expr.type=='binary'&&expr.operator=='|>'){ | |
let left = expr.left; | |
let right = expr.right; | |
if(right.type == "var"){ | |
return { | |
type:'call', | |
func: right, | |
line: right.line, | |
col: right.col, | |
args: [maybe_pipe(left)], | |
}; | |
} else if(right.type == "call"){ | |
return { | |
type: 'call', | |
func: { | |
value: right.func.value, | |
type: 'var', | |
line: right.func.line, | |
col: right.func.col | |
}, | |
line: right.line, | |
col: right.col, | |
args: [left,...right.args], | |
}; | |
} else { | |
input.err("Parse Error", "Expected call or variable after '|>'"); | |
} | |
} else { | |
return expr; | |
} | |
} | |
function parse_expression() { | |
return maybe_pipe((function(){ | |
return maybe_call(function() { | |
return maybe_binary(parse_atom(), 0); | |
}); | |
})()); | |
} | |
function parse_prog() { | |
let prog = []; | |
let peek = input.peek(); | |
while (!input.eof()) { | |
prog.push(parse_expression()); | |
if (!input.eof()) skip_punc(';'); | |
} | |
return { | |
type: "block", | |
prog: prog, | |
global: true, | |
line: peek.line, | |
col: peek.col | |
}; | |
} | |
return parse_prog(); | |
} | |
/* | |
function parse2(input){ | |
let FALSE = { type: "bool", value: false }; | |
let TRUE = { type: "bool", value: true }; | |
let is = type => value => { | |
let tok = input.peek(); | |
return tok && tok.type==type && (tok.value==value||!value); | |
} | |
let is_punc = is("punc"); | |
let is_kw = is("kw"); | |
let is_var = is("var"); | |
let is_op = is("op"); | |
let skip = type => ch => { | |
if (is(type)(ch)){ | |
input.next() | |
} else { | |
input.err("Parse Error",`Expecting ${type}: "${ch}", found ${JSON.stringify(input.peek())}`); | |
} | |
} | |
let skip_op = skip("op"); | |
let skip_punc = skip("punc"); | |
let skip_kw = skip("kw"); | |
function is_assign(){ | |
return is_op() && input.peek().value.endsWith("="); | |
} | |
function delimited(start,stop,sep,parser){ | |
let code = []; | |
let first = true; | |
skip_punc(start); | |
while(!input.eof()){ | |
if(is_punc(stop)) break; | |
if(first) first = false;else skip_punc(sep); | |
if(is_punc(stop)) break; | |
code.push(parser()); | |
} | |
skip_punc(stop); | |
return code; | |
} | |
function parse_define(){ | |
let type = input.next(); | |
let name = input.next(); | |
skip_op("="); | |
return { | |
type: type.value, | |
left: name, | |
right: parse_expression() | |
} | |
} | |
function parse_block(){ | |
let block = delimited("{","}",";",parse_expression); | |
if(block.length==0)return FALSE; | |
if(block.length==1)return block[0]; | |
return {type:"block",value:block}; | |
} | |
function parse_varname() { | |
var name = input.next(); | |
if (name.type != "var") input.err("Parse Error","Expecting variable name"); | |
return name.value; | |
} | |
function parse_function(){ | |
let next = input.next(); | |
let vars; | |
if(next.value=="fn"){ | |
vars = delimited("(",")",",",parse_varname); | |
skip_kw("->"); | |
} else {vars = null} | |
return { | |
type: "function", | |
vars: vars, | |
value: parse_expression() | |
}; | |
} | |
function parse_if(){ | |
skip_kw("if"); | |
let cond = parse_expression(); | |
if(!is_punc("{"))skip_kw("then"); | |
let then = parse_expression(); | |
let ret = { | |
type: "if", | |
cond: cond, | |
then: then | |
} | |
if(is_kw("else")) { | |
input.next(); | |
ret.else = parse_expression(); | |
} | |
return ret; | |
} | |
function parse_bool() { | |
return { | |
type : "bool", | |
value : input.next().value == "true" | |
}; | |
} | |
function make_call(func){ | |
return { | |
type: "call", | |
func: func, | |
args: delimited("(",")",",",parse_expression) | |
}; | |
} | |
function parse_assign(left){ | |
return { | |
type: "assign", | |
left: left, | |
right: parse_expression() | |
}; | |
} | |
function parse_binary(start){ | |
if(!is_op())return start; | |
function get(){ | |
let cache = []; | |
while(!input.eof()){ | |
if(is_op()){ | |
cache.push(input.next()); | |
} | |
} | |
} | |
} | |
function parse_expression(){ | |
if(is_kw("let")||is_kw("const"))return parse_define(); | |
if(is_punc("{"))return parse_block(); | |
if(is_kw("fn")||is_kw("fn->"))return parse_function(); | |
if(is_kw("if"))return parse_if(); | |
if(is_kw("true")||is_kw("false"))return parse_bool(); | |
if(is_punc("(")){ | |
input.next(); | |
let exp = parse_expression(); | |
input.next(); | |
if(is_punc("(")){ | |
return make_call(exp); | |
} else { | |
return exp; | |
} | |
} | |
if(is_var()){ | |
let iden = input.next(); | |
if(is_punc("(")){ | |
return make_call(iden); | |
} else if(is_assign()){ | |
return parse_assign(iden); | |
} else { | |
return parse_binary(iden); | |
} | |
} | |
let tok = input.next(); | |
if(is("string")()||is("number")()) return tok; | |
input.err("Parse Error",`Unexpected token ${JSON.stringify(tok)}`); | |
} | |
function parse_prog(){ | |
let prog = []; | |
while(!input.eof()){ | |
prog.push(parse_expression()); | |
if(!input.eof())skip_punc(";"); | |
} | |
return prog; | |
} | |
return parse_prog(); | |
} | |
*/ | |
// env | |
function Environment(parent) { | |
this.vars = Object.create(parent ? parent.vars : {}); | |
this.parent = parent; | |
} | |
Environment.prototype = { | |
extend: function() { | |
return new Environment(this); | |
}, | |
lookup: function(name) { | |
var scope = this; | |
while (scope) { | |
if (Object.prototype.hasOwnProperty.call(scope.vars, name)) | |
return scope; | |
scope = scope.parent; | |
} | |
return false; | |
}, | |
get: function(name,{line,col}={line:undefined,col:undefined}) { | |
if (name in this.vars) | |
return this.vars[name].value; | |
throw new Error(`Undefined variable "${name}" at ${line}:${col}`); | |
}, | |
set: function(name, value,{line,col}={line:undefined,col:undefined}) { | |
let scope = this.lookup(name); | |
if (!(scope || this).vars.hasOwnProperty(name)) | |
throw new Error(`Undefined variable "${name}" at ${line}:${col}`); | |
let opt = (scope || this).vars[name]; | |
if (opt.immutable) { | |
throw new Error(`Cannot write to read-only variable ${name} at ${line}:${col}`); | |
} | |
opt.value = value; | |
return value; | |
}, | |
def: function(name, value, { | |
immutable = true, | |
force = true | |
} = { | |
immutable: true, | |
force: true | |
},{line,col}={line:undefined,col:undefined}) { | |
if (this.vars.hasOwnProperty(name) && !force) | |
throw new Error(`Variable ${name} is already defined at ${line}:${col}`); | |
if (this.parent && !force) | |
throw new Error(`Cannot define value when not in global scope at ${line}:${col}`); | |
this.vars[name] = { | |
value, | |
immutable | |
}; | |
return this.vars[name]; | |
} | |
}; | |
function makeEnv() { | |
let globalEnv = new Environment(); | |
globalEnv.def("print", function(callback, txt) { | |
if (typeof txt !== "string") txt = txt.toString(); | |
output.setValue(output.getValue() + txt); | |
callback(); | |
}); | |
globalEnv.def("clear", function(callback) { | |
output.setValue(''); | |
callback(); | |
}); | |
globalEnv.def("time", function(callback, func) { | |
let start = performance.now(); | |
func(function(){ | |
let end = performance.now(); | |
Execute(callback, [(end - start).toFixed(3) + 'ms']); | |
}); | |
}); | |
globalEnv.def("sleep", function(callback, milliseconds) { | |
setTimeout(function() { | |
Execute(callback, [false]); // continuations expect a value, pass false | |
}, milliseconds); | |
}); | |
globalEnv.def("callcc",function(callback,fn, ...args){ | |
fn(callback,function CC(discard,ret){ | |
callback(ret); | |
}, ...args); | |
}); | |
//http://lisperator.net/pltut/cps-evaluator/yield#reset-shift | |
var pstack = []; | |
function _goto(f) { | |
f(function KGOTO(r) { | |
var h = pstack.pop(); | |
h(r); | |
}); | |
} | |
function reset(KRESET, th) { | |
pstack.push(KRESET); | |
_goto(th); | |
} | |
globalEnv.def("reset", reset); | |
function shift(KSHIFT, f) { | |
_goto(function(KGOTO) { | |
f(KGOTO, function SK(k1, v) { | |
pstack.push(k1); | |
KSHIFT(v); | |
}); | |
}); | |
} | |
globalEnv.def("shift", shift); | |
// for stdEnv | |
globalEnv.def("catch",null,{immutable:false}); | |
globalEnv.def("throw",null,{immutable:false}); | |
globalEnv.def("with-yield",null,{immutable:false}); | |
return globalEnv; | |
} | |
function stdEnv(ast){ | |
let std = `catch = fn(func)->{ | |
callcc(fn(cont)-> { | |
func(cont); | |
}); | |
}; | |
throw = fn(cont)->{ | |
cont(); | |
}; | |
with-yield = fn(func) ->{ | |
let (mut yield) -> { | |
yield = fn(val) ->{ | |
shift(fn(SK)->{ | |
func = SK; | |
val; ## return val | |
}); | |
}; | |
fn(val) -> { | |
reset( fn-> func(val || yield) ); | |
}; | |
} | |
};`; | |
std = parse(TokenStream(InputStream(std))); | |
ast = JSON.parse(JSON.stringify(ast)); | |
ast.prog = [...std.prog,...ast.prog]; | |
return ast; | |
} | |
// evaluating | |
/* function evaluate(exp, env) { | |
function apply_op(op, a, b) { | |
function num(x) { | |
x = parseFloat(x); | |
if (Number.isNaN(x)) | |
throw new Error("Expected number but got " + x); | |
return x; | |
} | |
function div(x) { | |
if (num(x) == 0) | |
throw new Error("Divide by zero"); | |
return x; | |
} | |
switch (op) { | |
case "+": | |
return num(a) + num(b); | |
case "-": | |
return num(a) - num(b); | |
case "*": | |
return num(a) * num(b); | |
case "/": | |
return num(a) / div(b); | |
case "%": | |
return num(a) % div(b); | |
case "&&": | |
return a !== false && b; | |
case "||": | |
return a !== false ? a : b; | |
case "<": | |
return num(a) < num(b); | |
case ">": | |
return num(a) > num(b); | |
case "<=": | |
return num(a) <= num(b); | |
case ">=": | |
return num(a) >= num(b); | |
case "==": | |
return a === b; | |
case "!=": | |
return a !== b; | |
} | |
throw new Error("Can't apply operator " + op); | |
} | |
function make_function(env, exp) { | |
function fn() { | |
var names = exp.vars; | |
var scope = env.extend(); | |
for (var i = 0; i < names.length; ++i) | |
scope.def(names[i].value, (i < arguments.length ? arguments[i] : false), false); | |
return evaluate(exp.body, scope); | |
} | |
return fn; | |
} | |
switch (exp.type) { | |
case "number": | |
case "string": | |
case "bool": | |
return exp.value; | |
case "var": | |
return env.get(exp.value); | |
case "define": | |
if (exp.left.type != "var") | |
throw new Error("Cannot assign to " + JSON.stringify(exp.left)); | |
return env.def(exp.left.value, evaluate(exp.right, env), exp.varient == "immutable"); | |
case "assign": | |
if (exp.left.type != "var") | |
throw new Error("Cannot assign to " + JSON.stringify(exp.left)); | |
return env.set(exp.left.value, evaluate(exp.right, env)); | |
case "binary": | |
return apply_op(exp.operator, | |
evaluate(exp.left, env), | |
evaluate(exp.right, env)); | |
case "function": | |
return make_function(env, exp); | |
case "if": | |
let cond = evaluate(exp.cond, env); | |
if (cond !== false) return evaluate(exp.then, env); | |
return exp.else ? evaluate(exp.else, env) : false; | |
case "block": | |
let val = false; | |
exp.prog.forEach(function(exp) { | |
val = evaluate(exp, env) | |
}); | |
return val; | |
case "call": | |
let func = evaluate(exp.func, env); | |
if(typeof func !== "function")func=func.value; | |
return func.apply(null, exp.args.map(arg => evaluate(arg, env))); | |
default: | |
throw new Error(`Cannot evaluate ${exp.type}`); | |
} | |
} | |
*/ | |
let STACKLEN; | |
function Continuation(f, args) { | |
this.f = f; | |
this.args = args; | |
} | |
function GUARD(f, args) { | |
if (--STACKLEN < 0) throw new Continuation(f, args); | |
} | |
function cpsEvaluate(exp, env, callback) { | |
function apply_op(op, a, b) { | |
function num(x) { | |
x = parseFloat(x); | |
if (Number.isNaN(x)) | |
throw new Error(`Expected number but got "${x}" at ${exp.line}:${exp.col}`); | |
return x; | |
} | |
function div(x) { | |
if (num(x) === 0) | |
throw new Error(`Divide by zero at ${exp.line}:${exp.col}`); | |
return x; | |
} | |
switch (op) { | |
case "+": | |
return num(a) + num(b); | |
case "-": | |
return num(a) - num(b); | |
case "*": | |
return num(a) * num(b); | |
case "/": | |
return num(a) / div(b); | |
case "%": | |
return num(a) % div(b); | |
case "&&": | |
return a !== false && b; | |
case "||": | |
return a !== false ? a : b; | |
case "<": | |
return num(a) < num(b); | |
case ">": | |
return num(a) > num(b); | |
case "<=": | |
return num(a) <= num(b); | |
case ">=": | |
return num(a) >= num(b); | |
case "==": | |
return a === b; | |
case "!=": | |
return a !== b; | |
} | |
throw new Error(`Can't apply operator "${op}" at ${exp.line}:${exp.col}`); | |
} | |
function make_function(env, exp) { | |
function err(arg,exp){ | |
throw new Error(`Argument ${arg} not provided to function at ${exp.line}:${exp.col}`) | |
} | |
function fn(callback) { | |
GUARD(fn, arguments); | |
let names = exp.vars; | |
let scope = env.extend(); | |
for (let i = 0; i < names.length; ++i) { | |
scope.def(names[i].value, i + 1 < arguments.length ? arguments[i + 1] : false , { | |
immutable: names[i].immutable, | |
force: true, | |
}); | |
} | |
cpsEvaluate(exp.body, scope, callback); | |
} | |
fn.len = exp.vars.length + 1; | |
if (exp.name) { | |
env = env.extend(); | |
env.def(exp.name.value, fn); | |
} | |
return fn; | |
} | |
GUARD(cpsEvaluate, arguments); | |
switch (exp.type) { | |
case "number": | |
case "string": | |
case "bool": | |
callback(exp.value); | |
return; | |
case "var": | |
callback(env.get(exp.value,exp)); | |
return; | |
case "define": | |
if (exp.left.type != "var") | |
throw new Error(`Cannot assign to ${exp.left.type} at ${exp.line}:${exp.col}`); | |
cpsEvaluate(exp.right, env, function CC(right) { | |
GUARD(CC, arguments); | |
callback(env.def(exp.left.value, right, { | |
immutable: exp.varient == "immutable", | |
force: false | |
},exp)); | |
}); | |
return; | |
case "let": | |
(function loop(env, i) { | |
if (i < exp.vars.length) { | |
let v = exp.vars[i]; | |
if (v.def) { | |
cpsEvaluate(v.def, env, function CC(val) { | |
let scope = env.extend(); | |
scope.def(v.name.value, val, { | |
immutable: v.name.immutable | |
},exp); | |
loop(scope, i + 1); | |
}); | |
} else { | |
let scope = env.extend(); | |
scope.def(v.name.value, false, { | |
immutable: v.name.immutable | |
},exp); | |
loop(scope, i + 1); | |
} | |
} else { | |
cpsEvaluate(exp.body, env, callback); | |
} | |
})(env, 0); | |
return; | |
case "assign": | |
if (exp.left.type != "var") | |
throw new Error(`Cannot assign to ${exp.left.type}`); | |
cpsEvaluate(exp.right, env, function CC(right) { | |
GUARD(CC, arguments); | |
callback(env.set(exp.left.value, right,env)); | |
}); | |
return; | |
case "binary": | |
cpsEvaluate(exp.left, env, function CC(left) { | |
GUARD(CC, arguments); | |
cpsEvaluate(exp.right, env, function CC(right) { | |
GUARD(CC, arguments); | |
callback(apply_op(exp.operator, left, right)); | |
}); | |
}); | |
return; | |
case "function": | |
callback(make_function(env, exp)); | |
return; | |
case "if": | |
cpsEvaluate(exp.cond, env, function CC(cond) { | |
GUARD(CC, arguments); | |
if (cond !== false) cpsEvaluate(exp.then, env, callback); | |
else if (exp.else) cpsEvaluate(exp.else, env, callback); | |
else callback(false); | |
}); | |
return; | |
case "block": | |
let scope = exp.global ? env : env.extend(); | |
(function loop(last, i) { | |
GUARD(loop, arguments); | |
if (i < exp.prog.length) { | |
cpsEvaluate(exp.prog[i], scope, function CC(val) { | |
GUARD(CC, arguments); | |
loop(val, i + 1); | |
}); | |
} else { | |
callback(last); | |
} | |
})(false, 0); | |
return; | |
case "call": | |
cpsEvaluate(exp.func, env, function CC(func) { | |
GUARD(CC, arguments); | |
(function loop(args, i) { | |
GUARD(loop, arguments); | |
if (i < exp.args.length) { | |
cpsEvaluate(exp.args[i], env, function CC(arg) { | |
GUARD(CC, arguments); | |
args[i + 1] = arg; | |
loop(args, i + 1); | |
}); | |
} else { | |
console.log(func,func.length) | |
if(args.length < (func.len || func.length)) | |
throw new Error(`Not enough arguments provided to function at ${exp.line}:${exp.col}`) | |
func(...args); | |
} | |
})([callback], 0); | |
}); | |
return; | |
case "not": | |
cpsEvaluate(exp.value,env,function CC(val){ | |
if(typeof val != 'boolean') | |
throw new Error(`'!' expects a boolean value, not "${typeof val}" at ${exp.line}:${exp.col}`); | |
callback(!val); | |
}); | |
return; | |
default: | |
throw new Error(`Cannot evaluate ${JSON.stringify(exp)} at ${exp.line}:${exp.col}`); | |
} | |
} | |
function Execute(f, args) { | |
while (true) try { | |
STACKLEN = 200; | |
return f(...args); | |
} catch (e) { | |
if (e instanceof Continuation) { | |
f = e.f; | |
args = e.args; | |
} else { | |
throw e; | |
} | |
} | |
} | |
// editor | |
let editor = document.getElementById('editor'); | |
let output = document.getElementById('output'); | |
CodeMirror.defineMode("lang", function() { | |
let c = TokenStreamCommon; | |
function token(stream, state) { | |
function read_comment(next) { | |
stream.eatWhile(ch => ch !== '\n'); | |
return "comment"; | |
} | |
function read_comment_m(next) { | |
stream.next(); | |
state.comment_m = true; | |
while (!stream.eol()) { | |
if (ch == "*" && next == "/") { | |
state.comment_m = false; | |
stream.next(); | |
return "comment"; | |
} | |
ch = stream.next(); | |
next = stream.peek(); | |
} | |
return "comment"; | |
} | |
function read_string(ch) { | |
state.string = ch; | |
if (stream.peek() == ch) { | |
stream.next(); | |
state.string = false; | |
return "string"; | |
} | |
if (stream.peek() == "\\") { | |
state.escape = true; | |
return "string"; | |
} | |
while (!stream.eol()) { | |
let ch = stream.next(); | |
let next = stream.peek(); | |
if (ch != "\\" && next == state.string) { | |
stream.next(); | |
state.string = false; | |
state.escape = false; | |
return "string"; | |
} | |
if (next == "\\") { | |
state.escape = true; | |
return "string"; | |
} | |
} | |
return "string"; | |
} | |
function read_number(ch) { | |
let has_dot = ch == "."; | |
let number = stream.eatWhile(function(ch) { | |
if (ch == ".") { | |
if (has_dot) return false; | |
has_dot = true; | |
return true; | |
} | |
return c.is_digit(ch); | |
}); | |
return "number"; | |
} | |
function read_ident() { | |
stream.eatWhile(c.is_id); | |
let curr = stream.current().trim(); | |
let cState = state.varState[state.varState.length - 1]; | |
if (c.is_keyword(curr)) { | |
if (curr == "def" || (curr == "mut" && state.context === null)) { | |
state.context = "def"; | |
} | |
if (curr == "fn") { | |
state.context = "fn"; | |
state.varState.push(JSON.parse(JSON.stringify(cState))); | |
cState = state.varState[state.varState.length - 1]; | |
} | |
if (curr == "->") { | |
state.context = null; | |
state.prev = curr; | |
} | |
if (curr == "let") { | |
state.context = "let"; | |
state.varState.push(JSON.parse(JSON.stringify(cState))); | |
cState = state.varState[state.varState.length - 1]; | |
} | |
return "keyword"; | |
} | |
if (state.context == "def") { | |
state.context = null; | |
if (!cState.includes(curr)) cState.push(curr); | |
return "def"; | |
} | |
if (state.context == "fn" || state.context == "let") { | |
cState.push(curr); | |
return "def"; | |
} | |
if (cState.includes(curr)) return "variable-2"; | |
return "variable-1"; | |
} | |
function read_punc() { | |
let cState = state.varState[state.varState.length - 1]; | |
let curr = stream.current().trim(); | |
if (curr == "{") { | |
if (state.prev != '->') { | |
state.varState.push(JSON.parse(JSON.stringify(cState))); | |
} else { | |
state.prev = null; | |
state.context = null; | |
} | |
} | |
if (curr == "," && state.context == "letexp") { | |
state.context = "let"; | |
} | |
if (curr == "}" && state.varState.length > 1) { | |
state.varState.pop(); | |
} | |
if ("(){}[]".indexOf(curr) >= 0) { | |
return "bracket"; | |
} | |
return; | |
} | |
function read_op() { | |
stream.eatWhile(c.is_op); | |
if (state.context == "let") { | |
state.context = "letexp"; | |
} | |
return "operator"; | |
} | |
stream.eatSpace(); | |
let ch = stream.next(); | |
let next = stream.peek(); | |
if (state.comment_m) { | |
while (!stream.eol()) { | |
if (ch == "*" && next == "/") { | |
state.comment_m = false; | |
stream.next(); | |
return "comment"; | |
} | |
ch = stream.next(); | |
next = stream.peek(); | |
} | |
return "comment"; | |
} | |
if (state.escape) { | |
stream.next(); | |
state.escape = false; | |
return "atom"; | |
} | |
if (state.string) { | |
if (ch == state.string) { | |
state.string = false; | |
return "string"; | |
} | |
while (!stream.eol()) { | |
if (ch != "\\" && next == state.string) { | |
stream.next(); | |
state.string = false; | |
return "string"; | |
} | |
if (ch == "\\") { | |
state.escape = true; | |
return "string"; | |
} | |
ch = stream.next(); | |
next = stream.peek(); | |
} | |
return "string"; | |
} | |
if (ch == "#") { | |
return read_comment(); | |
} | |
if (ch == "/" && next == "*") { | |
return read_comment_m(); | |
} | |
if (ch == "/" && next == "/") { | |
return read_comment(); | |
} | |
if (ch == "'" || ch == '"') return read_string(ch); | |
if (c.is_digit(ch)) return read_number(ch); | |
if (c.is_id_start(ch) || (ch == "-" && next == ">")) return read_ident(); | |
if (c.is_punc(ch)) return read_punc(); | |
if (c.is_op(ch)) return read_op(); | |
if (ch == "?") return "atom"; | |
stream.skipToEnd(); | |
return "error"; | |
} | |
return { | |
startState() { | |
return { | |
comment_m: false, | |
string: false, | |
context: null, | |
escape: false, | |
varState: [ | |
Object.keys(makeEnv().vars) | |
], | |
prev: null, | |
}; | |
}, | |
copyState(state) { | |
return JSON.parse(JSON.stringify(state)); | |
}, | |
indent(state, text) { | |
let indent = state.varState.length - 1; | |
if (text.includes("}")) indent = state.varState.length - 2; | |
return editor.options.indentUnit * indent; | |
}, | |
token | |
}; | |
}); | |
editor = CodeMirror(editor, { | |
value: document.getElementById('example').innerText, | |
lineNumbers: true, | |
theme: "railscasts", | |
lineWrapping: true, | |
scrollbarStyle: "overlay", | |
matchBrackets: true, | |
autoCloseBrackets: true, | |
styleActiveLine: true, | |
mode: "lang", | |
tabSize: 2, | |
extraKeys: { | |
["Ctrl-Enter"]() { | |
display(); | |
}, | |
["Ctrl-L"]() { | |
output.setValue(''); | |
} | |
} | |
}); | |
editor.widgets = []; | |
CodeMirror.defineMode("console", function() { | |
return { | |
token: function(stream) { | |
if (stream.match("Error: ")) { | |
stream.skipToEnd(); | |
return "error"; | |
} else if (stream.match("*** Result: ")) { | |
stream.skipToEnd(); | |
return "variable-2"; | |
} | |
stream.skipToEnd(); | |
return "comment"; | |
} | |
}; | |
}); | |
output = CodeMirror(output, { | |
readOnly: "nocursor", | |
theme: "railscasts", | |
lineWrapping: true, | |
scrollbarStyle: "overlay", | |
mode: "console" | |
}); | |
let run = document.getElementById('run'); | |
function display() { | |
// clear errors | |
editor.widgets.forEach(w => w.clear()); | |
editor.widgets = []; | |
// get Token stream | |
let code = editor.getValue(); | |
code = InputStream(code, editor); | |
code = TokenStream(code); | |
// define environment | |
let globalEnv = makeEnv(); | |
function print(txt) { | |
if (typeof txt !== "string") txt = txt.toString(); | |
output.setValue(output.getValue() + txt); | |
} | |
// make ast and eval | |
let x = parse(code); | |
window.x = x; | |
window.onerror = function(msg, source, line, col, e) { | |
if (!(output.getValue().endsWith('\n') || output.getValue() === "")) print('\n'); | |
print(e); | |
window.e = e; | |
}; | |
if (!(output.getValue().endsWith('\n') || output.getValue() === "")) print('\n'); | |
Execute(cpsEvaluate, [stdEnv(x), globalEnv, function(result) { | |
if (!(output.getValue().endsWith('\n') || output.getValue() === "")) print('\n'); | |
print(`*** Result: ${result}\n`); | |
}]); | |
} | |
run.addEventListener('click', display); | |
display(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.23.0/codemirror.min.js"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.23.0/addon/scroll/simplescrollbars.min.js"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.23.0/addon/edit/matchbrackets.min.js"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.23.0/addon/edit/closebrackets.min.js"></script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
html,body { | |
padding:0; | |
height:100%; | |
width:100%; | |
} | |
.CodeMirror { | |
height:100%; | |
} | |
.CodeMirror-overlayscroll-vertical div { | |
background-color:#8F938F; | |
} | |
.CodeMirror div div.CodeMirror-selected { //increase specificity | |
background-color:black!important; | |
} | |
#editor,#output { | |
width:50%; | |
float:left; | |
height:100%; | |
box-sizing:border-box; | |
} | |
#output { | |
height: calc(100% - 35px); | |
white-space: pre-wrap; | |
word-break: break-word; | |
font-family:monospace; | |
overflow:auto; | |
background-color: rgb(44, 40, 39); | |
border-left: 5px solid #34302f; | |
box-sizing:border-box; | |
color:#8F938F; | |
} | |
#run { | |
position:absolute; | |
width:50%; | |
height:35px; | |
right:0; | |
bottom:0; | |
box-sizing:border-box; | |
line-height:30px; | |
text-align:center; | |
border-left: 5px solid #34302f; | |
border-top: 5px solid #34302f; | |
background-color: rgb(44, 40, 39); | |
color:#8F938F; | |
} | |
.err { | |
background-color:red; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<link href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.23.0/codemirror.min.css" rel="stylesheet" /> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.23.0/addon/scroll/simplescrollbars.min.css" rel="stylesheet" /> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.23.0/theme/railscasts.min.css" rel="stylesheet" /> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment