diff --git a/jsvar.d b/jsvar.d new file mode 100644 index 0000000..06e8ad4 --- /dev/null +++ b/jsvar.d @@ -0,0 +1,838 @@ +/** + jsvar provides a D type called 'var' that works similarly to the same in Javascript. + + It is weakly and dynamically typed, but interops pretty easily with D itself: + + var a = 10; + a ~= "20"; + assert(a == "1020"); + + var a = function(int b, int c) { return b+c; }; + // note the second set of () is because of broken @property + assert(a()(10,20) == 30); + + var a = var.emptyObject; + a.foo = 30; + assert(a["foo"] == 30); + + var b = json!q{ + "foo":12, + "bar":{"hey":[1,2,3,"lol"]} + }; + + assert(b.bar.hey[1] == 2); + + + You can also use var.fromJson, a static method, to quickly and easily + read json or var.toJson to write it. + + Also, if you combine this with my new arsd.script module, you get pretty + easy interop with a little scripting language that resembles a cross between + D and Javascript - just like you can write in D itself using this type. +*/ +module arsd.jsvar; + +import std.stdio; +import std.traits; +import std.conv; +import std.json; + +/* + PrototypeObject FIXME: + make undefined variables reaction overloadable in PrototypeObject, not just a switch + + script FIXME: + + the Expression should keep scriptFilename and lineNumber around for error messages + + it should consistently throw on missing semicolons + + 0) add global functions to the object like assert() + 1) ensure operator precedence is sane + 2) a++ would prolly be nice, and def -a + 3) loops (foreach - preferably about as well as D (int ranges, arrays, objects with opApply overloaded, and input ranges), do while?) + foreach(i; 1 .. 10) -> for(var i = 1; i < 10; i++) + foreach(i; array) -> for(var i = 0; i < array.length; i++) + foreach(i; object) -> for(var v = object.visit; !v.empty; v.popFront) { var i = v.front; / *...* / } + 4) switches? + 8) IMPORTANT FIXME: make sure return/break/continue/exit? work right + 15) perhaps new, which can work in the style of javascript i figure + + 10) __FILE__ and __LINE__ as default function arguments should work like in D + + 14) import???????/ it could just attach a particular object to the local scope, and the module decl just giving the local scope a name + there could be a super-global object that is the prototype of the "global" used here + then you import, and it pulls moduleGlobal.prototype = superGlobal.modulename... or soemthing. + + to get the vars out in D, you'd have to be aware of this, since you pass the superglobal + hmmm maybe not worth it + + though maybe to export vars there could be an explicit export namespace or something. + + 16) stack traces on script exceptions + 17) an exception type that we can create in the script + 5) gotos? labels? + + try is considered a statement right now and this only works on top level surrounded by {} + it should be usable anywhere + + var FIXME: + + user defined operator overloading on objects, including opCall + prototype objects for Array, String, and Function + + opEquals and stricterOpEquals + + it would be nice if delegates on native types could work +*/ + + +/* + Script notes: + + the one type is var. It works just like the var type in D from arsd.jsvar. + (it might be fun to try to add other types, and match D a little better here! We could allow implicit conversion to and from var, but not on the other types, they could get static checking. But for now it is only var. BTW auto is an alias for var right now) + + There is no comma operator, but you can use a scope as an expression: a++, b++; can be written as {a++;b++;} +*/ + + +version(test_script) +void main() { + import arsd.script; + struct Test { + int a = 10; + string name = "ten"; + } + + auto globals = var.emptyObject; + globals.lol = 100; + globals.rofl = 23; + + globals.arrtest = var.emptyArray; + + globals.write = (var a) { writeln("script said: ", a); }; + + // call D defined functions in script + globals.func = (var a, var b) { writeln("Hello, world! You are : ", a, " and ", b); }; + + globals.ex = () { throw new ScriptRuntimeException("test", 1); }; + + globals.fun = { return var({ writeln("hello inside!"); }); }; + + import std.file; + writeln(interpret(readText("scripttest_code.d"), globals)); + + globals.ten = 10.0; + globals.five = 5.0; + writeln(interpret(q{ + var a = json!q{ }; + a.b = json!q{ }; + a.b.c = 10; + a; + }, globals)); + + repl(globals); + + writeln("BACK IN D!"); + globals.c()(10); // call script defined functions in D (note: this runs the interpreter) + + //writeln(globals._getMember("lol", false)); + return; + + var k,l ; + + var j = json!q{ + "hello": { + "data":[1,2,"giggle",4] + }, + "world":20 + }; + + writeln(j.hello.data[2]); + + + Test t; + var rofl = t; + writeln(rofl.name); + writeln(rofl.a); + + rofl.a = "20"; + rofl.name = "twenty"; + + t = rofl.get!Test; + writeln(t); + + var a1 = 10; + a1 -= "5"; + a1 /= 2; + + writeln(a1); + + var a = 10; + var b = 20; + a = b; + + b = 30; + a += 100.2; + writeln(a); + + var c = var.emptyObject; + c.a = b; + + var d = c; + d.b = 50; + + writeln(c.b); + + writeln(d.toJson()); + + var e = a + b; + writeln(a, " + ", b, " = ", e); + + e = function(var lol) { + writeln("hello with ",lol,"!"); + return lol + 10; + }; + + writeln(e("15")); + + if(var("ass") > 100) + writeln(var("10") / "3"); +} + +template json(string s) { + // ctfe doesn't support the unions std.json uses :( + //enum json = var.fromJsonObject(s); + + // FIXME we should at least validate string s at compile time + var json() { + return var.fromJson("{" ~ s ~ "}"); + } +} + +private real stringToNumber(string s) { + real r; + try { + r = to!real(s); + } catch (Exception e) { + r = real.nan; + } + + return r; +} + +private bool realIsInteger(real r) { + return (r == cast(long) r); +} + +// helper template for operator overloading +private var _op(alias _this, alias this2, string op, T)(T t) if(op == "~") { + static if(is(T == var)) { + if(t.payloadType() == var.Type.Array) + return _op!(_this, this2, op)(t._payload._array); + else if(t.payloadType() == var.Type.String) + return _op!(_this, this2, op)(t._payload._string); + //else + //return _op!(_this, this2, op)(t.get!string); + } + + if(this2.payloadType() == var.Type.Array) { + auto l = this2._payload._array; + static if(isArray!T && !isSomeString!T) + foreach(item; t) + l ~= var(item); + else + l ~= var(t); + + _this._type = var.Type.Array; + _this._payload._array = l; + return _this; + } else if(this2.payloadType() == var.Type.String) { + auto l = this2._payload._string; + l ~= var(t).get!string; // is this right? + _this._type = var.Type.String; + _this._payload._string = l; + return _this; + } else { + auto l = this2.get!string; + l ~= var(t).get!string; + _this._type = var.Type.String; + _this._payload._string = l; + return _this; + } + + assert(0); + +} + +private var _op(alias _this, alias this2, string op, T)(T t) if(op != "~") { + static if(is(T == var)) { + if(t.payloadType() == var.Type.Integral) + return _op!(_this, this2, op)(t._payload._integral); + if(t.payloadType() == var.Type.Floating) + return _op!(_this, this2, op)(t._payload._floating); + if(t.payloadType() == var.Type.String) + return _op!(_this, this2, op)(t._payload._string); + assert(0, to!string(t.payloadType())); + } else { + if(this2.payloadType() == var.Type.Integral) { + auto l = this2._payload._integral; + static if(isIntegral!T) { + mixin("l "~op~"= t;"); + _this._type = var.Type.Integral; + _this._payload._integral = l; + return _this; + } else static if(isFloatingPoint!T) { + this2._type = var.Type.Floating; + real f = l; + mixin("f "~op~"= t;"); + _this._type = var.Type.Floating; + _this._payload._floating = f; + return _this; + } else static if(isSomeString!T) { + auto rhs = stringToNumber(t); + if(realIsInteger(rhs)) { + mixin("l "~op~"= rhs;"); + _this._type = var.Type.Integral; + _this._payload._integral = l; + } else{ + real f = l; + mixin("f "~op~"= rhs;"); + _this._type = var.Type.Floating; + _this._payload._floating = f; + } + return _this; + + } + } else if(this2.payloadType() == var.Type.Floating) { + auto f = this._payload._floating; + + static if(isIntegral!T || isFloatingPoint!T) { + mixin("f "~op~"= t;"); + _this._type = var.Type.Floating; + _this._payload._floating = f; + return _this; + } else static if(isSomeString!T) { + auto rhs = stringToNumber(t); + mixin("f "~op~"= rhs;"); + _this._type = var.Type.Floating; + _this._payload._floating = f; + return _this; + } else assert(0); + } else if(this2.payloadType() == var.Type.String) { + real r = stringToNumber(this2._payload._string); + real rhs; + + static if(isSomeString!T) { + rhs = stringToNumber(t); + } else { + rhs = to!real(t); + } + + mixin("r " ~ op ~ "= rhs;"); + _this._type = var.Type.Floating; + _this._payload._floating = r; + return _this; + } else { + // the operation is nonsensical, we should throw or ignore it + var i = 0; + return i; + } + } + + assert(0); +} + + +struct var { + public this(T)(T t) { + static if(is(T == var)) + this = t; + else + this.opAssign(t); + } + + public bool opCast(T:bool)() { + final switch(this._type) { + case Type.Object: + return this._payload._object !is null; + case Type.Array: + return this._payload._array.length != 0; + case Type.String: + return this._payload._string.length != 0; + case Type.Integral: + return this._payload._integral != 0; + case Type.Floating: + return this._payload._floating != 0; + case Type.Boolean: + return this._payload._boolean; + case Type.Function: + return this._payload._function !is null; + } + } + + public int opApply(int delegate(ref var) dg) { + if(this.payloadType() == Type.Array) + foreach(ref v; this._payload._array) + if(auto result = dg(v)) + return result; + return 0; + } + + public T opCast(T)() { + return this.get!T; + } + + public auto ref putInto(T)(ref T t) { + return t = this.get!T; + } + + // if it is var, we'll just blit it over + public var opAssign(T)(T t) if(!is(T == var)) { + static if(isFloatingPoint!T) { + this._type = Type.Floating; + this._payload._floating = t; + } else static if(isIntegral!T) { + this._type = Type.Integral; + this._payload._integral = t; + } else static if(isCallable!T) { + this._type = Type.Function; + this._payload._function = delegate var(var _this, var[] args) { + var ret; + + ParameterTypeTuple!T fargs; + foreach(idx, a; fargs) { + if(idx == args.length) + break; + cast(Unqual!(typeof(a))) fargs[idx] = args[idx].get!(typeof(a)); + } + + static if(is(ReturnType!t == void)) { + t(fargs); + } else { + ret = t(fargs); + } + + return ret; + }; + } else static if(isSomeString!T) { + this._type = Type.String; + this._payload._string = to!string(t); + } else static if((is(T == class) || is(T == struct) || isAssociativeArray!T)) { + this,_type = Type.Object; + auto obj = new PrototypeObject(); + this._payload._object = obj; + + foreach(member; __traits(allMembers, T)) { + static if(__traits(compiles, __traits(getMember, t, member))) { + static if(is(typeof(__traits(getMember, t, member)) == function)) { + // skipping these because the delegate we get isn't going to work anyway; the object may be dead and certainly won't be updated + //this[member] = &__traits(getMember, proxyObject, member); + } else + this[member] = __traits(getMember, t, member); + } + } + } else static if(isArray!T) { + this._type = Type.Array; + var[] arr; + arr.length = t.length; + foreach(i, item; t) + arr[i] = var(item); + this._payload._array = arr; + } else static if(is(T == bool)) { + this._type = Type.Boolean; + this._payload._boolean = t; + } + + return this; + } + + public var opOpAssign(string op, T)(T t) { + return _op!(this, this, op, T)(t); + } + + public var opBinary(string op, T)(T t) { + var n; + return _op!(n, this, op, T)(t); + } + + public var apply(var _this, var[] args) { + if(this.payloadType() == Type.Function) { + return this._payload._function(_this, args); + } + + // or we could throw + var ret; + return ret; + } + + public var call(T...)(var _this, T t) { + var[] args; + foreach(a; t) { + args ~= var(a); + } + return this.apply(_this, args); + } + + public var opCall(T...)(T t) { + return this.call(this, t); + } + + public string toString() { + return this.get!string; + } + + public T get(T)() { + static if(is(T == var)) { + return this; + } else + final switch(payloadType) { + case Type.Boolean: + static if(is(T == bool)) + return this._payload._boolean; + else static if(isFloatingPoint!T || isIntegral!T) + return this._payload._boolean ? 1 : 0; + else static if(isSomeString!T) + return this._payload._boolean ? "true" : "false"; + else + return T.init; + case Type.Object: + static if(isAssociativeArray!T) { + T ret; + foreach(k, v; this._properties) + ret[to!(KeyType!T)(k)] = v.get!(ValueType!T); + + return ret; + } else static if(is(T == struct) || is(T == class)) { + T t; + static if(is(T == class)) + t = new T(); + + foreach(i, a; t.tupleof) { + cast(Unqual!(typeof((a)))) t.tupleof[i] = this[t.tupleof[i].stringof[2..$]].get!(typeof(a)); + } + + return t; + } else static if(isSomeString!T) { + // FIXME: is this best? + return this.toJson(); + } + + return T.init; + case Type.Integral: + static if(isFloatingPoint!T || isIntegral!T) + return to!T(this._payload._integral); + else static if(isSomeString!T) + return to!string(this._payload._integral); + else + return T.init; + case Type.Floating: + static if(isFloatingPoint!T || isIntegral!T) + return to!T(this._payload._floating); + else static if(isSomeString!T) + return to!string(this._payload._floating); + else + return T.init; + case Type.String: + static if(__traits(compiles, to!T(this._payload._string))) + try { + return to!T(this._payload._string); + } catch (Exception e) {} + return T.init; + case Type.Array: + auto pl = this._payload._array; + static if(isSomeString!T) { + return to!string(pl); + } else static if(isArray!T) { + T ret; + foreach(item; pl) + ret ~= item.get!(ElementType!T); + return ret; + } + + // is it sane to translate anything else? + + return T.init; + case Type.Function: + // FIXME: we just might be able to do better for both of these + return T.init; + break; + } + } + + public int opCmp(T)(T t) { + auto f = this.get!real; + static if(is(T == var)) + auto r = t.get!real; + else + auto r = t; + return cast(int)(f - r); + } + + /* + public bool opEquals(T)(T t) { + + } + */ + + public enum Type { + Object, Array, Integral, Floating, String, Function, Boolean + } + + public Type payloadType() { + return _type; + } + + private Type _type; + + // FIXME: arrays, functions, and strings are supposed to have prototypes too + private union Payload { + PrototypeObject _object; + var[] _array; + long _integral; + real _floating; + string _string; + bool _boolean; + var delegate(var _this, var[] args) _function; + } + + public void _function(var delegate(var, var[]) f) { + this._payload._function = f; + this._type = Type.Function; + } + + public void _object(PrototypeObject obj) { + this._type = Type.Object; + this._payload._object = obj; + } + + package Payload _payload; + + private void _requireType(Type t, string file = __FILE__, size_t line = __LINE__){ + if(this.payloadType() != t) + throw new DynamicTypeException(this, t, file, line); + } + + public @property ref var opDispatch(string name, string file = __FILE__, size_t line = __LINE__)() { + return this[name]; + } + + public @property ref var opDispatch(string name, string file = __FILE__, size_t line = __LINE__, T)(T r) { + return this.opIndexAssign!T(r, name); + } + + public ref var opIndex(var name, string file = __FILE__, size_t line = __LINE__) { + return opIndex(name.get!string, file, line); + } + + public ref var opIndexAssign(T)(T t, var name, string file = __FILE__, size_t line = __LINE__) { + return opIndexAssign(t, name.get!string, file, line); + } + + public ref var opIndex(string name, string file = __FILE__, size_t line = __LINE__) { + // if name is numeric, we should convert to int + if(name.length && name[0] >= '0' && name[0] <= '9') + return opIndex(to!size_t(name), file, line); + _requireType(Type.Object); // FIXME + if(_payload._object is null) + throw new DynamicTypeException(var(null), Type.Object, file, line); + return this._payload._object._getMember(name, true, false, file, line); + } + + public ref var opIndexAssign(T)(T t, string name, string file = __FILE__, size_t line = __LINE__) { + if(name.length && name[0] >= '0' && name[0] <= '9') + return opIndexAssign(t, to!size_t(name), file, line); + _requireType(Type.Object); // FIXME + if(_payload._object is null) + throw new DynamicTypeException(var(null), Type.Object, file, line); + this._payload._object._getMember(name, false, false, file, line) = t; + return this._payload._object._properties[name]; + } + + public ref var opIndex(size_t idx, string file = __FILE__, size_t line = __LINE__) { + if(_type == Type.Array) { + auto arr = this._payload._array; + if(idx < arr.length) + return arr[idx]; + } + var* n = new var(); + return *n; + } + + public ref var opIndexAssign(T)(T t, size_t idx, string file = __FILE__, size_t line = __LINE__) { + if(_type == Type.Array) { + alias arr = this._payload._array; + if(idx >= this._payload._array.length) + this._payload._array.length = idx + 1; + this._payload._array[idx] = t; + return this._payload._array[idx]; + } + var* n = new var(); + return *n; + } + + + @property static var emptyObject(PrototypeObject prototype = null) { + var v; + v._type = Type.Object; + v._payload._object = new PrototypeObject(); + v._payload._object.prototype = prototype; + return v; + } + + @property static var emptyArray() { + var v; + v._type = Type.Array; + return v; + } + + // FIXME + static var fromJson(string json) { + auto decoded = parseJSON(json); + return var.fromJsonValue(decoded); + } + + static var fromJsonValue(JSONValue v) { + var ret; + + final switch(v.type) { + case JSON_TYPE.STRING: + ret = v.str; + break; + case JSON_TYPE.UINTEGER: + ret = v.uinteger; + break; + case JSON_TYPE.INTEGER: + ret = v.integer; + break; + case JSON_TYPE.FLOAT: + ret = v.floating; + break; + case JSON_TYPE.OBJECT: + ret = var.emptyObject; + foreach(k, val; v.object) { + ret[k] = var.fromJsonValue(val); + } + break; + case JSON_TYPE.ARRAY: + ret = var.emptyArray; + ret._payload._array.length = v.array.length; + foreach(idx, item; v.array) { + ret._payload._array[idx] = var.fromJsonValue(item); + } + break; + case JSON_TYPE.TRUE: + ret = true; + break; + case JSON_TYPE.FALSE: + ret = false; + break; + case JSON_TYPE.NULL: + ret = null; + break; + } + + return ret; + } + + string toJson() { + auto v = toJsonValue(); + return toJSON(&v); + } + + // FIXME: maybe we could do some cool stuff with boolean true/false and uinteger too + JSONValue toJsonValue() { + JSONValue val; + final switch(payloadType()) { + case Type.Boolean: + if(this._payload._boolean) + val.type = JSON_TYPE.TRUE; + else + val.type = JSON_TYPE.FALSE; + break; + case Type.Object: + if(_payload._object is null) { + val.type = JSON_TYPE.NULL; + } else { + val.type = JSON_TYPE.OBJECT; + foreach(k, v; _payload._object._properties) + val.object[k] = v.toJsonValue(); + } + break; + case Type.String: + val.type = JSON_TYPE.STRING; + val.str = _payload._string; + break; + case Type.Integral: + val.type = JSON_TYPE.INTEGER; + val.integer = _payload._integral; + break; + case Type.Floating: + val.type = JSON_TYPE.FLOAT; + val.floating = _payload._floating; + break; + case Type.Array: + val.type = JSON_TYPE.ARRAY; + + auto a = _payload._array; + val.array.length = a.length; + foreach(i, v; a) { + val.array[i] = v.toJsonValue(); + } + break; + case Type.Function: + val.type = JSON_TYPE.NULL; // ideally we would just skip it entirely... + break; + } + return val; + } +} + +/* +@PrototypeConstructor +void foo(var _this) { + +} + +@PrototypeThis +a.foo = function(var _this) {} +*/ + +class PrototypeObject { + string name; + PrototypeObject prototype; + var[string] _properties; + // FIXME: maybe throw something else + package ref var _getMember(string name, bool recurse, bool throwOnFailure, string file = __FILE__, size_t line = __LINE__) { + auto curr = this; + do { + auto prop = name in curr._properties; + if(prop is null) { + if(!recurse) + break; + else + curr = curr.prototype; + } else + return *prop; + } while(curr); + + // if we're here, the property was not found, so let's implicitly create it + if(throwOnFailure) + throw new Exception("no such property " ~ name, file, line); + var n; + this._properties[name] = n; + return this._properties[name]; + } +} + + +class DynamicTypeException : Exception { + this(var v, var.Type required, string file = __FILE__, size_t line = __LINE__) { + import std.string; + if(v.payloadType() == required) + super(format("Tried to use null as a %s", required), file, line); + else + super(format("Tried to use %s as a %s", v.payloadType(), required), file, line); + } +} diff --git a/script.d b/script.d new file mode 100644 index 0000000..175c1ba --- /dev/null +++ b/script.d @@ -0,0 +1,1586 @@ +/** + Script features: + + * scope guards + * try/catch/finally/throw + * for/while + * D style operators + * if/else + * int, float, string, array, and json!q{} literals + * __FILE__, __LINE__ + * functions: + var fn = function(args...) expr; + or + function fn(args....) expr; + + Special function things: + _arguments + _thisfunc + this + + args can say var if you want, but don't have to + default arguments supported in any position + when calling, you can use the default keyword to use the default value in any position + + * easy interop with D thanks to arsd.jsvar. When interpreting, pass a var object to use as globals. + This object also contains the global state when interpretation is done. + +*/ +module arsd.script; + +import arsd.jsvar; + +import std.stdio; +import std.traits; +import std.conv; +import std.json; + +import std.array; +import std.range; + +/*************************************** + script to follow +****************************************/ + +class ScriptCompileException : Exception { + this(string msg, int lineNumber, string file = __FILE__, size_t line = __LINE__) { + super(to!string(lineNumber) ~ ": " ~ msg, file, line); + } +} + +class ScriptRuntimeException : Exception { + this(string msg, int lineNumber, string file = __FILE__, size_t line = __LINE__) { + super(to!string(lineNumber) ~ ": " ~ msg, file, line); + } +} + +class ScriptException : Exception { + var payload; + int lineNumber; + this(var payload, int lineNumber, string file = __FILE__, size_t line = __LINE__) { + this.payload = payload; + this.lineNumber = lineNumber; + super("script@" ~ to!string(lineNumber) ~ ": " ~ to!string(payload), file, line); + } + + override string toString() { + return "script@" ~ to!string(lineNumber) ~ ": " ~ payload.get!string; + } +} + +struct ScriptToken { + enum Type { identifier, keyword, symbol, string, int_number, float_number } + Type type; + string str; + string scriptFilename; + int lineNumber; + + string wasSpecial; +} + + // these need to be ordered from longest to shortest +private enum string[] keywords = [ + "function", + "__FILE__", "__LINE__", // these two are special to the lexer + "foreach", "json!q{", "default", "finally", + "return", + "while", "catch", "throw", "scope", + "auto", // provided as an alias for var right now, may change later + "null", "else", + "var", "for", "try", + "if", +]; +private enum string[] symbols = [ + "//", "/*", "/+", + "+=", "-=", "*=", "/=", "~=", "==", "<=", ">=","!=", + "..", + ".",",",";",":", + "[", "]", "{", "}", "(", ")", + "+", "-", "*", "/", "=", "<", ">","~","!", +]; + +// we need reference semantics on this all the time +class TokenStream(TextStream) { + TextStream textStream; + string text; + int lineNumber = 1; + string scriptFilename; + + void advance(int size) { + foreach(i; 0 .. size) { + if(text[0] == '\n') + lineNumber ++; + text.popFront(); + } + } + + this(TextStream ts, string fn) { + textStream = ts; + scriptFilename = fn; + text = textStream.front; + popFront; + } + + ScriptToken next; + + ScriptToken peek; + bool peeked; + void pushFront(ScriptToken f) { + peek = f; + peeked = true; + } + + ScriptToken front() { + if(peeked) + return peek; + else + return next; + } + + bool empty() { + return text.length == 0 && textStream.empty && !peeked; + } + + void popFront() { + if(peeked) { + peeked = false; + return; + } + + assert(!empty); + mainLoop: + while(text.length) { + ScriptToken token; + token.lineNumber = lineNumber; + token.scriptFilename = scriptFilename; + + if(text[0] == ' ' || text[0] == '\t' || text[0] == '\n') { + advance(1); + continue; + } else if(text[0] >= '0' && text[0] <= '9') { + int pos; + bool sawDot; + while(pos < text.length && ((text[pos] >= '0' && text[pos] <= '9') || text[pos] == '.')) { + if(text[pos] == '.') { + if(sawDot) + break; + else + sawDot = true; + } + pos++; + } + + token.type = sawDot ? ScriptToken.Type.float_number : ScriptToken.Type.int_number; + token.str = text[0 .. pos]; + advance(pos); + } else if((text[0] >= 'a' && text[0] <= 'z') || (text[0] == '_') || (text[0] >= 'A' && text[0] <= 'Z')) { + bool found = false; + foreach(keyword; keywords) + if(text.length >= keyword.length && text[0 .. keyword.length] == keyword && + // making sure this isn't an identifier that starts with a keyword + (text.length == keyword.length || !( + ( + (text[keyword.length] >= '0' && text[keyword.length] <= '9') || + (text[keyword.length] >= 'a' && text[keyword.length] <= 'z') || + (text[keyword.length] == '_') || + (text[keyword.length] >= 'A' && text[keyword.length] <= 'Z') + ) + ))) + { + found = true; + if(keyword == "__FILE__") { + token.type = ScriptToken.Type.string; + token.str = to!string(token.scriptFilename); + token.wasSpecial = keyword; + } else if(keyword == "__LINE__") { + token.type = ScriptToken.Type.int_number; + token.str = to!string(token.lineNumber); + token.wasSpecial = keyword; + } else { + token.type = ScriptToken.Type.keyword; + // auto is done as an alias to var in the lexer just so D habits work there too + if(keyword == "auto") { + token.str = "var"; + token.wasSpecial = keyword; + } else + token.str = keyword; + } + advance(keyword.length); + break; + } + + if(!found) { + token.type = ScriptToken.Type.identifier; + int pos; + while(pos < text.length + && ((text[pos] >= 'a' && text[pos] <= 'z') || + (text[pos] == '_') || + (text[pos] >= 'A' && text[pos] <= 'Z') || + (text[pos] >= '0' && text[pos] <= '9'))) + { + pos++; + } + + token.str = text[0 .. pos]; + advance(pos); + } + } else if(text[0] == '"') { + token.type = ScriptToken.Type.string; + int pos = 1; // skip the opening " + bool escaped = false; + // FIXME: escaping doesn't do the right thing lol. we should slice if we can, copy if not + while(pos < text.length && !escaped && text[pos] != '"') { + if(escaped) + escaped = false; + else + if(text[pos] == '\\') + escaped = true; + pos++; + } + + token.str = text[1 .. pos]; + advance(pos + 1); // skip the closing " too + } else { + // let's check all symbols + bool found = false; + foreach(symbol; symbols) + if(text.length >= symbol.length && text[0 .. symbol.length] == symbol) { + + if(symbol == "//") { + // one line comment + int pos = 0; + while(pos < text.length && text[pos] != '\n') + pos++; + advance(pos); + continue mainLoop; + } else if(symbol == "/*") { + int pos = 0; + while(pos + 1 < text.length && text[pos..pos+2] != "*/") + pos++; + + if(pos + 1 == text.length) + throw new ScriptCompileException("unclosed /* */ comment", lineNumber); + + advance(pos + 2); + continue mainLoop; + + } else if(symbol == "/+") { + // FIXME: nesting comment + } + + found = true; + token.type = ScriptToken.Type.symbol; + token.str = symbol; + advance(symbol.length); + break; + } + + if(!found) { + // FIXME: make sure this gives a valid utf-8 sequence + throw new ScriptCompileException("unknown token " ~ text[0], lineNumber); + } + } + + next = token; + return; + } + + textStream.popFront(); + if(!textStream.empty()) { + text = textStream.front; + goto mainLoop; + } + + return; + } + +} + +TokenStream!TextStream lexScript(TextStream)(TextStream textStream, string scriptFilename) if(is(ElementType!TextStream == string)) { + return new TokenStream!TextStream(textStream, scriptFilename); +} + +struct InterpretResult { + var value; + PrototypeObject sc; + enum FlowControl { Normal, Return, Continue, Break } + FlowControl flowControl; +} + +class Expression { + abstract InterpretResult interpret(PrototypeObject sc); +} + +class StringLiteralExpression : Expression { + string literal; + + override string toString() { + import std.string : replace; + return `"` ~ literal.replace("\\", "\\\\").replace("\"", "\\\"") ~ "\""; + } + + this(string s) { + literal = s; + } + + override InterpretResult interpret(PrototypeObject sc) { + return InterpretResult(var(literal), sc); + } +} +class IntLiteralExpression : Expression { + long literal; + + this(string s) { + literal = to!long(s); + } + + override string toString() { return to!string(literal); } + + override InterpretResult interpret(PrototypeObject sc) { + return InterpretResult(var(literal), sc); + } +} +class FloatLiteralExpression : Expression { + this(string s) { + literal = to!real(s); + } + real literal; + override string toString() { return to!string(literal); } + override InterpretResult interpret(PrototypeObject sc) { + return InterpretResult(var(literal), sc); + } +} +class NullLiteralExpression : Expression { + this() {} + override string toString() { return "null"; } + + override InterpretResult interpret(PrototypeObject sc) { + var n; + return InterpretResult(n, sc); + } +} +class ArrayLiteralExpression : Expression { + this() {} + + Expression[] elements; + override InterpretResult interpret(PrototypeObject sc) { + var n = var.emptyArray; + foreach(i, element; elements) + n[i] = element.interpret(sc).value; + return InterpretResult(n, sc); + } +} +class ObjectLiteralExpression : Expression { + Expression[string] elements; + override InterpretResult interpret(PrototypeObject sc) { + var n = var.emptyObject; + foreach(k, v; elements) + n[k] = v.interpret(sc).value; + + return InterpretResult(n, sc); + } +} +class FunctionLiteralExpression : Expression { + this() { + // we want this to not be null at all when we're interpreting since it is used as a comparison for a magic operation + if(DefaultArgumentDummyObject is null) + DefaultArgumentDummyObject = new PrototypeObject(); + } + + /* + function identifier (arg list) expression + + so + var e = function foo() 10; // valid + var e = function foo() { return 10; } // also valid + + // the return value is just the last expression's result that was evaluated + // to return void, be sure to do a "return;" at the end of the function + */ + // PrototypeObject lexicalScope; + VariableDeclaration arguments; + Expression functionBody; // can be a ScopeExpression btw + + override InterpretResult interpret(PrototypeObject sc) { + assert(DefaultArgumentDummyObject !is null); + var v; + v._function = (var _this, var[] args) { + auto argumentsScope = new PrototypeObject(); + argumentsScope.prototype = sc; + + argumentsScope._getMember("this", false, false) = _this; + argumentsScope._getMember("_arguments", false, false) = args; + argumentsScope._getMember("_thisfunc", false, false) = v; + + if(arguments) + foreach(i, identifier; arguments.identifiers) { + argumentsScope._getMember(identifier, false, false); // create it in this scope... + if(i < args.length && !(args[i].payloadType() == var.Type.Object && args[i]._payload._object is DefaultArgumentDummyObject)) + argumentsScope._getMember(identifier, false, true) = args[i]; + else + if(arguments.initializers[i] !is null) + argumentsScope._getMember(identifier, false, true) = arguments.initializers[i].interpret(sc).value; + } + + if(functionBody !is null) + return functionBody.interpret(argumentsScope).value; + else { + assert(0); + } + }; + return InterpretResult(v, sc); + } +} + + +class VariableDeclaration : Expression { + string[] identifiers; + Expression[] initializers; + + this() {} + + override InterpretResult interpret(PrototypeObject sc) { + var n; + + foreach(i, identifier; identifiers) { + n = sc._getMember(identifier, false, false); + auto initializer = initializers[i]; + if(initializer) { + n = initializer.interpret(sc).value; + sc._getMember(identifier, false, false) = n; + } + } + return InterpretResult(n, sc); + } +} + +template CtList(T...) { alias CtList = T; } + +class BinaryExpression : Expression { + string op; + Expression e1; + Expression e2; + + override string toString() { + return e1.toString() ~ " " ~ op ~ " " ~ e2.toString(); + } + + this(string op, Expression e1, Expression e2) { + this.op = op; + this.e1 = e1; + this.e2 = e2; + } + + override InterpretResult interpret(PrototypeObject sc) { + var left = e1.interpret(sc).value; + var right = e2.interpret(sc).value; + + //writeln(left, " "~op~" ", right); + + var n; + foreach(ctOp; CtList!("+", "-", "*", "/", "==", "!=", "<=", ">=", ">", "<", "~")) + if(ctOp == op) { + n = mixin("left "~ctOp~" right"); + } + + return InterpretResult(n, sc); + } +} + +class OpAssignExpression : Expression { + string op; + Expression e1; + Expression e2; + + this(string op, Expression e1, Expression e2) { + this.op = op; + this.e1 = e1; + this.e2 = e2; + } + + override string toString() { + return e1.toString() ~ " " ~ op ~ "= " ~ e2.toString(); + } + + override InterpretResult interpret(PrototypeObject sc) { + + auto v = cast(VariableExpression) e1; + if(v is null) + throw new ScriptRuntimeException("not an lvalue", 0 /* FIXME */); + + var right = e2.interpret(sc).value; + + //writeln(left, " "~op~"= ", right); + + var n; + foreach(ctOp; CtList!("+=", "-=", "*=", "/=", "~=")) + if(ctOp[0..1] == op) + n = mixin("v.getVar(sc) "~ctOp~" right"); + + // FIXME: ensure the variable is updated in scope too + + return InterpretResult(n, sc); + + } +} + +class AssignExpression : Expression { + Expression e1; + Expression e2; + + this(Expression e1, Expression e2) { + this.e1 = e1; + this.e2 = e2; + } + + override InterpretResult interpret(PrototypeObject sc) { + auto v = cast(VariableExpression) e1; + if(v is null) + throw new ScriptRuntimeException("not an lvalue", 0 /* FIXME */); + + auto ret = v.getVar(sc) = e2.interpret(sc).value; + + return InterpretResult(ret, sc); + } +} + + +class UnaryExpression : Expression { + string op; + Expression e; + // FIXME + + override InterpretResult interpret(PrototypeObject sc) { + return InterpretResult(); + } +} + +class VariableExpression : Expression { + string identifier; + + this(string identifier) { + this.identifier = identifier; + } + + override string toString() { + return identifier; + } + + ref var getVar(PrototypeObject sc) { + return sc._getMember(identifier, true, true); + } + + ref var getVarFrom(PrototypeObject sc, ref var v) { + return v[identifier]; + } + + override InterpretResult interpret(PrototypeObject sc) { + return InterpretResult(getVar(sc), sc); + } +} + +class DotVarExpression : VariableExpression { + Expression e1; + VariableExpression e2; + + this(Expression e1) { + this.e1 = e1; + super(null); + } + + this(Expression e1, VariableExpression e2) { + this.e1 = e1; + this.e2 = e2; + //assert(typeid(e2) == typeid(VariableExpression)); + super("");//e1.identifier ~ "." ~ e2.identifier); + } + + override string toString() { + return e1.toString() ~ "." ~ e2.toString(); + } + + override ref var getVar(PrototypeObject sc) { + if(auto ve = cast(VariableExpression) e1) + return this.getVarFrom(sc, ve.getVar(sc)); + else { + // make a temporary for the lhs + auto v = new var(); + *v = e1.interpret(sc).value; + return this.getVarFrom(sc, *v); + } + } + + override ref var getVarFrom(PrototypeObject sc, ref var v) { + return e2.getVarFrom(sc, v); + } +} + +class IndexExpression : VariableExpression { + Expression e1; + Expression e2; + + this(Expression e1, Expression e2) { + this.e1 = e1; + this.e2 = e2; + super(null); + } + + override string toString() { + return e1.toString() ~ "[" ~ e2.toString() ~ "]"; + } + + override ref var getVar(PrototypeObject sc) { + if(auto ve = cast(VariableExpression) e1) + return ve.getVar(sc)[e2.interpret(sc).value]; + else { + auto v = new var(); + *v = e1.interpret(sc).value; + return this.getVarFrom(sc, *v); + } + } +} + + +class ReturnExpression : Expression { + Expression value; + + override InterpretResult interpret(PrototypeObject sc) { + return InterpretResult(value.interpret(sc).value, sc, InterpretResult.FlowControl.Return); + } +} + +class ScopeExpression : Expression { + this(Expression[] expressions) { + this.expressions = expressions; + } + + Expression[] expressions; + + override InterpretResult interpret(PrototypeObject sc) { + var ret; + + auto innerScope = new PrototypeObject(); + innerScope.prototype = sc; + + innerScope._getMember("__scope_exit", false, false) = var.emptyArray; + innerScope._getMember("__scope_success", false, false) = var.emptyArray; + innerScope._getMember("__scope_failure", false, false) = var.emptyArray; + + scope(exit) { + foreach(func; innerScope._getMember("__scope_exit", false, true)) + func(); + } + scope(success) { + foreach(func; innerScope._getMember("__scope_success", false, true)) + func(); + } + scope(failure) { + foreach(func; innerScope._getMember("__scope_failure", false, true)) + func(); + } + + foreach(expression; expressions) { + ret = expression.interpret(innerScope).value; + } + return InterpretResult(ret, sc); + } +} + +class ForExpression : Expression { + Expression initialization; + Expression condition; + Expression advancement; + Expression loopBody; + + this() {} + + override InterpretResult interpret(PrototypeObject sc) { + var result; + + assert(loopBody !is null); + + auto loopScope = new PrototypeObject(); + loopScope.prototype = sc; + if(initialization !is null) + initialization.interpret(loopScope); + + void doLoopBody() { + // FIXME: break, continue + result = loopBody.interpret(loopScope).value; + if(advancement) + advancement.interpret(loopScope); + } + + if(condition !is null) { + while(condition.interpret(loopScope).value) { + doLoopBody(); + } + } else + while(true) { + doLoopBody(); + } + + return InterpretResult(result, sc); + } +} + +class IfExpression : Expression { + Expression condition; + Expression ifTrue; + Expression ifFalse; + + this() {} + + override InterpretResult interpret(PrototypeObject sc) { + var result; + assert(condition !is null); + + auto ifScope = new PrototypeObject(); + ifScope.prototype = sc; + + if(condition.interpret(ifScope).value) { + if(ifTrue !is null) + result = ifTrue.interpret(ifScope).value; + } else { + if(ifFalse !is null) + result = ifFalse.interpret(ifScope).value; + } + return InterpretResult(result, sc); + } +} + +class ThrowExpression : Expression { + Expression whatToThrow; + ScriptToken where; + + this(Expression e, ScriptToken where) { + whatToThrow = e; + this.where = where; + } + + override InterpretResult interpret(PrototypeObject sc) { + assert(whatToThrow !is null); + throw new ScriptException(whatToThrow.interpret(sc).value, where.lineNumber); + assert(0); + } +} + +class ExceptionBlockExpression : Expression { + Expression tryExpression; + + string[] catchVarDecls; + Expression[] catchExpressions; + + Expression[] finallyExpressions; + + override InterpretResult interpret(PrototypeObject sc) { + InterpretResult result; + result.sc = sc; + assert(tryExpression !is null); + assert(catchVarDecls.length == catchExpressions.length); + + if(catchExpressions.length) + try { + result = tryExpression.interpret(sc); + } catch(Exception e) { + // FIXME: this only allows one but it might be nice to actually do different types at some point + foreach(i, ce; catchExpressions) { + auto catchScope = new PrototypeObject(); + catchScope.prototype = sc; + var ex = var.emptyObject; + ex.type = typeid(e).name; + ex.msg = e.msg; + ex.file = e.file; + ex.line = e.line; + catchScope._getMember(catchVarDecls[i], false, false) = ex; + + result = ce.interpret(catchScope); + } + } finally { + foreach(fe; finallyExpressions) + result = fe.interpret(sc); + } + else + try { + result = tryExpression.interpret(sc); + } finally { + foreach(fe; finallyExpressions) + result = fe.interpret(sc); + } + + return result; + } +} + +class ParentheticalExpression : Expression { + Expression inside; + this(Expression inside) { + this.inside = inside; + } + + override InterpretResult interpret(PrototypeObject sc) { + return InterpretResult(inside.interpret(sc).value, sc); + } +} + +PrototypeObject DefaultArgumentDummyObject; + +class CallExpression : Expression { + Expression func; + Expression[] arguments; + + override string toString() { + string s = func.toString() ~ "("; + foreach(i, arg; arguments) { + if(i) s ~= ", "; + s ~= arg.toString(); + } + + s ~= ")"; + return s; + } + + this(Expression func) { + this.func = func; + } + + override InterpretResult interpret(PrototypeObject sc) { + auto f = func.interpret(sc).value; + var[] args; + foreach(argument; arguments) + if(argument !is null) { + args ~= argument.interpret(sc).value; + } else { + if(DefaultArgumentDummyObject is null) + DefaultArgumentDummyObject = new PrototypeObject(); + + var dummy; + dummy._object = DefaultArgumentDummyObject; + + args ~= dummy; + } + + var _this; + if(auto dve = cast(DotVarExpression) func) + _this = dve.interpret(sc).value; + else if(auto ide = cast(IndexExpression) func) + _this = ide.interpret(sc).value; + + return InterpretResult(f.apply(var(_this), args), sc); + } +} + +ScriptToken requireNextToken(MyTokenStreamHere)(ref MyTokenStreamHere tokens, ScriptToken.Type type, string str = null, string file = __FILE__, size_t line = __LINE__) { + if(tokens.empty) + throw new ScriptCompileException("script ended prematurely", 0, file, line); + auto next = tokens.front; + if(next.type != type || (str !is null && next.str != str)) + throw new ScriptCompileException("unexpected '"~next.str~"'", next.lineNumber, file, line); + + tokens.popFront(); + return next; +} + +bool peekNextToken(MyTokenStreamHere)(MyTokenStreamHere tokens, ScriptToken.Type type, string str = null, string file = __FILE__, size_t line = __LINE__) { + if(tokens.empty) + return false; + auto next = tokens.front; + if(next.type != type || (str !is null && next.str != str)) + return false; + return true; +} + +VariableExpression parseVariableName(MyTokenStreamHere)(ref MyTokenStreamHere tokens) { + assert(!tokens.empty); + auto token = tokens.front; + if(token.type == ScriptToken.Type.identifier) { + tokens.popFront(); + return new VariableExpression(token.str); + } + throw new ScriptCompileException("Found "~token.str~" when expecting identifier", token.lineNumber); +} + +Expression parsePart(MyTokenStreamHere)(ref MyTokenStreamHere tokens) { + if(!tokens.empty) { + auto token = tokens.front; + + Expression e; + if(token.type == ScriptToken.Type.identifier) + e = parseVariableName(tokens); + else { + tokens.popFront(); + + if(token.type == ScriptToken.Type.int_number) + e = new IntLiteralExpression(token.str); + else if(token.type == ScriptToken.Type.float_number) + e = new FloatLiteralExpression(token.str); + else if(token.type == ScriptToken.Type.string) + e = new StringLiteralExpression(token.str); + else if(token.type == ScriptToken.Type.symbol || token.type == ScriptToken.Type.keyword) { + switch(token.str) { + case "[": + // array literal + auto arr = new ArrayLiteralExpression(); + + bool first = true; + moreElements: + if(tokens.empty) + throw new ScriptCompileException("unexpected end of file when reading array literal", token.lineNumber); + + auto peek = tokens.front; + if(peek.type == ScriptToken.Type.symbol && peek.str == "]") { + tokens.popFront(); + return arr; + } + + if(!first) + tokens.requireNextToken(ScriptToken.Type.symbol, ","); + else + first = false; + + arr.elements ~= parseExpression(tokens); + + goto moreElements; + break; + case "json!q{": + // json object literal + auto obj = new ObjectLiteralExpression(); + /* + these go + + string or ident which is the key + then a colon + then an expression which is the value + + then optionally a comma + + then either } which finishes it, or another key + */ + + if(tokens.empty) + throw new ScriptCompileException("unexpected end of file when reading object literal", token.lineNumber); + + moreKeys: + auto key = tokens.front; + tokens.popFront(); + if(key.type == ScriptToken.Type.symbol && key.str == "}") { + // all done! + e = obj; + break; + } + if(key.type != ScriptToken.Type.string && key.type != ScriptToken.Type.identifier) { + throw new ScriptCompileException("unexpected '"~key.str~"' when reading object literal", key.lineNumber); + + } + + tokens.requireNextToken(ScriptToken.Type.symbol, ":"); + + auto value = parseExpression(tokens); + if(tokens.empty) + throw new ScriptCompileException("unclosed object literal", key.lineNumber); + + if(tokens.peekNextToken(ScriptToken.Type.symbol, ",")) + tokens.popFront(); + + obj.elements[key.str] = value; + + goto moreKeys; + break; + case "function": + tokens.requireNextToken(ScriptToken.Type.symbol, "("); + + auto exp = new FunctionLiteralExpression(); + if(!tokens.peekNextToken(ScriptToken.Type.symbol, ")")) + exp.arguments = parseVariableDeclaration(tokens, ")"); + + tokens.requireNextToken(ScriptToken.Type.symbol, ")"); + + exp.functionBody = parseExpression(tokens); + + e = exp; + break; + case "null": + e = new NullLiteralExpression(); + break; + default: + goto unknown; + } + } else { + unknown: + throw new ScriptCompileException("unexpected '"~token.str~"' when reading ident", token.lineNumber); + } + } + + funcLoop: while(!tokens.empty) { + auto peek = tokens.front; + if(peek.type == ScriptToken.Type.symbol) { + switch(peek.str) { + case "(": + e = parseFunctionCall(tokens, e); + break; + case "[": + tokens.popFront(); + e = new IndexExpression(e, parseExpression(tokens)); + tokens.requireNextToken(ScriptToken.Type.symbol, "]"); + break; + case ".": + tokens.popFront(); + e = new DotVarExpression(e, parseVariableName(tokens)); + break; + default: + return e; // we don't know, punt it elsewhere + } + } else return e; // again, we don't know, so just punt it down the line + } + return e; + } + assert(0, to!string(tokens)); +} + +Expression parseFunctionCall(MyTokenStreamHere)(ref MyTokenStreamHere tokens, Expression e) { + assert(!tokens.empty); + auto peek = tokens.front; + auto exp = new CallExpression(e); + tokens.popFront(); + if(tokens.empty) + throw new ScriptCompileException("unexpected end of file when parsing call expression", peek.lineNumber); + // arguments. + peek = tokens.front; + if(peek.type == ScriptToken.Type.symbol && peek.str == ")") { + tokens.popFront(); + return exp; + } + + moreArguments: + + if(tokens.peekNextToken(ScriptToken.Type.keyword, "default")) { + tokens.popFront(); + exp.arguments ~= null; + } else { + exp.arguments ~= parseExpression(tokens); + } + + if(tokens.empty) + throw new ScriptCompileException("unexpected end of file when parsing call expression", peek.lineNumber); + peek = tokens.front; + if(peek.type == ScriptToken.Type.symbol && peek.str == ",") { + tokens.popFront(); + goto moreArguments; + } else if(peek.type == ScriptToken.Type.symbol && peek.str == ")") { + tokens.popFront(); + return exp; + } else + throw new ScriptCompileException("unexpected '"~peek.str~"' when reading argument list", peek.lineNumber); +} + +Expression parseFactor(MyTokenStreamHere)(ref MyTokenStreamHere tokens) { + auto e1 = parsePart(tokens); + loop: while(!tokens.empty) { + auto peek = tokens.front; + + if(peek.type == ScriptToken.Type.symbol) { + switch(peek.str) { + case "*": + case "/": + tokens.popFront(); + e1 = new BinaryExpression(peek.str, e1, parsePart(tokens)); + break; + default: + break loop; + } + } + } + + return e1; +} + +Expression parseAddend(MyTokenStreamHere)(ref MyTokenStreamHere tokens) { + auto e1 = parseFactor(tokens); + loop: while(!tokens.empty) { + auto peek = tokens.front; + + if(peek.type == ScriptToken.Type.symbol) { + switch(peek.str) { + case ")": // possible FIXME + case "]": // possible FIXME + case "}": // possible FIXME + case ",": // possible FIXME these are passed on to the next thing + case ";": + return e1; + case "=": + tokens.popFront(); + return new AssignExpression(e1, parseAddend(tokens)); + case "~": + case "+": + case "-": + + case "==": + case "!=": + case "<=": + case ">=": + case "<": + case ">": + tokens.popFront(); + e1 = new BinaryExpression(peek.str, e1, parseFactor(tokens)); + break; + case "+=": + case "-=": + case "*=": + case "/=": + case "~=": + tokens.popFront(); + return new OpAssignExpression(peek.str[0..1], e1, parseExpression(tokens)); + default: + throw new ScriptCompileException("Parse error, unexpected " ~ peek.str ~ " when looking for operator", peek.lineNumber); + } + //} else if(peek.type == ScriptToken.Type.identifier || peek.type == ScriptToken.Type.number) { + //return parseFactor(tokens); + } else + throw new ScriptCompileException("Parse error, unexpected '" ~ peek.str ~ "'", peek.lineNumber); + } + + return e1; +} + +Expression parseExpression(MyTokenStreamHere)(ref MyTokenStreamHere tokens) { + Expression ret; + ScriptToken first; + string expectedEnd = ";"; + //auto e1 = parseFactor(tokens); + if(!tokens.empty) { + first = tokens.front; + if(tokens.peekNextToken(ScriptToken.Type.symbol, "{")) { + auto start = tokens.front; + tokens.popFront(); + auto e = parseCompoundStatement(tokens, start.lineNumber, "}").array; + ret = new ScopeExpression(e); + expectedEnd = null; // {} don't need ; at the end + } else if(tokens.peekNextToken(ScriptToken.Type.keyword, "scope")) { + auto start = tokens.front; + tokens.popFront(); + tokens.requireNextToken(ScriptToken.Type.symbol, "("); + + auto ident = tokens.requireNextToken(ScriptToken.Type.identifier); + switch(ident.str) { + case "success": + case "failure": + case "exit": + break; + default: + throw new ScriptCompileException("unexpected " ~ ident.str ~ ". valid scope(idents) are success, failure, and exit", ident.lineNumber); + } + + tokens.requireNextToken(ScriptToken.Type.symbol, ")"); + + string i = "__scope_" ~ ident.str; + auto literal = new FunctionLiteralExpression(); + literal.functionBody = parseExpression(tokens); + + auto e = new OpAssignExpression("~", new VariableExpression(i), literal); + return e; + } else if(tokens.peekNextToken(ScriptToken.Type.symbol, "(")) { + auto start = tokens.front; + tokens.popFront(); + auto parenthetical = new ParentheticalExpression(parseExpression(tokens)); + tokens.requireNextToken(ScriptToken.Type.symbol, ")"); + if(tokens.peekNextToken(ScriptToken.Type.symbol, "(")) { + // we have a function call, e.g. (test)() + ret = parseFunctionCall(tokens, parenthetical); + } else + ret = parenthetical; + } else if(tokens.peekNextToken(ScriptToken.Type.keyword, "if")) { + tokens.popFront(); + auto e = new IfExpression(); + e.condition = parseExpression(tokens); + e.ifTrue = parseExpression(tokens); + if(tokens.peekNextToken(ScriptToken.Type.symbol, ";")) { + tokens.popFront(); + } + if(tokens.peekNextToken(ScriptToken.Type.keyword, "else")) { + tokens.popFront(); + e.ifFalse = parseExpression(tokens); + } + ret = e; + } else if(tokens.peekNextToken(ScriptToken.Type.keyword, "for")) { + tokens.popFront(); + auto e = new ForExpression(); + tokens.requireNextToken(ScriptToken.Type.symbol, "("); + e.initialization = parseStatement(tokens, ";"); + + tokens.requireNextToken(ScriptToken.Type.symbol, ";"); + + e.condition = parseExpression(tokens); + tokens.requireNextToken(ScriptToken.Type.symbol, ";"); + e.advancement = parseExpression(tokens); + tokens.requireNextToken(ScriptToken.Type.symbol, ")"); + e.loopBody = parseExpression(tokens); + + ret = e; + + expectedEnd = ""; + } else if(tokens.peekNextToken(ScriptToken.Type.keyword, "while")) { + tokens.popFront(); + auto e = new ForExpression(); + e.condition = parseExpression(tokens); + e.loopBody = parseExpression(tokens); + ret = e; + expectedEnd = ""; + } else if(tokens.peekNextToken(ScriptToken.Type.keyword, "throw")) { + auto token = tokens.front; + tokens.popFront(); + ret = new ThrowExpression(parseExpression(tokens), token); + } else if(tokens.peekNextToken(ScriptToken.Type.keyword, "try")) { + auto tryToken = tokens.front; + auto e = new ExceptionBlockExpression(); + tokens.popFront(); + e.tryExpression = parseExpression(tokens); + bool hadSomething = false; + while(tokens.peekNextToken(ScriptToken.Type.keyword, "catch")) { + if(hadSomething) + throw new ScriptCompileException("Only one catch block is allowed currently ", tokens.front.lineNumber); + hadSomething = true; + tokens.popFront(); + tokens.requireNextToken(ScriptToken.Type.symbol, "("); + if(tokens.peekNextToken(ScriptToken.Type.keyword, "var")) + tokens.popFront(); + auto ident = tokens.requireNextToken(ScriptToken.Type.identifier); + e.catchVarDecls ~= ident.str; + tokens.requireNextToken(ScriptToken.Type.symbol, ")"); + e.catchExpressions ~= parseExpression(tokens); + } + while(tokens.peekNextToken(ScriptToken.Type.keyword, "finally")) { + hadSomething = true; + tokens.popFront(); + e.finallyExpressions ~= parseExpression(tokens); + } + + if(!hadSomething) + throw new ScriptCompileException("Parse error, missing finally or catch after try", tryToken.lineNumber); + + ret = e; + } else + ret = parseAddend(tokens); + } else { + assert(0); + return null; + // throw new ScriptCompileException("Parse error, unexpected end of input when reading expression", token.lineNumber); + } + + //writeln("parsed expression ", ret.toString()); + + if(tokens.empty) + throw new ScriptCompileException("Parse error, unexpected end of input when reading expression", first.lineNumber); + + if(expectedEnd.length) { + // FIXME + //if(tokens.front.type != ScriptToken.Type.symbol && tokens.front.str != expectedEnd) + //throw new ScriptCompileException("Parse error, missing "~expectedEnd~" at end of expression (starting on "~to!string(first.lineNumber)~"). Saw "~tokens.front.str~" instead", tokens.front.lineNumber); + // tokens = tokens[1 .. $]; + } + + return ret; +} + +VariableDeclaration parseVariableDeclaration(MyTokenStreamHere)(ref MyTokenStreamHere tokens, string termination) { + VariableDeclaration decl = new VariableDeclaration(); + bool equalOk; + anotherVar: + assert(!tokens.empty); + + auto firstToken = tokens.front; + + // var a, var b is acceptable + if(tokens.peekNextToken(ScriptToken.Type.keyword, "var")) + tokens.popFront(); + + equalOk= true; + if(tokens.empty) + throw new ScriptCompileException("Parse error, dangling var at end of file", firstToken.lineNumber); + + Expression initializer; + auto identifier = tokens.front; + if(identifier.type != ScriptToken.Type.identifier) + throw new ScriptCompileException("Parse error, found '"~identifier.str~"' when expecting var identifier", identifier.lineNumber); + + tokens.popFront(); + + tryTermination: + if(tokens.empty) + throw new ScriptCompileException("Parse error, missing ; after var declaration at end of file", firstToken.lineNumber); + + auto peek = tokens.front; + if(peek.type == ScriptToken.Type.symbol) { + if(peek.str == "=") { + if(!equalOk) + throw new ScriptCompileException("Parse error, unexpected '"~identifier.str~"' after reading var initializer", peek.lineNumber); + equalOk = false; + tokens.popFront(); + initializer = parseExpression(tokens); + goto tryTermination; + } else if(peek.str == ",") { + tokens.popFront(); + decl.identifiers ~= identifier.str; + decl.initializers ~= initializer; + goto anotherVar; + } else if(peek.str == termination) { + decl.identifiers ~= identifier.str; + decl.initializers ~= initializer; + //tokens = tokens[1 .. $]; + // we're done! + } else + throw new ScriptCompileException("Parse error, unexpected '"~peek.str~"' when reading var declaration", peek.lineNumber); + } else + throw new ScriptCompileException("Parse error, unexpected '"~peek.str~"' when reading var declaration", peek.lineNumber); + + return decl; +} + +Expression parseStatement(MyTokenStreamHere)(ref MyTokenStreamHere tokens, string terminatingSymbol = null) { + skip: // FIXME + if(tokens.empty) + return null; + + if(terminatingSymbol !is null && (tokens.front.type == ScriptToken.Type.symbol && tokens.front.str == terminatingSymbol)) + return null; // we're done + + auto token = tokens.front; + + // tokens = tokens[1 .. $]; + final switch(token.type) { + case ScriptToken.Type.keyword: + case ScriptToken.Type.symbol: + switch(token.str) { + // declarations + case "var": + return parseVariableDeclaration(tokens, ";"); + case ";": + tokens.popFront(); // FIXME + goto skip; + // literals + case "function": + // function can be a literal, or a declaration. + + tokens.popFront(); // we're peeking ahead + + if(tokens.peekNextToken(ScriptToken.Type.identifier)) { + // decl style, rewrite it into var ident = function style + // tokens.popFront(); // skipping the function keyword // already done above with the popFront + auto ident = tokens.front; + tokens.popFront(); + + tokens.requireNextToken(ScriptToken.Type.symbol, "("); + + auto exp = new FunctionLiteralExpression(); + if(!tokens.peekNextToken(ScriptToken.Type.symbol, ")")) + exp.arguments = parseVariableDeclaration(tokens, ")"); + tokens.requireNextToken(ScriptToken.Type.symbol, ")"); + + exp.functionBody = parseExpression(tokens); + + // a ; should NOT be required here btw + + auto e = new VariableDeclaration(); + e.identifiers ~= ident.str; + e.initializers ~= exp; + + return e; + + } else { + tokens.pushFront(token); // put it back since everyone expects us to have done that + goto case; // handle it like any other expression + } + case "json!{": + case "[": + case "(": + case "null": + + // scope + case "{": + case "scope": + + // flow control + case "if": + case "while": + case "for": + case "foreach": + + // exceptions + case "try": + case "throw": + return parseExpression(tokens); + case "return": + + // unary prefix operators + case "!": + case "~": + case "-": + + // BTW add custom object operator overloading to struct var + // and custom property overloading to PrototypeObject + + default: + // whatever else keyword or operator related is actually illegal here + throw new ScriptCompileException("Parse error, unexpected " ~ token.str, token.lineNumber); + } + break; + case ScriptToken.Type.identifier: + case ScriptToken.Type.string: + case ScriptToken.Type.int_number: + case ScriptToken.Type.float_number: + return parseExpression(tokens); + break; + } + + assert(0); +} + +struct CompoundStatementRange(MyTokenStreamHere) { + // FIXME: if MyTokenStreamHere is not a class, this fails! + MyTokenStreamHere tokens; + int startingLine; + string terminatingSymbol; + bool isEmpty; + + this(MyTokenStreamHere t, int startingLine, string terminatingSymbol) { + tokens = t; + this.startingLine = startingLine; + this.terminatingSymbol = terminatingSymbol; + popFront(); + } + + bool empty() { + return isEmpty; + } + + Expression got; + + Expression front() { + return got; + } + + void popFront() { + while(!tokens.empty && (terminatingSymbol is null || !(tokens.front.type == ScriptToken.Type.symbol && tokens.front.str == terminatingSymbol))) { + auto n = parseStatement(tokens, terminatingSymbol); + if(n is null) + continue; + got = n; + return; + } + + if(tokens.empty && terminatingSymbol !is null) { + throw new ScriptCompileException("Reached end of file while trying to reach matching " ~ terminatingSymbol, startingLine); + } + + if(terminatingSymbol !is null) { + assert(tokens.front.str == terminatingSymbol); + tokens.popFront(); + } + + isEmpty = true; + } +} + +CompoundStatementRange!MyTokenStreamHere +//Expression[] +parseCompoundStatement(MyTokenStreamHere)(ref MyTokenStreamHere tokens, int startingLine = 1, string terminatingSymbol = null) { + return (CompoundStatementRange!MyTokenStreamHere(tokens, startingLine, terminatingSymbol)); +} + +auto parseScript(MyTokenStreamHere)(MyTokenStreamHere tokens) { + /* + the language's grammar is simple enough + + maybe flow control should be statements though lol. they might not make sense inside. + + Expressions: + var identifier; + var identifier = initializer; + var identifier, identifier2 + + return expression; + return ; + + json!{ object literal } + + { scope expression } + + [ array literal ] + other literal + function (arg list) other expression + + ( expression ) // parenthesized expression + operator expression // unary expression + + expression operator expression // binary expression + expression (other expression... args) // function call + + Binary Operator precedence : + . [] + * / + + - + ~ + < > == != + = + */ + + return parseCompoundStatement(tokens); +} + +var interpretExpressions(ExpressionStream)(ExpressionStream expressions, PrototypeObject variables) if(is(ElementType!ExpressionStream == Expression)) { + assert(variables !is null); + var ret; + foreach(expression; expressions) { + auto res = expression.interpret(variables); + variables = res.sc; + ret = res.value; + } + return ret; +} + +var interpretStream(MyTokenStreamHere)(MyTokenStreamHere tokens, PrototypeObject variables) if(is(ElementType!MyTokenStreamHere == ScriptToken)) { + assert(variables !is null); + return interpretExpressions(parseScript(tokens), variables); +} + +var interpretStream(MyTokenStreamHere)(MyTokenStreamHere tokens, var variables) if(is(ElementType!MyTokenStreamHere == ScriptToken)) { + return interpretExpressions(parseScript(tokens), + (variables.payloadType() == var.Type.Object && variables._payload._object !is null) ? variables._payload._object : new PrototypeObject()); +} + +var interpret(string code, PrototypeObject variables, string scriptFilename = null) { + assert(variables !is null); + return interpretStream(lexScript(repeat(code, 1), scriptFilename), variables); +} + +var interpret(string code, var variables = null, string scriptFilename = null) { + return interpretStream( + lexScript(repeat(code, 1), scriptFilename), + (variables.payloadType() == var.Type.Object && variables._payload._object !is null) ? variables._payload._object : new PrototypeObject()); +} + +void repl(var globals) { + import std.stdio; + import std.algorithm; + auto variables = (globals.payloadType() == var.Type.Object && globals._payload._object !is null) ? globals._payload._object : new PrototypeObject(); + + // we chain to ensure the priming popFront succeeds so we don't throw here + auto tokens = lexScript( + chain(["var __skipme = 0;"], map!((a) => a.idup)(stdin.byLine)) + , "stdin"); + auto expressions = parseScript(tokens); + + while(!expressions.empty) { + try { + expressions.popFront; + auto expression = expressions.front; + auto res = expression.interpret(variables); + variables = res.sc; + writeln(">>> ", res.value); + } catch(ScriptCompileException e) { + writeln("*+* ", e.msg); + tokens.popFront(); // skip the one we threw on... + } catch(Exception e) { + writeln("*** ", e.msg); + } + } +}