mirror of https://github.com/adamdruppe/arsd.git
3875 lines
109 KiB
D
3875 lines
109 KiB
D
// dmd -g -ofscripttest -unittest -main script.d jsvar.d && ./scripttest
|
|
/*
|
|
|
|
FIXME: fix `(new A()).b`
|
|
|
|
|
|
FIXME: i kinda do want a catch type filter e.g. catch(Exception f)
|
|
and perhaps overloads
|
|
|
|
|
|
|
|
For type annotations, maybe it can statically match later, but right now
|
|
it just forbids any assignment to that variable that isn't that type.
|
|
|
|
I'll have to define int, float, etc though as basic types.
|
|
|
|
|
|
|
|
FIXME: I also kinda want implicit construction of structs at times.
|
|
|
|
REPL plan:
|
|
easy movement to/from a real editor
|
|
can edit a specific function
|
|
repl is a different set of globals
|
|
maybe ctrl+enter to execute vs insert another line
|
|
|
|
|
|
write state to file
|
|
read state from file
|
|
state consists of all variables and source to functions.
|
|
maybe need @retained for a variable that is meant to keep
|
|
its value between loads?
|
|
|
|
ddoc????
|
|
udas?!?!?!
|
|
|
|
Steal Ruby's [regex, capture] maybe
|
|
|
|
and the => operator too
|
|
|
|
I kinda like the javascript foo`blargh` template literals too.
|
|
|
|
++ and -- are not implemented.
|
|
|
|
*/
|
|
|
|
/++
|
|
A small script interpreter that builds on [arsd.jsvar] to be easily embedded inside and to have has easy
|
|
two-way interop with the host D program. The script language it implements is based on a hybrid of D and Javascript.
|
|
The type the language uses is based directly on [var] from [arsd.jsvar].
|
|
|
|
The interpreter is slightly buggy and poorly documented, but the basic functionality works well and much of
|
|
your existing knowledge from Javascript will carry over, making it hopefully easy to use right out of the box.
|
|
See the [#examples] to quickly get the feel of the script language as well as the interop.
|
|
|
|
I haven't benchmarked it, but I expect it is pretty slow. My goal is to see what is possible for easy interoperability
|
|
with dynamic functionality and D rather than speed.
|
|
|
|
|
|
$(TIP
|
|
A goal of this language is to blur the line between D and script, but
|
|
in the examples below, which are generated from D unit tests,
|
|
the non-italics code is D, and the italics is the script. Notice
|
|
how it is a string passed to the [interpret] function.
|
|
|
|
In some smaller, stand-alone code samples, there will be a tag "adrscript"
|
|
in the upper right of the box to indicate it is script. Otherwise, it
|
|
is D.
|
|
)
|
|
|
|
Installation_instructions:
|
|
This script interpreter is contained entirely in two files: jsvar.d and script.d. Download both of them
|
|
and add them to your project. Then, `import arsd.script;`, declare and populate a `var globals = var.emptyObject;`,
|
|
and `interpret("some code", globals);` in D.
|
|
|
|
There's nothing else to it, no complicated build, no external dependencies.
|
|
|
|
$(CONSOLE
|
|
$ wget https://raw.githubusercontent.com/adamdruppe/arsd/master/script.d
|
|
$ wget https://raw.githubusercontent.com/adamdruppe/arsd/master/jsvar.d
|
|
|
|
$ dmd yourfile.d script.d jsvar.d
|
|
)
|
|
|
|
Script_features:
|
|
|
|
OVERVIEW
|
|
$(LIST
|
|
* Can subclass D objects in script. See [http://dpldocs.info/this-week-in-d/Blog.Posted_2020_04_27.html#subclasses-in-script
|
|
* 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.
|
|
* mostly familiar syntax, hybrid of D and Javascript
|
|
* simple implementation is moderately small and fairly easy to hack on (though it gets messier by the day), but it isn't made for speed.
|
|
)
|
|
|
|
SPECIFICS
|
|
$(LIST
|
|
// * Allows identifiers-with-dashes. To do subtraction, put spaces around the minus sign.
|
|
* Allows identifiers starting with a dollar sign.
|
|
* string literals come in "foo" or 'foo', like Javascript, or `raw string` like D. Also come as “nested “double quotes” are an option!”
|
|
* double quoted string literals can do Ruby-style interpolation: "Hello, #{name}".
|
|
* mixin aka eval (does it at runtime, so more like eval than mixin, but I want it to look like D)
|
|
* scope guards, like in D
|
|
* Built-in assert() which prints its source and its arguments
|
|
* try/catch/finally/throw
|
|
You can use try as an expression without any following catch to return the exception:
|
|
|
|
```adrscript
|
|
var a = try throw "exception";; // the double ; is because one closes the try, the second closes the var
|
|
// a is now the thrown exception
|
|
```
|
|
* for/while/foreach
|
|
* D style operators: +-/* on all numeric types, ~ on strings and arrays, |&^ on integers.
|
|
Operators can coerce types as needed: 10 ~ "hey" == "10hey". 10 + "3" == 13.
|
|
Any math, except bitwise math, with a floating point component returns a floating point component, but pure int math is done as ints (unlike Javascript btw).
|
|
Any bitwise math coerces to int.
|
|
|
|
So you can do some type coercion like this:
|
|
|
|
```adrscript
|
|
a = a|0; // forces to int
|
|
a = "" ~ a; // forces to string
|
|
a = a+0.0; // coerces to float
|
|
```
|
|
|
|
Though casting is probably better.
|
|
* Type coercion via cast, similarly to D.
|
|
```adrscript
|
|
var a = "12";
|
|
a.typeof == "String";
|
|
a = cast(int) a;
|
|
a.typeof == "Integral";
|
|
a == 12;
|
|
```
|
|
|
|
Supported types for casting to: int/long (both actually an alias for long, because of how var works), float/double/real, string, char/dchar (these return *integral* types), and arrays, int[], string[], and float[].
|
|
|
|
This forwards directly to the D function var.opCast.
|
|
|
|
* some operator overloading on objects, passing opBinary(op, rhs), length, and perhaps others through like they would be in D.
|
|
opIndex(name)
|
|
opIndexAssign(value, name) // same order as D, might some day support [n1, n2] => (value, n1, n2)
|
|
|
|
obj.__prop("name", value); // bypasses operator overloading, useful for use inside the opIndexAssign especially
|
|
|
|
Note: if opIndex is not overloaded, getting a non-existent member will actually add it to the member. This might be a bug but is needed right now in the D impl for nice chaining. Or is it? FIXME
|
|
|
|
FIXME: it doesn't do opIndex with multiple args.
|
|
* if/else
|
|
* array slicing, but note that slices are rvalues currently
|
|
* variables must start with A-Z, a-z, _, or $, then must be [A-Za-z0-9_]*.
|
|
(The $ can also stand alone, and this is a special thing when slicing, so you probably shouldn't use it at all.).
|
|
Variable names that start with __ are reserved and you shouldn't use them.
|
|
* int, float, string, array, bool, and `#{}` (previously known as `json!q{}` aka object) literals
|
|
* var.prototype, var.typeof. prototype works more like Mozilla's __proto__ than standard javascript prototype.
|
|
* the `|>` pipeline operator
|
|
* classes:
|
|
```adrscript
|
|
// inheritance works
|
|
class Foo : bar {
|
|
// constructors, D style
|
|
this(var a) { ctor.... }
|
|
|
|
// static vars go on the auto created prototype
|
|
static var b = 10;
|
|
|
|
// instance vars go on this instance itself
|
|
var instancevar = 20;
|
|
|
|
// "virtual" functions can be overridden kinda like you expect in D, though there is no override keyword
|
|
function virt() {
|
|
b = 30; // lexical scoping is supported for static variables and functions
|
|
|
|
// but be sure to use this. as a prefix for any class defined instance variables in here
|
|
this.instancevar = 10;
|
|
}
|
|
}
|
|
|
|
var foo = new Foo(12);
|
|
|
|
foo.newFunc = function() { this.derived = 0; }; // this is ok too, and scoping, including 'this', works like in Javascript
|
|
```
|
|
|
|
You can also use 'new' on another object to get a copy of it.
|
|
* return, break, continue, but currently cannot do labeled breaks and continues
|
|
* __FILE__, __LINE__, but currently not as default arguments for D behavior (they always evaluate at the definition point)
|
|
* most everything are expressions, though note this is pretty buggy! But as a consequence:
|
|
for(var a = 0, b = 0; a < 10; a+=1, b+=1) {}
|
|
won't work but this will:
|
|
for(var a = 0, b = 0; a < 10; {a+=1; b+=1}) {}
|
|
|
|
You can encase things in {} anywhere instead of a comma operator, and it works kinda similarly.
|
|
|
|
{} creates a new scope inside it and returns the last value evaluated.
|
|
* functions:
|
|
var fn = function(args...) expr;
|
|
or
|
|
function fn(args....) expr;
|
|
|
|
Special function local variables:
|
|
_arguments = var[] of the arguments passed
|
|
_thisfunc = reference to the function itself
|
|
this = reference to the object on which it is being called - note this is like Javascript, not D.
|
|
|
|
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
|
|
* macros:
|
|
A macro is defined just like a function, except with the
|
|
macro keyword instead of the function keyword. The difference
|
|
is a macro must interpret its own arguments - it is passed
|
|
AST objects instead of values. Still a WIP.
|
|
)
|
|
|
|
|
|
Todo_list:
|
|
|
|
I also have a wishlist here that I may do in the future, but don't expect them any time soon.
|
|
|
|
FIXME: maybe some kind of splat operator too. choose([1,2,3]...) expands to choose(1,2,3)
|
|
|
|
make sure superclass ctors are called
|
|
|
|
FIXME: prettier stack trace when sent to D
|
|
|
|
FIXME: support more escape things in strings like \n, \t etc.
|
|
|
|
FIXME: add easy to use premade packages for the global object.
|
|
|
|
FIXME: the debugger statement from javascript might be cool to throw in too.
|
|
|
|
FIXME: add continuations or something too - actually doing it with fibers works pretty well
|
|
|
|
FIXME: Also ability to get source code for function something so you can mixin.
|
|
|
|
FIXME: add COM support on Windows ????
|
|
|
|
|
|
Might be nice:
|
|
varargs
|
|
lambdas - maybe without function keyword and the x => foo syntax from D.
|
|
|
|
Author:
|
|
Adam D. Ruppe
|
|
|
|
History:
|
|
November 17, 2023: added support for hex, octal, and binary literals and added _ separators in numbers.
|
|
|
|
September 1, 2020: added overloading for functions and type matching in `catch` blocks among other bug fixes
|
|
|
|
April 28, 2020: added `#{}` as an alternative to the `json!q{}` syntax for object literals. Also fixed unary `!` operator.
|
|
|
|
April 26, 2020: added `switch`, fixed precedence bug, fixed doc issues and added some unittests
|
|
|
|
Started writing it in July 2013. Yes, a basic precedence issue was there for almost SEVEN YEARS. You can use this as a toy but please don't use it for anything too serious, it really is very poorly written and not intelligently designed at all.
|
|
+/
|
|
module arsd.script;
|
|
|
|
/++
|
|
This example shows the basics of how to interact with the script.
|
|
The string enclosed in `q{ .. }` is the script language source.
|
|
|
|
The [var] type comes from [arsd.jsvar] and provides a dynamic type
|
|
to D. It is the same type used in the script language and is weakly
|
|
typed, providing operator overloads to work with many D types seamlessly.
|
|
|
|
However, if you do need to convert it to a static type, such as if passing
|
|
to a function, you can use `get!T` to get a static type out of it.
|
|
+/
|
|
unittest {
|
|
var globals = var.emptyObject;
|
|
globals.x = 25; // we can set variables on the global object
|
|
globals.name = "script.d"; // of various types
|
|
// and we can make native functions available to the script
|
|
globals.sum = (int a, int b) {
|
|
return a + b;
|
|
};
|
|
|
|
// This is the source code of the script. It is similar
|
|
// to javascript with pieces borrowed from D, so should
|
|
// be pretty familiar.
|
|
string scriptSource = q{
|
|
function foo() {
|
|
return 13;
|
|
}
|
|
|
|
var a = foo() + 12;
|
|
assert(a == 25);
|
|
|
|
// you can also access the D globals from the script
|
|
assert(x == 25);
|
|
assert(name == "script.d");
|
|
|
|
// as well as call D functions set via globals:
|
|
assert(sum(5, 6) == 11);
|
|
|
|
// I will also set a function to call from D
|
|
function bar(str) {
|
|
// unlike Javascript though, we use the D style
|
|
// concatenation operator.
|
|
return str ~ " concatenation";
|
|
}
|
|
};
|
|
|
|
// once you have the globals set up, you call the interpreter
|
|
// with one simple function.
|
|
interpret(scriptSource, globals);
|
|
|
|
// finally, globals defined from the script are accessible here too:
|
|
// however, notice the two sets of parenthesis: the first is because
|
|
// @property is broken in D. The second set calls the function and you
|
|
// can pass values to it.
|
|
assert(globals.foo()() == 13);
|
|
|
|
assert(globals.bar()("test") == "test concatenation");
|
|
|
|
// this shows how to convert the var back to a D static type.
|
|
int x = globals.x.get!int;
|
|
}
|
|
|
|
/++
|
|
$(H3 Macros)
|
|
|
|
Macros are like functions, but instead of evaluating their arguments at
|
|
the call site and passing value, the AST nodes are passed right in. Calling
|
|
the node evaluates the argument and yields the result (this is similar to
|
|
to `lazy` parameters in D), and they also have methods like `toSourceCode`,
|
|
`type`, and `interpolate`, which forwards to the given string.
|
|
|
|
The language also supports macros and custom interpolation functions. This
|
|
example shows an interpolation string being passed to a macro and used
|
|
with a custom interpolation string.
|
|
|
|
You might use this to encode interpolated things or something like that.
|
|
+/
|
|
unittest {
|
|
var globals = var.emptyObject;
|
|
interpret(q{
|
|
macro test(x) {
|
|
return x.interpolate(function(str) {
|
|
return str ~ "test";
|
|
});
|
|
}
|
|
|
|
var a = "cool";
|
|
assert(test("hey #{a}") == "hey cooltest");
|
|
}, globals);
|
|
}
|
|
|
|
/++
|
|
$(H3 Classes demo)
|
|
|
|
See also: [arsd.jsvar.subclassable] for more interop with D classes.
|
|
+/
|
|
unittest {
|
|
var globals = var.emptyObject;
|
|
interpret(q{
|
|
class Base {
|
|
function foo() { return "Base"; }
|
|
function set() { this.a = 10; }
|
|
function get() { return this.a; } // this MUST be used for instance variables though as they do not exist in static lookup
|
|
function test() { return foo(); } // I did NOT use `this` here which means it does STATIC lookup!
|
|
// kinda like mixin templates in D lol.
|
|
var a = 5;
|
|
static var b = 10; // static vars are attached to the class specifically
|
|
}
|
|
class Child : Base {
|
|
function foo() {
|
|
assert(super.foo() == "Base");
|
|
return "Child";
|
|
};
|
|
function set() { this.a = 7; }
|
|
function get2() { return this.a; }
|
|
var a = 9;
|
|
}
|
|
|
|
var c = new Child();
|
|
assert(c.foo() == "Child");
|
|
|
|
assert(c.test() == "Base"); // static lookup of methods if you don't use `this`
|
|
|
|
/*
|
|
// these would pass in D, but do NOT pass here because of dynamic variable lookup in script.
|
|
assert(c.get() == 5);
|
|
assert(c.get2() == 9);
|
|
c.set();
|
|
assert(c.get() == 5); // parent instance is separate
|
|
assert(c.get2() == 7);
|
|
*/
|
|
|
|
// showing the shared vars now.... I personally prefer the D way but meh, this lang
|
|
// is an unholy cross of D and Javascript so that means it sucks sometimes.
|
|
assert(c.get() == c.get2());
|
|
c.set();
|
|
assert(c.get2() == 7);
|
|
assert(c.get() == c.get2());
|
|
|
|
// super, on the other hand, must always be looked up statically, or else this
|
|
// next example with infinite recurse and smash the stack.
|
|
class Third : Child { }
|
|
var t = new Third();
|
|
assert(t.foo() == "Child");
|
|
}, globals);
|
|
}
|
|
|
|
/++
|
|
$(H3 Properties from D)
|
|
|
|
Note that it is not possible yet to define a property function from the script language.
|
|
+/
|
|
unittest {
|
|
static class Test {
|
|
// the @scriptable is required to make it accessible
|
|
@scriptable int a;
|
|
|
|
@scriptable @property int ro() { return 30; }
|
|
|
|
int _b = 20;
|
|
@scriptable @property int b() { return _b; }
|
|
@scriptable @property int b(int val) { return _b = val; }
|
|
}
|
|
|
|
Test test = new Test;
|
|
|
|
test.a = 15;
|
|
|
|
var globals = var.emptyObject;
|
|
globals.test = test;
|
|
// but once it is @scriptable, both read and write works from here:
|
|
interpret(q{
|
|
assert(test.a == 15);
|
|
test.a = 10;
|
|
assert(test.a == 10);
|
|
|
|
assert(test.ro == 30); // @property functions from D wrapped too
|
|
test.ro = 40;
|
|
assert(test.ro == 30); // setting it does nothing though
|
|
|
|
assert(test.b == 20); // reader still works if read/write available too
|
|
test.b = 25;
|
|
assert(test.b == 25); // writer action reflected
|
|
|
|
// however other opAssign operators are not implemented correctly on properties at this time so this fails!
|
|
//test.b *= 2;
|
|
//assert(test.b == 50);
|
|
}, globals);
|
|
|
|
// and update seen back in D
|
|
assert(test.a == 10); // on the original native object
|
|
assert(test.b == 25);
|
|
|
|
assert(globals.test.a == 10); // and via the var accessor for member var
|
|
assert(globals.test.b == 25); // as well as @property func
|
|
}
|
|
|
|
|
|
public import arsd.jsvar;
|
|
|
|
import std.stdio;
|
|
import std.traits;
|
|
import std.conv;
|
|
import std.json;
|
|
|
|
import std.array;
|
|
import std.range;
|
|
|
|
/* **************************************
|
|
script to follow
|
|
****************************************/
|
|
|
|
/++
|
|
A base class for exceptions that can never be caught by scripts;
|
|
throwing it from a function called from a script is guaranteed to
|
|
bubble all the way up to your [interpret] call..
|
|
(scripts can also never catch Error btw)
|
|
|
|
History:
|
|
Added on April 24, 2020 (v7.3.0)
|
|
+/
|
|
class NonScriptCatchableException : Exception {
|
|
import std.exception;
|
|
///
|
|
mixin basicExceptionCtors;
|
|
}
|
|
|
|
//class TEST : Throwable {this() { super("lol"); }}
|
|
|
|
/// Thrown on script syntax errors and the sort.
|
|
class ScriptCompileException : Exception {
|
|
string s;
|
|
int lineNumber;
|
|
this(string msg, string s, int lineNumber, string file = __FILE__, size_t line = __LINE__) {
|
|
this.s = s;
|
|
this.lineNumber = lineNumber;
|
|
super(to!string(lineNumber) ~ ": " ~ msg, file, line);
|
|
}
|
|
}
|
|
|
|
/// Thrown on things like interpretation failures.
|
|
class ScriptRuntimeException : Exception {
|
|
string s;
|
|
int lineNumber;
|
|
this(string msg, string s, int lineNumber, string file = __FILE__, size_t line = __LINE__) {
|
|
this.s = s;
|
|
this.lineNumber = lineNumber;
|
|
super(to!string(lineNumber) ~ ": " ~ msg, file, line);
|
|
}
|
|
}
|
|
|
|
/// This represents an exception thrown by `throw x;` inside the script as it is interpreted.
|
|
class ScriptException : Exception {
|
|
///
|
|
var payload;
|
|
///
|
|
ScriptLocation loc;
|
|
///
|
|
ScriptLocation[] callStack;
|
|
this(var payload, ScriptLocation loc, string file = __FILE__, size_t line = __LINE__) {
|
|
this.payload = payload;
|
|
if(loc.scriptFilename.length == 0)
|
|
loc.scriptFilename = "user_script";
|
|
this.loc = loc;
|
|
super(loc.scriptFilename ~ "@" ~ to!string(loc.lineNumber) ~ ": " ~ to!string(payload), file, line);
|
|
}
|
|
|
|
/*
|
|
override string toString() {
|
|
return loc.scriptFilename ~ "@" ~ to!string(loc.lineNumber) ~ ": " ~ payload.get!string ~ to!string(callStack);
|
|
}
|
|
*/
|
|
|
|
// might be nice to take a D exception and put a script stack trace in there too......
|
|
// also need toString to show the callStack
|
|
}
|
|
|
|
struct ScriptToken {
|
|
enum Type { identifier, keyword, symbol, string, int_number, hex_number, binary_number, oct_number, float_number }
|
|
Type type;
|
|
string str;
|
|
string scriptFilename;
|
|
int lineNumber;
|
|
|
|
string wasSpecial;
|
|
}
|
|
|
|
// these need to be ordered from longest to shortest
|
|
// some of these aren't actually used, like struct and goto right now, but I want them reserved for later
|
|
private enum string[] keywords = [
|
|
"function", "continue",
|
|
"__FILE__", "__LINE__", // these two are special to the lexer
|
|
"foreach", "json!q{", "default", "finally",
|
|
"return", "static", "struct", "import", "module", "assert", "switch",
|
|
"while", "catch", "throw", "scope", "break", "class", "false", "mixin", "macro", "super",
|
|
// "this" is just treated as just a magic identifier.....
|
|
"auto", // provided as an alias for var right now, may change later
|
|
"null", "else", "true", "eval", "goto", "enum", "case", "cast",
|
|
"var", "for", "try", "new",
|
|
"if", "do",
|
|
];
|
|
private enum string[] symbols = [
|
|
">>>", // FIXME
|
|
"//", "/*", "/+",
|
|
"&&", "||",
|
|
"+=", "-=", "*=", "/=", "~=", "==", "<=", ">=","!=", "%=",
|
|
"&=", "|=", "^=",
|
|
"#{",
|
|
"..",
|
|
"<<", ">>", // FIXME
|
|
"|>",
|
|
"=>", // FIXME
|
|
"?", ".",",",";",":",
|
|
"[", "]", "{", "}", "(", ")",
|
|
"&", "|", "^",
|
|
"+", "-", "*", "/", "=", "<", ">","~","!","%"
|
|
];
|
|
|
|
// we need reference semantics on this all the time
|
|
class TokenStream(TextStream) {
|
|
TextStream textStream;
|
|
string text;
|
|
int lineNumber = 1;
|
|
string scriptFilename;
|
|
|
|
void advance(ptrdiff_t size) {
|
|
foreach(i; 0 .. size) {
|
|
if(text.empty)
|
|
break;
|
|
if(text[0] == '\n')
|
|
lineNumber ++;
|
|
text = text[1 .. $];
|
|
// text.popFront(); // don't want this because it pops too much trying to do its own UTF-8, which we already handled!
|
|
}
|
|
}
|
|
|
|
this(TextStream ts, string fn) {
|
|
textStream = ts;
|
|
scriptFilename = fn;
|
|
text = textStream.front;
|
|
popFront;
|
|
}
|
|
|
|
ScriptToken next;
|
|
|
|
// FIXME: might be worth changing this so i can peek far enough ahead to do () => expr lambdas.
|
|
ScriptToken peek;
|
|
bool peeked;
|
|
void pushFront(ScriptToken f) {
|
|
peek = f;
|
|
peeked = true;
|
|
}
|
|
|
|
ScriptToken front() {
|
|
if(peeked)
|
|
return peek;
|
|
else
|
|
return next;
|
|
}
|
|
|
|
bool empty() {
|
|
advanceSkips();
|
|
return text.length == 0 && textStream.empty && !peeked;
|
|
}
|
|
|
|
int skipNext;
|
|
void advanceSkips() {
|
|
if(skipNext) {
|
|
skipNext--;
|
|
popFront();
|
|
}
|
|
}
|
|
|
|
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' || text[0] == '\r') {
|
|
advance(1);
|
|
continue;
|
|
} else if(text[0] >= '0' && text[0] <= '9') {
|
|
int radix = 10;
|
|
if(text.length > 2 && text[0] == '0') {
|
|
if(text[1] == 'x' || text[1] == 'X')
|
|
radix = 16;
|
|
if(text[1] == 'b')
|
|
radix = 2;
|
|
if(text[1] == 'o')
|
|
radix = 8;
|
|
|
|
if(radix != 10)
|
|
text = text[2 .. $];
|
|
}
|
|
|
|
int pos;
|
|
bool sawDot;
|
|
while(pos < text.length && (
|
|
(text[pos] >= '0' && text[pos] <= '9')
|
|
|| (text[pos] >= 'A' && text[pos] <= 'F')
|
|
|| (text[pos] >= 'a' && text[pos] <= 'f')
|
|
|| text[pos] == '_'
|
|
|| text[pos] == '.'
|
|
)) {
|
|
if(text[pos] == '.') {
|
|
if(sawDot)
|
|
break;
|
|
else
|
|
sawDot = true;
|
|
}
|
|
pos++;
|
|
}
|
|
|
|
if(text[pos - 1] == '.') {
|
|
// This is something like "1.x", which is *not* a floating literal; it is UFCS on an int
|
|
sawDot = false;
|
|
pos --;
|
|
}
|
|
|
|
token.type = sawDot ? ScriptToken.Type.float_number : ScriptToken.Type.int_number;
|
|
if(radix == 2) token.type = ScriptToken.Type.binary_number;
|
|
if(radix == 8) token.type = ScriptToken.Type.oct_number;
|
|
if(radix == 16) token.type = ScriptToken.Type.hex_number;
|
|
token.str = text[0 .. pos];
|
|
advance(pos);
|
|
} else if((text[0] >= 'a' && text[0] <= 'z') || (text[0] == '_') || (text[0] >= 'A' && text[0] <= 'Z') || text[0] == '$') {
|
|
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;
|
|
if(text[0] == '$')
|
|
pos++;
|
|
|
|
while(pos < text.length
|
|
&& ((text[pos] >= 'a' && text[pos] <= 'z') ||
|
|
(text[pos] == '_') ||
|
|
//(pos != 0 && text[pos] == '-') || // allow mid-identifier dashes for this-kind-of-name. For subtraction, add a space.
|
|
(text[pos] >= 'A' && text[pos] <= 'Z') ||
|
|
(text[pos] >= '0' && text[pos] <= '9')))
|
|
{
|
|
pos++;
|
|
}
|
|
|
|
token.str = text[0 .. pos];
|
|
advance(pos);
|
|
}
|
|
} else if(text[0] == '"' || text[0] == '\'' || text[0] == '`' ||
|
|
// Also supporting double curly quoted strings: “foo” which nest. This is the utf 8 coding:
|
|
(text.length >= 3 && text[0] == 0xe2 && text[1] == 0x80 && text[2] == 0x9c))
|
|
{
|
|
char end = text[0]; // support single quote and double quote strings the same
|
|
int openCurlyQuoteCount = (end == 0xe2) ? 1 : 0;
|
|
bool escapingAllowed = end != '`'; // `` strings are raw, they don't support escapes. the others do.
|
|
token.type = ScriptToken.Type.string;
|
|
int pos = openCurlyQuoteCount ? 3 : 1; // skip the opening dchar
|
|
int started = pos;
|
|
bool escaped = false;
|
|
bool mustCopy = false;
|
|
|
|
bool allowInterpolation = text[0] == '"';
|
|
|
|
bool atEnd() {
|
|
if(pos == text.length)
|
|
return false;
|
|
if(openCurlyQuoteCount) {
|
|
if(openCurlyQuoteCount == 1)
|
|
return (pos + 3 <= text.length && text[pos] == 0xe2 && text[pos+1] == 0x80 && text[pos+2] == 0x9d); // ”
|
|
else // greater than one means we nest
|
|
return false;
|
|
} else
|
|
return text[pos] == end;
|
|
}
|
|
|
|
bool interpolationDetected = false;
|
|
bool inInterpolate = false;
|
|
int interpolateCount = 0;
|
|
|
|
while(pos < text.length && (escaped || inInterpolate || !atEnd())) {
|
|
if(inInterpolate) {
|
|
if(text[pos] == '{')
|
|
interpolateCount++;
|
|
else if(text[pos] == '}') {
|
|
interpolateCount--;
|
|
if(interpolateCount == 0)
|
|
inInterpolate = false;
|
|
}
|
|
pos++;
|
|
continue;
|
|
}
|
|
|
|
if(escaped) {
|
|
mustCopy = true;
|
|
escaped = false;
|
|
} else {
|
|
if(text[pos] == '\\' && escapingAllowed)
|
|
escaped = true;
|
|
if(allowInterpolation && text[pos] == '#' && pos + 1 < text.length && text[pos + 1] == '{') {
|
|
interpolationDetected = true;
|
|
inInterpolate = true;
|
|
}
|
|
if(openCurlyQuoteCount) {
|
|
// also need to count curly quotes to support nesting
|
|
if(pos + 3 <= text.length && text[pos+0] == 0xe2 && text[pos+1] == 0x80 && text[pos+2] == 0x9c) // “
|
|
openCurlyQuoteCount++;
|
|
if(pos + 3 <= text.length && text[pos+0] == 0xe2 && text[pos+1] == 0x80 && text[pos+2] == 0x9d) // ”
|
|
openCurlyQuoteCount--;
|
|
}
|
|
}
|
|
pos++;
|
|
}
|
|
|
|
if(pos == text.length && (escaped || inInterpolate || !atEnd()))
|
|
throw new ScriptCompileException("Unclosed string literal", token.scriptFilename, token.lineNumber);
|
|
|
|
if(mustCopy) {
|
|
// there must be something escaped in there, so we need
|
|
// to copy it and properly handle those cases
|
|
string copy;
|
|
copy.reserve(pos + 4);
|
|
|
|
escaped = false;
|
|
int readingUnicode;
|
|
dchar uniChar = 0;
|
|
|
|
int hexCharToInt(dchar ch) {
|
|
if(ch >= '0' && ch <= '9')
|
|
return ch - '0';
|
|
if(ch >= 'a' && ch <= 'f')
|
|
return ch - 'a' + 10;
|
|
if(ch >= 'A' && ch <= 'F')
|
|
return ch - 'A' + 10;
|
|
throw new ScriptCompileException("Invalid hex char in \\u unicode section: " ~ cast(char) ch, token.scriptFilename, token.lineNumber);
|
|
}
|
|
|
|
foreach(idx, dchar ch; text[started .. pos]) {
|
|
if(readingUnicode) {
|
|
if(readingUnicode == 4 && ch == '{') {
|
|
readingUnicode = 5;
|
|
continue;
|
|
}
|
|
if(readingUnicode == 5 && ch == '}') {
|
|
readingUnicode = 1;
|
|
} else {
|
|
uniChar <<= 4;
|
|
uniChar |= hexCharToInt(ch);
|
|
}
|
|
if(readingUnicode != 5)
|
|
readingUnicode--;
|
|
if(readingUnicode == 0)
|
|
copy ~= uniChar;
|
|
continue;
|
|
}
|
|
if(escaped) {
|
|
escaped = false;
|
|
switch(ch) {
|
|
case '\\': copy ~= "\\"; break;
|
|
case 'n': copy ~= "\n"; break;
|
|
case 'r': copy ~= "\r"; break;
|
|
case 'a': copy ~= "\a"; break;
|
|
case 't': copy ~= "\t"; break;
|
|
case '#': copy ~= "#"; break;
|
|
case '"': copy ~= "\""; break;
|
|
case 'u': readingUnicode = 4; uniChar = 0; break;
|
|
case '\'': copy ~= "'"; break;
|
|
default:
|
|
throw new ScriptCompileException("Unknown escape char " ~ cast(char) ch, token.scriptFilename, token.lineNumber);
|
|
}
|
|
continue;
|
|
} else if(ch == '\\') {
|
|
escaped = true;
|
|
continue;
|
|
}
|
|
copy ~= ch;
|
|
}
|
|
|
|
token.str = copy;
|
|
} else {
|
|
token.str = text[started .. pos];
|
|
}
|
|
if(interpolationDetected)
|
|
token.wasSpecial = "\"";
|
|
advance(pos + ((end == 0xe2) ? 3 : 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' && text[0] != '\r')
|
|
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", token.scriptFilename, lineNumber);
|
|
|
|
advance(pos + 2);
|
|
continue mainLoop;
|
|
|
|
} else if(symbol == "/+") {
|
|
int open = 0;
|
|
int pos = 0;
|
|
while(pos + 1 < text.length) {
|
|
if(text[pos..pos+2] == "/+") {
|
|
open++;
|
|
pos++;
|
|
} else if(text[pos..pos+2] == "+/") {
|
|
open--;
|
|
pos++;
|
|
if(open == 0)
|
|
break;
|
|
}
|
|
pos++;
|
|
}
|
|
|
|
if(pos + 1 == text.length)
|
|
throw new ScriptCompileException("unclosed /+ +/ comment", token.scriptFilename, lineNumber);
|
|
|
|
advance(pos + 1);
|
|
continue mainLoop;
|
|
}
|
|
// FIXME: documentation comments
|
|
|
|
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], token.scriptFilename, 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);
|
|
}
|
|
|
|
class MacroPrototype : PrototypeObject {
|
|
var func;
|
|
|
|
// macros are basically functions that get special treatment for their arguments
|
|
// they are passed as AST objects instead of interpreted
|
|
// calling an AST object will interpret it in the script
|
|
this(var func) {
|
|
this.func = func;
|
|
this._properties["opCall"] = (var _this, var[] args) {
|
|
return func.apply(_this, args);
|
|
};
|
|
}
|
|
}
|
|
|
|
alias helper(alias T) = T;
|
|
// alternative to virtual function for converting the expression objects to script objects
|
|
void addChildElementsOfExpressionToScriptExpressionObject(ClassInfo c, Expression _thisin, PrototypeObject sc, ref var obj) {
|
|
foreach(itemName; __traits(allMembers, mixin(__MODULE__)))
|
|
static if(__traits(compiles, __traits(getMember, mixin(__MODULE__), itemName))) {
|
|
alias Class = helper!(__traits(getMember, mixin(__MODULE__), itemName));
|
|
static if(is(Class : Expression)) if(c == typeid(Class)) {
|
|
auto _this = cast(Class) _thisin;
|
|
foreach(memberName; __traits(allMembers, Class)) {
|
|
alias member = helper!(__traits(getMember, Class, memberName));
|
|
|
|
static if(is(typeof(member) : Expression)) {
|
|
auto lol = __traits(getMember, _this, memberName);
|
|
if(lol is null)
|
|
obj[memberName] = null;
|
|
else
|
|
obj[memberName] = lol.toScriptExpressionObject(sc);
|
|
}
|
|
static if(is(typeof(member) : Expression[])) {
|
|
obj[memberName] = var.emptyArray;
|
|
foreach(m; __traits(getMember, _this, memberName))
|
|
if(m !is null)
|
|
obj[memberName] ~= m.toScriptExpressionObject(sc);
|
|
else
|
|
obj[memberName] ~= null;
|
|
}
|
|
static if(is(typeof(member) : string) || is(typeof(member) : long) || is(typeof(member) : real) || is(typeof(member) : bool)) {
|
|
obj[memberName] = __traits(getMember, _this, memberName);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct InterpretResult {
|
|
var value;
|
|
PrototypeObject sc;
|
|
enum FlowControl { Normal, Return, Continue, Break, Goto }
|
|
FlowControl flowControl;
|
|
string flowControlDetails; // which label
|
|
}
|
|
|
|
class Expression {
|
|
abstract InterpretResult interpret(PrototypeObject sc);
|
|
|
|
// this returns an AST object that can be inspected and possibly altered
|
|
// by the script. Calling the returned object will interpret the object in
|
|
// the original scope passed
|
|
var toScriptExpressionObject(PrototypeObject sc) {
|
|
var obj = var.emptyObject;
|
|
|
|
obj["type"] = typeid(this).name;
|
|
obj["toSourceCode"] = (var _this, var[] args) {
|
|
Expression e = this;
|
|
return var(e.toString());
|
|
};
|
|
obj["opCall"] = (var _this, var[] args) {
|
|
Expression e = this;
|
|
// FIXME: if they changed the properties in the
|
|
// script, we should update them here too.
|
|
return e.interpret(sc).value;
|
|
};
|
|
obj["interpolate"] = (var _this, var[] args) {
|
|
StringLiteralExpression e = cast(StringLiteralExpression) this;
|
|
if(!e)
|
|
return var(null);
|
|
return e.interpolate(args.length ? args[0] : var(null), sc);
|
|
};
|
|
|
|
|
|
// adding structure is going to be a little bit magical
|
|
// I could have done this with a virtual function, but I'm lazy.
|
|
addChildElementsOfExpressionToScriptExpressionObject(typeid(this), this, sc, obj);
|
|
|
|
return obj;
|
|
}
|
|
|
|
string toInterpretedString(PrototypeObject sc) {
|
|
return toString();
|
|
}
|
|
}
|
|
|
|
class MixinExpression : Expression {
|
|
Expression e1;
|
|
this(Expression e1) {
|
|
this.e1 = e1;
|
|
}
|
|
|
|
override string toString() { return "mixin(" ~ e1.toString() ~ ")"; }
|
|
|
|
override InterpretResult interpret(PrototypeObject sc) {
|
|
return InterpretResult(.interpret(e1.interpret(sc).value.get!string ~ ";", sc), sc);
|
|
}
|
|
}
|
|
|
|
class StringLiteralExpression : Expression {
|
|
string content;
|
|
bool allowInterpolation;
|
|
|
|
ScriptToken token;
|
|
|
|
override string toString() {
|
|
import std.string : replace;
|
|
return "\"" ~ content.replace(`\`, `\\`).replace("\"", "\\\"") ~ "\"";
|
|
}
|
|
|
|
this(ScriptToken token) {
|
|
this.token = token;
|
|
this(token.str);
|
|
if(token.wasSpecial == "\"")
|
|
allowInterpolation = true;
|
|
|
|
}
|
|
|
|
this(string s) {
|
|
content = s;
|
|
}
|
|
|
|
var interpolate(var funcObj, PrototypeObject sc) {
|
|
import std.string : indexOf;
|
|
if(allowInterpolation) {
|
|
string r;
|
|
|
|
auto c = content;
|
|
auto idx = c.indexOf("#{");
|
|
while(idx != -1) {
|
|
r ~= c[0 .. idx];
|
|
c = c[idx + 2 .. $];
|
|
idx = 0;
|
|
int open = 1;
|
|
while(idx < c.length) {
|
|
if(c[idx] == '}')
|
|
open--;
|
|
else if(c[idx] == '{')
|
|
open++;
|
|
if(open == 0)
|
|
break;
|
|
idx++;
|
|
}
|
|
if(open != 0)
|
|
throw new ScriptRuntimeException("Unclosed interpolation thing", token.scriptFilename, token.lineNumber);
|
|
auto code = c[0 .. idx];
|
|
|
|
var result = .interpret(code, sc);
|
|
|
|
if(funcObj == var(null))
|
|
r ~= result.get!string;
|
|
else
|
|
r ~= funcObj(result).get!string;
|
|
|
|
c = c[idx + 1 .. $];
|
|
idx = c.indexOf("#{");
|
|
}
|
|
|
|
r ~= c;
|
|
return var(r);
|
|
} else {
|
|
return var(content);
|
|
}
|
|
}
|
|
|
|
override InterpretResult interpret(PrototypeObject sc) {
|
|
return InterpretResult(interpolate(var(null), sc), sc);
|
|
}
|
|
}
|
|
|
|
class BoolLiteralExpression : Expression {
|
|
bool literal;
|
|
this(string l) {
|
|
literal = to!bool(l);
|
|
}
|
|
|
|
override string toString() { return to!string(literal); }
|
|
|
|
override InterpretResult interpret(PrototypeObject sc) {
|
|
return InterpretResult(var(literal), sc);
|
|
}
|
|
}
|
|
|
|
class IntLiteralExpression : Expression {
|
|
long literal;
|
|
|
|
this(string s, int radix) {
|
|
literal = to!long(s.replace("_", ""), radix);
|
|
}
|
|
|
|
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.replace("_", ""));
|
|
}
|
|
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 NegationExpression : Expression {
|
|
Expression e;
|
|
this(Expression e) { this.e = e;}
|
|
override string toString() { return "-" ~ e.toString(); }
|
|
|
|
override InterpretResult interpret(PrototypeObject sc) {
|
|
var n = e.interpret(sc).value;
|
|
return InterpretResult(-n, sc);
|
|
}
|
|
}
|
|
class NotExpression : Expression {
|
|
Expression e;
|
|
this(Expression e) { this.e = e;}
|
|
override string toString() { return "!" ~ e.toString(); }
|
|
|
|
override InterpretResult interpret(PrototypeObject sc) {
|
|
var n = e.interpret(sc).value;
|
|
return InterpretResult(var(!n), sc);
|
|
}
|
|
}
|
|
class BitFlipExpression : Expression {
|
|
Expression e;
|
|
this(Expression e) { this.e = e;}
|
|
override string toString() { return "~" ~ e.toString(); }
|
|
|
|
override InterpretResult interpret(PrototypeObject sc) {
|
|
var n = e.interpret(sc).value;
|
|
// possible FIXME given the size. but it is fuzzy when dynamic..
|
|
return InterpretResult(var(~(n.get!long)), sc);
|
|
}
|
|
}
|
|
|
|
class ArrayLiteralExpression : Expression {
|
|
this() {}
|
|
|
|
override string toString() {
|
|
string s = "[";
|
|
foreach(i, ele; elements) {
|
|
if(i) s ~= ", ";
|
|
s ~= ele.toString();
|
|
}
|
|
s ~= "]";
|
|
return s;
|
|
}
|
|
|
|
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 string toString() {
|
|
string s = "#{";
|
|
bool first = true;
|
|
foreach(k, e; elements) {
|
|
if(first)
|
|
first = false;
|
|
else
|
|
s ~= ", ";
|
|
|
|
s ~= "\"" ~ k ~ "\":"; // FIXME: escape if needed
|
|
s ~= e.toString();
|
|
}
|
|
|
|
s ~= "}";
|
|
return s;
|
|
}
|
|
|
|
PrototypeObject backing;
|
|
this(PrototypeObject backing = null) {
|
|
this.backing = backing;
|
|
}
|
|
|
|
override InterpretResult interpret(PrototypeObject sc) {
|
|
var n;
|
|
if(backing is null)
|
|
n = var.emptyObject;
|
|
else
|
|
n._object = backing;
|
|
|
|
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();
|
|
}
|
|
|
|
this(VariableDeclaration args, Expression bod, PrototypeObject lexicalScope = null) {
|
|
this();
|
|
this.arguments = args;
|
|
this.functionBody = bod;
|
|
this.lexicalScope = lexicalScope;
|
|
}
|
|
|
|
override string toString() {
|
|
string s = (isMacro ? "macro" : "function") ~ " (";
|
|
if(arguments !is null)
|
|
s ~= arguments.toString();
|
|
|
|
s ~= ") ";
|
|
s ~= functionBody.toString();
|
|
return s;
|
|
}
|
|
|
|
/*
|
|
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
|
|
*/
|
|
VariableDeclaration arguments;
|
|
Expression functionBody; // can be a ScopeExpression btw
|
|
|
|
PrototypeObject lexicalScope;
|
|
|
|
bool isMacro;
|
|
|
|
override InterpretResult interpret(PrototypeObject sc) {
|
|
assert(DefaultArgumentDummyObject !is null);
|
|
var v;
|
|
v._metadata = new ScriptFunctionMetadata(this);
|
|
v._function = (var _this, var[] args) {
|
|
auto argumentsScope = new PrototypeObject();
|
|
PrototypeObject scToUse;
|
|
if(lexicalScope is null)
|
|
scToUse = sc;
|
|
else {
|
|
scToUse = lexicalScope;
|
|
scToUse._secondary = sc;
|
|
}
|
|
|
|
argumentsScope.prototype = scToUse;
|
|
|
|
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);
|
|
}
|
|
};
|
|
if(isMacro) {
|
|
var n = var.emptyObject;
|
|
n._object = new MacroPrototype(v);
|
|
v = n;
|
|
}
|
|
return InterpretResult(v, sc);
|
|
}
|
|
}
|
|
|
|
class CastExpression : Expression {
|
|
string type;
|
|
Expression e1;
|
|
|
|
override string toString() {
|
|
return "cast(" ~ type ~ ") " ~ e1.toString();
|
|
}
|
|
|
|
override InterpretResult interpret(PrototypeObject sc) {
|
|
var n = e1.interpret(sc).value;
|
|
switch(type) {
|
|
foreach(possibleType; CtList!("int", "long", "float", "double", "real", "char", "dchar", "string", "int[]", "string[]", "float[]")) {
|
|
case possibleType:
|
|
n = mixin("cast(" ~ possibleType ~ ") n");
|
|
break;
|
|
}
|
|
default:
|
|
// FIXME, we can probably cast other types like classes here.
|
|
}
|
|
|
|
return InterpretResult(n, sc);
|
|
}
|
|
}
|
|
|
|
class VariableDeclaration : Expression {
|
|
string[] identifiers;
|
|
Expression[] initializers;
|
|
string[] typeSpecifiers;
|
|
|
|
this() {}
|
|
|
|
override string toString() {
|
|
string s = "";
|
|
foreach(i, ident; identifiers) {
|
|
if(i)
|
|
s ~= ", ";
|
|
s ~= "var ";
|
|
if(typeSpecifiers[i].length) {
|
|
s ~= typeSpecifiers[i];
|
|
s ~= " ";
|
|
}
|
|
s ~= ident;
|
|
if(initializers[i] !is null)
|
|
s ~= " = " ~ initializers[i].toString();
|
|
}
|
|
return s;
|
|
}
|
|
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
class FunctionDeclaration : Expression {
|
|
DotVarExpression where;
|
|
string ident;
|
|
FunctionLiteralExpression expr;
|
|
|
|
this(DotVarExpression where, string ident, FunctionLiteralExpression expr) {
|
|
this.where = where;
|
|
this.ident = ident;
|
|
this.expr = expr;
|
|
}
|
|
|
|
override InterpretResult interpret(PrototypeObject sc) {
|
|
var n = expr.interpret(sc).value;
|
|
|
|
var replacement;
|
|
|
|
if(expr.isMacro) {
|
|
// can't overload macros
|
|
replacement = n;
|
|
} else {
|
|
var got;
|
|
|
|
if(where is null) {
|
|
got = sc._getMember(ident, false, false);
|
|
} else {
|
|
got = where.interpret(sc).value;
|
|
}
|
|
|
|
OverloadSet os = got.get!OverloadSet;
|
|
if(os is null) {
|
|
os = new OverloadSet;
|
|
}
|
|
|
|
os.addOverload(OverloadSet.Overload(expr.arguments ? toTypes(expr.arguments.typeSpecifiers, sc) : null, n));
|
|
|
|
replacement = var(os);
|
|
}
|
|
|
|
if(where is null) {
|
|
sc._getMember(ident, false, false) = replacement;
|
|
} else {
|
|
where.setVar(sc, replacement, false, true);
|
|
}
|
|
|
|
return InterpretResult(n, sc);
|
|
}
|
|
|
|
override string toString() {
|
|
string s = (expr.isMacro ? "macro" : "function") ~ " ";
|
|
s ~= ident;
|
|
s ~= "(";
|
|
if(expr.arguments !is null)
|
|
s ~= expr.arguments.toString();
|
|
|
|
s ~= ") ";
|
|
s ~= expr.functionBody.toString();
|
|
|
|
return s;
|
|
}
|
|
}
|
|
|
|
template CtList(T...) { alias CtList = T; }
|
|
|
|
class BinaryExpression : Expression {
|
|
string op;
|
|
Expression e1;
|
|
Expression e2;
|
|
|
|
override string toString() {
|
|
return e1.toString() ~ " " ~ op ~ " " ~ e2.toString();
|
|
}
|
|
|
|
override string toInterpretedString(PrototypeObject sc) {
|
|
return e1.toInterpretedString(sc) ~ " " ~ op ~ " " ~ e2.toInterpretedString(sc);
|
|
}
|
|
|
|
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;
|
|
sw: switch(op) {
|
|
// I would actually kinda prefer this to be static foreach, but normal
|
|
// tuple foreach here has broaded compiler compatibility.
|
|
foreach(ctOp; CtList!("+", "-", "*", "/", "==", "!=", "<=", ">=", ">", "<", "~", "&&", "||", "&", "|", "^", "%", ">>", "<<", ">>>")) // FIXME
|
|
case ctOp: {
|
|
n = mixin("left "~ctOp~" right");
|
|
break sw;
|
|
}
|
|
default:
|
|
assert(0, op);
|
|
}
|
|
|
|
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", null, 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, true, true) "~ctOp~" right");
|
|
|
|
// FIXME: ensure the variable is updated in scope too
|
|
|
|
return InterpretResult(n, sc);
|
|
|
|
}
|
|
}
|
|
|
|
class PipelineExpression : Expression {
|
|
Expression e1;
|
|
Expression e2;
|
|
CallExpression ce;
|
|
ScriptLocation loc;
|
|
|
|
this(ScriptLocation loc, Expression e1, Expression e2) {
|
|
this.loc = loc;
|
|
this.e1 = e1;
|
|
this.e2 = e2;
|
|
|
|
if(auto ce = cast(CallExpression) e2) {
|
|
this.ce = new CallExpression(loc, ce.func);
|
|
this.ce.arguments = [e1] ~ ce.arguments;
|
|
} else {
|
|
this.ce = new CallExpression(loc, e2);
|
|
this.ce.arguments ~= e1;
|
|
}
|
|
}
|
|
|
|
override string toString() { return e1.toString() ~ " |> " ~ e2.toString(); }
|
|
|
|
override InterpretResult interpret(PrototypeObject sc) {
|
|
return ce.interpret(sc);
|
|
}
|
|
}
|
|
|
|
class AssignExpression : Expression {
|
|
Expression e1;
|
|
Expression e2;
|
|
bool suppressOverloading;
|
|
|
|
this(Expression e1, Expression e2, bool suppressOverloading = false) {
|
|
this.e1 = e1;
|
|
this.e2 = e2;
|
|
this.suppressOverloading = suppressOverloading;
|
|
}
|
|
|
|
override string toString() { return e1.toString() ~ " = " ~ e2.toString(); }
|
|
|
|
override InterpretResult interpret(PrototypeObject sc) {
|
|
auto v = cast(VariableExpression) e1;
|
|
if(v is null)
|
|
throw new ScriptRuntimeException("not an lvalue", null, 0 /* FIXME */);
|
|
|
|
auto ret = v.setVar(sc, e2 is null ? var(null) : e2.interpret(sc).value, false, suppressOverloading);
|
|
|
|
return InterpretResult(ret, sc);
|
|
}
|
|
}
|
|
class VariableExpression : Expression {
|
|
string identifier;
|
|
ScriptLocation loc;
|
|
|
|
this(string identifier, ScriptLocation loc = ScriptLocation.init) {
|
|
this.identifier = identifier;
|
|
this.loc = loc;
|
|
}
|
|
|
|
override string toString() {
|
|
return identifier;
|
|
}
|
|
|
|
override string toInterpretedString(PrototypeObject sc) {
|
|
return getVar(sc).get!string;
|
|
}
|
|
|
|
ref var getVar(PrototypeObject sc, bool recurse = true, bool returnRawProperty = false) {
|
|
try {
|
|
return sc._getMember(identifier, true /* FIXME: recurse?? */, true, returnRawProperty);
|
|
} catch(DynamicTypeException dte) {
|
|
dte.callStack ~= loc;
|
|
throw dte;
|
|
}
|
|
}
|
|
|
|
ref var setVar(PrototypeObject sc, var t, bool recurse = true, bool suppressOverloading = false) {
|
|
return sc._setMember(identifier, t, true /* FIXME: recurse?? */, true, suppressOverloading);
|
|
}
|
|
|
|
ref var getVarFrom(PrototypeObject sc, ref var v, bool returnRawProperty) {
|
|
if(returnRawProperty) {
|
|
if(v.payloadType == var.Type.Object)
|
|
return v._payload._object._getMember(identifier, true, false, returnRawProperty);
|
|
}
|
|
|
|
return v[identifier];
|
|
}
|
|
|
|
override InterpretResult interpret(PrototypeObject sc) {
|
|
return InterpretResult(getVar(sc), sc);
|
|
}
|
|
}
|
|
|
|
class SuperExpression : Expression {
|
|
VariableExpression dot;
|
|
string origDot;
|
|
this(VariableExpression dot) {
|
|
if(dot !is null) {
|
|
origDot = dot.identifier;
|
|
//dot.identifier = "__super_" ~ dot.identifier; // omg this is so bad
|
|
}
|
|
this.dot = dot;
|
|
}
|
|
|
|
override string toString() {
|
|
if(dot is null)
|
|
return "super";
|
|
else
|
|
return "super." ~ origDot;
|
|
}
|
|
|
|
override InterpretResult interpret(PrototypeObject sc) {
|
|
var a = sc._getMember("super", true, true);
|
|
if(a._object is null)
|
|
throw new Exception("null proto for super");
|
|
PrototypeObject proto = a._object.prototype;
|
|
if(proto is null)
|
|
throw new Exception("no super");
|
|
//proto = proto.prototype;
|
|
|
|
if(dot !is null)
|
|
a = proto._getMember(dot.identifier, true, true);
|
|
else
|
|
a = proto._getMember("__ctor", true, true);
|
|
return InterpretResult(a, sc);
|
|
}
|
|
}
|
|
|
|
class DotVarExpression : VariableExpression {
|
|
Expression e1;
|
|
VariableExpression e2;
|
|
bool recurse = true;
|
|
|
|
this(Expression e1) {
|
|
this.e1 = e1;
|
|
super(null);
|
|
}
|
|
|
|
this(Expression e1, VariableExpression e2, bool recurse = true) {
|
|
this.e1 = e1;
|
|
this.e2 = e2;
|
|
this.recurse = recurse;
|
|
//assert(typeid(e2) == typeid(VariableExpression));
|
|
super("<do not use>");//e1.identifier ~ "." ~ e2.identifier);
|
|
}
|
|
|
|
override string toString() {
|
|
return e1.toString() ~ "." ~ e2.toString();
|
|
}
|
|
|
|
override ref var getVar(PrototypeObject sc, bool recurse = true, bool returnRawProperty = false) {
|
|
if(!this.recurse) {
|
|
// this is a special hack...
|
|
if(auto ve = cast(VariableExpression) e1) {
|
|
return ve.getVar(sc)._getOwnProperty(e2.identifier);
|
|
}
|
|
assert(0);
|
|
}
|
|
|
|
if(e2.identifier == "__source") {
|
|
auto val = e1.interpret(sc).value;
|
|
if(auto meta = cast(ScriptFunctionMetadata) val._metadata)
|
|
return *(new var(meta.convertToString()));
|
|
else
|
|
return *(new var(val.toJson()));
|
|
}
|
|
|
|
if(auto ve = cast(VariableExpression) e1) {
|
|
return this.getVarFrom(sc, ve.getVar(sc, recurse), returnRawProperty);
|
|
} else if(cast(StringLiteralExpression) e1 && e2.identifier == "interpolate") {
|
|
auto se = cast(StringLiteralExpression) e1;
|
|
var* functor = new var;
|
|
//if(!se.allowInterpolation)
|
|
//throw new ScriptRuntimeException("Cannot interpolate this string", se.token.lineNumber);
|
|
(*functor)._function = (var _this, var[] args) {
|
|
return se.interpolate(args.length ? args[0] : var(null), sc);
|
|
};
|
|
return *functor;
|
|
} else {
|
|
// make a temporary for the lhs
|
|
auto v = new var();
|
|
*v = e1.interpret(sc).value;
|
|
return this.getVarFrom(sc, *v, returnRawProperty);
|
|
}
|
|
}
|
|
|
|
override ref var setVar(PrototypeObject sc, var t, bool recurse = true, bool suppressOverloading = false) {
|
|
if(suppressOverloading)
|
|
return e1.interpret(sc).value.opIndexAssignNoOverload(t, e2.identifier);
|
|
else
|
|
return e1.interpret(sc).value.opIndexAssign(t, e2.identifier);
|
|
}
|
|
|
|
|
|
override ref var getVarFrom(PrototypeObject sc, ref var v, bool returnRawProperty) {
|
|
return e2.getVarFrom(sc, v, returnRawProperty);
|
|
}
|
|
|
|
override string toInterpretedString(PrototypeObject sc) {
|
|
return getVar(sc).get!string;
|
|
}
|
|
}
|
|
|
|
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, bool recurse = true, bool returnRawProperty = false) {
|
|
if(auto ve = cast(VariableExpression) e1)
|
|
return ve.getVar(sc, recurse, returnRawProperty)[e2.interpret(sc).value];
|
|
else {
|
|
auto v = new var();
|
|
*v = e1.interpret(sc).value;
|
|
return this.getVarFrom(sc, *v, returnRawProperty);
|
|
}
|
|
}
|
|
|
|
override ref var setVar(PrototypeObject sc, var t, bool recurse = true, bool suppressOverloading = false) {
|
|
return getVar(sc,recurse) = t;
|
|
}
|
|
}
|
|
|
|
class SliceExpression : Expression {
|
|
// e1[e2 .. e3]
|
|
Expression e1;
|
|
Expression e2;
|
|
Expression e3;
|
|
|
|
this(Expression e1, Expression e2, Expression e3) {
|
|
this.e1 = e1;
|
|
this.e2 = e2;
|
|
this.e3 = e3;
|
|
}
|
|
|
|
override string toString() {
|
|
return e1.toString() ~ "[" ~ e2.toString() ~ " .. " ~ e3.toString() ~ "]";
|
|
}
|
|
|
|
override InterpretResult interpret(PrototypeObject sc) {
|
|
var lhs = e1.interpret(sc).value;
|
|
|
|
auto specialScope = new PrototypeObject();
|
|
specialScope.prototype = sc;
|
|
specialScope._getMember("$", false, false) = lhs.length;
|
|
|
|
return InterpretResult(lhs[e2.interpret(specialScope).value .. e3.interpret(specialScope).value], sc);
|
|
}
|
|
}
|
|
|
|
|
|
class LoopControlExpression : Expression {
|
|
InterpretResult.FlowControl op;
|
|
this(string op) {
|
|
if(op == "continue")
|
|
this.op = InterpretResult.FlowControl.Continue;
|
|
else if(op == "break")
|
|
this.op = InterpretResult.FlowControl.Break;
|
|
else assert(0, op);
|
|
}
|
|
|
|
override string toString() {
|
|
import std.string;
|
|
return to!string(this.op).toLower();
|
|
}
|
|
|
|
override InterpretResult interpret(PrototypeObject sc) {
|
|
return InterpretResult(var(null), sc, op);
|
|
}
|
|
}
|
|
|
|
|
|
class ReturnExpression : Expression {
|
|
Expression value;
|
|
|
|
this(Expression v) {
|
|
value = v;
|
|
}
|
|
|
|
override string toString() { return "return " ~ value.toString(); }
|
|
|
|
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 string toString() {
|
|
string s;
|
|
s = "{\n";
|
|
foreach(expr; expressions) {
|
|
s ~= "\t";
|
|
s ~= expr.toString();
|
|
s ~= ";\n";
|
|
}
|
|
s ~= "}";
|
|
return s;
|
|
}
|
|
|
|
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) {
|
|
auto res = expression.interpret(innerScope);
|
|
ret = res.value;
|
|
if(res.flowControl != InterpretResult.FlowControl.Normal)
|
|
return InterpretResult(ret, sc, res.flowControl);
|
|
}
|
|
return InterpretResult(ret, sc);
|
|
}
|
|
}
|
|
|
|
class SwitchExpression : Expression {
|
|
Expression expr;
|
|
CaseExpression[] cases;
|
|
CaseExpression default_;
|
|
|
|
override InterpretResult interpret(PrototypeObject sc) {
|
|
auto e = expr.interpret(sc);
|
|
|
|
bool hitAny;
|
|
bool fallingThrough;
|
|
bool secondRun;
|
|
|
|
var last;
|
|
|
|
again:
|
|
foreach(c; cases) {
|
|
if(!secondRun && !fallingThrough && c is default_) continue;
|
|
if(fallingThrough || (secondRun && c is default_) || c.condition.interpret(sc) == e) {
|
|
fallingThrough = false;
|
|
if(!secondRun)
|
|
hitAny = true;
|
|
InterpretResult ret;
|
|
expr_loop: foreach(exp; c.expressions) {
|
|
ret = exp.interpret(sc);
|
|
with(InterpretResult.FlowControl)
|
|
final switch(ret.flowControl) {
|
|
case Normal:
|
|
last = ret.value;
|
|
break;
|
|
case Return:
|
|
case Goto:
|
|
return ret;
|
|
case Continue:
|
|
fallingThrough = true;
|
|
break expr_loop;
|
|
case Break:
|
|
return InterpretResult(last, sc);
|
|
}
|
|
}
|
|
|
|
if(!fallingThrough)
|
|
break;
|
|
}
|
|
}
|
|
|
|
if(!hitAny && !secondRun) {
|
|
secondRun = true;
|
|
goto again;
|
|
}
|
|
|
|
return InterpretResult(last, sc);
|
|
}
|
|
}
|
|
|
|
class CaseExpression : Expression {
|
|
this(Expression condition) {
|
|
this.condition = condition;
|
|
}
|
|
Expression condition;
|
|
Expression[] expressions;
|
|
|
|
override string toString() {
|
|
string code;
|
|
if(condition is null)
|
|
code = "default:";
|
|
else
|
|
code = "case " ~ condition.toString() ~ ":";
|
|
|
|
foreach(expr; expressions)
|
|
code ~= "\n" ~ expr.toString() ~ ";";
|
|
|
|
return code;
|
|
}
|
|
|
|
override InterpretResult interpret(PrototypeObject sc) {
|
|
// I did this inline up in the SwitchExpression above. maybe insane?!
|
|
assert(0);
|
|
}
|
|
}
|
|
|
|
unittest {
|
|
interpret(q{
|
|
var a = 10;
|
|
// case and break should work
|
|
var brk;
|
|
|
|
// var brk = switch doesn't parse, but this will.....
|
|
// (I kinda went everything is an expression but not all the way. this code SUX.)
|
|
brk = switch(a) {
|
|
case 10:
|
|
a = 30;
|
|
break;
|
|
case 30:
|
|
a = 40;
|
|
break;
|
|
default:
|
|
a = 0;
|
|
}
|
|
|
|
assert(a == 30);
|
|
assert(brk == 30); // value of switch set to last expression evaled inside
|
|
|
|
// so should default
|
|
switch(a) {
|
|
case 20:
|
|
a = 40;
|
|
break;
|
|
default:
|
|
a = 40;
|
|
}
|
|
|
|
assert(a == 40);
|
|
|
|
switch(a) {
|
|
case 40:
|
|
a = 50;
|
|
case 60: // no implicit fallthrough in this lang...
|
|
a = 60;
|
|
}
|
|
|
|
assert(a == 50);
|
|
|
|
var ret;
|
|
|
|
ret = switch(a) {
|
|
case 50:
|
|
a = 60;
|
|
continue; // request fallthrough. D uses "goto case", but I haven't implemented any goto yet so continue is best fit
|
|
case 90:
|
|
a = 70;
|
|
}
|
|
|
|
assert(a == 70); // the explicit `continue` requests fallthrough behavior
|
|
assert(ret == 70);
|
|
});
|
|
}
|
|
|
|
unittest {
|
|
// overloads
|
|
interpret(q{
|
|
function foo(int a) { return 10 + a; }
|
|
function foo(float a) { return 100 + a; }
|
|
function foo(string a) { return "string " ~ a; }
|
|
|
|
assert(foo(4) == 14);
|
|
assert(foo(4.5) == 104.5);
|
|
assert(foo("test") == "string test");
|
|
|
|
// can redefine specific override
|
|
function foo(int a) { return a; }
|
|
assert(foo(4) == 4);
|
|
// leaving others in place
|
|
assert(foo(4.5) == 104.5);
|
|
assert(foo("test") == "string test");
|
|
});
|
|
}
|
|
|
|
unittest {
|
|
// catching objects
|
|
interpret(q{
|
|
class Foo {}
|
|
class Bar : Foo {}
|
|
|
|
var res = try throw new Bar(); catch(Bar b) { 2 } catch(e) { 1 };
|
|
assert(res == 2);
|
|
|
|
var res = try throw new Foo(); catch(Bar b) { 2 } catch(e) { 1 };
|
|
assert(res == 1);
|
|
|
|
var res = try throw Foo; catch(Foo b) { 2 } catch(e) { 1 };
|
|
assert(res == 2);
|
|
});
|
|
}
|
|
|
|
unittest {
|
|
// ternary precedence
|
|
interpret(q{
|
|
assert(0 == 0 ? true : false == true);
|
|
assert((0 == 0) ? true : false == true);
|
|
// lol FIXME
|
|
//assert(((0 == 0) ? true : false) == true);
|
|
});
|
|
}
|
|
|
|
unittest {
|
|
// new nested class
|
|
interpret(q{
|
|
class A {}
|
|
A.b = class B { var c; this(a) { this.c = a; } }
|
|
var c = new A.b(5);
|
|
assert(A.b.c == null);
|
|
assert(c.c == 5);
|
|
});
|
|
}
|
|
|
|
unittest {
|
|
interpret(q{
|
|
assert(0x10 == 16);
|
|
assert(0o10 == 8);
|
|
assert(0b10 == 2);
|
|
assert(10 == 10);
|
|
assert(10_10 == 1010);
|
|
});
|
|
}
|
|
|
|
class ForeachExpression : Expression {
|
|
VariableDeclaration decl;
|
|
Expression subject;
|
|
Expression subject2;
|
|
Expression loopBody;
|
|
|
|
override string toString() {
|
|
return "foreach(" ~ decl.toString() ~ "; " ~ subject.toString() ~ ((subject2 is null) ? "" : (".." ~ subject2.toString)) ~ ") " ~ loopBody.toString();
|
|
}
|
|
|
|
override InterpretResult interpret(PrototypeObject sc) {
|
|
var result;
|
|
|
|
assert(loopBody !is null);
|
|
|
|
auto loopScope = new PrototypeObject();
|
|
loopScope.prototype = sc;
|
|
|
|
InterpretResult.FlowControl flowControl;
|
|
|
|
static string doLoopBody() { return q{
|
|
if(decl.identifiers.length > 1) {
|
|
sc._getMember(decl.identifiers[0], false, false) = i;
|
|
sc._getMember(decl.identifiers[1], false, false) = item;
|
|
} else {
|
|
sc._getMember(decl.identifiers[0], false, false) = item;
|
|
}
|
|
|
|
auto res = loopBody.interpret(loopScope);
|
|
result = res.value;
|
|
flowControl = res.flowControl;
|
|
if(flowControl == InterpretResult.FlowControl.Break)
|
|
break;
|
|
if(flowControl == InterpretResult.FlowControl.Return)
|
|
break;
|
|
//if(flowControl == InterpretResult.FlowControl.Continue)
|
|
// this is fine, we still want to do the advancement
|
|
};}
|
|
|
|
var what = subject.interpret(sc).value;
|
|
var termination = subject2 is null ? var(null) : subject2.interpret(sc).value;
|
|
if(what.payloadType == var.Type.Integral && subject2 is null) {
|
|
// loop from 0 to what
|
|
int end = what.get!int;
|
|
foreach(item; 0 .. end) {
|
|
auto i = item;
|
|
mixin(doLoopBody());
|
|
}
|
|
} else if(what.payloadType == var.Type.Integral && termination.payloadType == var.Type.Integral) {
|
|
// loop what .. termination
|
|
int start = what.get!int;
|
|
int end = termination.get!int;
|
|
int stride;
|
|
if(end < start) {
|
|
stride = -1;
|
|
} else {
|
|
stride = 1;
|
|
}
|
|
int i = -1;
|
|
for(int item = start; item != end; item += stride) {
|
|
i++;
|
|
mixin(doLoopBody());
|
|
}
|
|
} else {
|
|
if(subject2 !is null)
|
|
throw new ScriptRuntimeException("foreach( a .. b ) invalid unless a is an integer", null, 0); // FIXME
|
|
foreach(i, item; what) {
|
|
mixin(doLoopBody());
|
|
}
|
|
}
|
|
|
|
if(flowControl != InterpretResult.FlowControl.Return)
|
|
flowControl = InterpretResult.FlowControl.Normal;
|
|
|
|
return InterpretResult(result, sc, flowControl);
|
|
}
|
|
}
|
|
|
|
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);
|
|
|
|
InterpretResult.FlowControl flowControl;
|
|
|
|
static string doLoopBody() { return q{
|
|
auto res = loopBody.interpret(loopScope);
|
|
result = res.value;
|
|
flowControl = res.flowControl;
|
|
if(flowControl == InterpretResult.FlowControl.Break)
|
|
break;
|
|
if(flowControl == InterpretResult.FlowControl.Return)
|
|
break;
|
|
//if(flowControl == InterpretResult.FlowControl.Continue)
|
|
// this is fine, we still want to do the advancement
|
|
if(advancement)
|
|
advancement.interpret(loopScope);
|
|
};}
|
|
|
|
if(condition !is null) {
|
|
while(condition.interpret(loopScope).value) {
|
|
mixin(doLoopBody());
|
|
}
|
|
} else
|
|
while(true) {
|
|
mixin(doLoopBody());
|
|
}
|
|
|
|
if(flowControl != InterpretResult.FlowControl.Return)
|
|
flowControl = InterpretResult.FlowControl.Normal;
|
|
|
|
return InterpretResult(result, sc, flowControl);
|
|
}
|
|
|
|
override string toString() {
|
|
string code = "for(";
|
|
if(initialization !is null)
|
|
code ~= initialization.toString();
|
|
code ~= "; ";
|
|
if(condition !is null)
|
|
code ~= condition.toString();
|
|
code ~= "; ";
|
|
if(advancement !is null)
|
|
code ~= advancement.toString();
|
|
code ~= ") ";
|
|
code ~= loopBody.toString();
|
|
|
|
return code;
|
|
}
|
|
}
|
|
|
|
class IfExpression : Expression {
|
|
Expression condition;
|
|
Expression ifTrue;
|
|
Expression ifFalse;
|
|
|
|
this() {}
|
|
|
|
override InterpretResult interpret(PrototypeObject sc) {
|
|
InterpretResult 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);
|
|
} else {
|
|
if(ifFalse !is null)
|
|
result = ifFalse.interpret(ifScope);
|
|
}
|
|
return InterpretResult(result.value, sc, result.flowControl);
|
|
}
|
|
|
|
override string toString() {
|
|
string code = "if ";
|
|
code ~= condition.toString();
|
|
code ~= " ";
|
|
if(ifTrue !is null)
|
|
code ~= ifTrue.toString();
|
|
else
|
|
code ~= " { }";
|
|
if(ifFalse !is null)
|
|
code ~= " else " ~ ifFalse.toString();
|
|
return code;
|
|
}
|
|
}
|
|
|
|
class TernaryExpression : Expression {
|
|
Expression condition;
|
|
Expression ifTrue;
|
|
Expression ifFalse;
|
|
|
|
this() {}
|
|
|
|
override InterpretResult interpret(PrototypeObject sc) {
|
|
InterpretResult result;
|
|
assert(condition !is null);
|
|
|
|
auto ifScope = new PrototypeObject();
|
|
ifScope.prototype = sc;
|
|
|
|
if(condition.interpret(ifScope).value) {
|
|
result = ifTrue.interpret(ifScope);
|
|
} else {
|
|
result = ifFalse.interpret(ifScope);
|
|
}
|
|
return InterpretResult(result.value, sc, result.flowControl);
|
|
}
|
|
|
|
override string toString() {
|
|
string code = "";
|
|
code ~= condition.toString();
|
|
code ~= " ? ";
|
|
code ~= ifTrue.toString();
|
|
code ~= " : ";
|
|
code ~= ifFalse.toString();
|
|
return code;
|
|
}
|
|
}
|
|
|
|
// this is kinda like a placement new, and currently isn't exposed inside the language,
|
|
// but is used for class inheritance
|
|
class ShallowCopyExpression : 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", null, 0 /* FIXME */);
|
|
|
|
v.getVar(sc, false)._object.copyPropertiesFrom(e2.interpret(sc).value._object);
|
|
|
|
return InterpretResult(var(null), sc);
|
|
}
|
|
|
|
}
|
|
|
|
class NewExpression : Expression {
|
|
Expression what;
|
|
Expression[] args;
|
|
this(Expression w) {
|
|
what = w;
|
|
}
|
|
|
|
override InterpretResult interpret(PrototypeObject sc) {
|
|
assert(what !is null);
|
|
|
|
var[] args;
|
|
foreach(arg; this.args)
|
|
args ~= arg.interpret(sc).value;
|
|
|
|
var original = what.interpret(sc).value;
|
|
var n = original._copy_new;
|
|
if(n.payloadType() == var.Type.Object) {
|
|
var ctor = original.prototype ? original.prototype._getOwnProperty("__ctor") : var(null);
|
|
if(ctor)
|
|
ctor.apply(n, args);
|
|
}
|
|
|
|
return InterpretResult(n, 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, ScriptLocation(where.scriptFilename, where.lineNumber));
|
|
assert(0);
|
|
}
|
|
}
|
|
|
|
bool isCompatibleType(var v, string specifier, PrototypeObject sc) {
|
|
var t = toType(specifier, sc);
|
|
auto score = typeCompatibilityScore(v, t);
|
|
return score > 0;
|
|
}
|
|
|
|
var toType(string specifier, PrototypeObject sc) {
|
|
switch(specifier) {
|
|
case "int", "long": return var(0);
|
|
case "float", "double": return var(0.0);
|
|
case "string": return var("");
|
|
default:
|
|
auto got = sc._peekMember(specifier, true);
|
|
if(got)
|
|
return *got;
|
|
else
|
|
return var.init;
|
|
}
|
|
}
|
|
|
|
var[] toTypes(string[] specifiers, PrototypeObject sc) {
|
|
var[] arr;
|
|
foreach(s; specifiers)
|
|
arr ~= toType(s, sc);
|
|
return arr;
|
|
}
|
|
|
|
|
|
class ExceptionBlockExpression : Expression {
|
|
Expression tryExpression;
|
|
|
|
string[] catchVarDecls;
|
|
string[] catchVarTypeSpecifiers;
|
|
Expression[] catchExpressions;
|
|
|
|
Expression[] finallyExpressions;
|
|
|
|
override InterpretResult interpret(PrototypeObject sc) {
|
|
InterpretResult result;
|
|
result.sc = sc;
|
|
assert(tryExpression !is null);
|
|
assert(catchVarDecls.length == catchExpressions.length);
|
|
|
|
void caught(var ex) {
|
|
if(catchExpressions.length)
|
|
foreach(i, ce; catchExpressions) {
|
|
if(catchVarTypeSpecifiers[i].length == 0 || isCompatibleType(ex, catchVarTypeSpecifiers[i], sc)) {
|
|
auto catchScope = new PrototypeObject();
|
|
catchScope.prototype = sc;
|
|
catchScope._getMember(catchVarDecls[i], false, false) = ex;
|
|
|
|
result = ce.interpret(catchScope);
|
|
break;
|
|
}
|
|
} else
|
|
result = InterpretResult(ex, sc);
|
|
}
|
|
|
|
if(catchExpressions.length || (catchExpressions.length == 0 && finallyExpressions.length == 0))
|
|
try {
|
|
result = tryExpression.interpret(sc);
|
|
} catch(NonScriptCatchableException e) {
|
|
// the script cannot catch these so it continues up regardless
|
|
throw e;
|
|
} catch(ScriptException e) {
|
|
// FIXME: what about the other information here? idk.
|
|
caught(e.payload);
|
|
} catch(Exception e) {
|
|
var ex = var.emptyObject;
|
|
ex.type = typeid(e).name;
|
|
ex.msg = e.msg;
|
|
ex.file = e.file;
|
|
ex.line = e.line;
|
|
|
|
caught(ex);
|
|
} 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 string toString() {
|
|
return "(" ~ inside.toString() ~ ")";
|
|
}
|
|
|
|
override InterpretResult interpret(PrototypeObject sc) {
|
|
return InterpretResult(inside.interpret(sc).value, sc);
|
|
}
|
|
}
|
|
|
|
class AssertKeyword : Expression {
|
|
ScriptToken token;
|
|
this(ScriptToken token) {
|
|
this.token = token;
|
|
}
|
|
override string toString() {
|
|
return "assert";
|
|
}
|
|
|
|
override InterpretResult interpret(PrototypeObject sc) {
|
|
if(AssertKeywordObject is null)
|
|
AssertKeywordObject = new PrototypeObject();
|
|
var dummy;
|
|
dummy._object = AssertKeywordObject;
|
|
return InterpretResult(dummy, sc);
|
|
}
|
|
}
|
|
|
|
PrototypeObject AssertKeywordObject;
|
|
PrototypeObject DefaultArgumentDummyObject;
|
|
|
|
class CallExpression : Expression {
|
|
Expression func;
|
|
Expression[] arguments;
|
|
ScriptLocation loc;
|
|
|
|
override string toString() {
|
|
string s = func.toString() ~ "(";
|
|
foreach(i, arg; arguments) {
|
|
if(i) s ~= ", ";
|
|
s ~= arg.toString();
|
|
}
|
|
|
|
s ~= ")";
|
|
return s;
|
|
}
|
|
|
|
this(ScriptLocation loc, Expression func) {
|
|
this.loc = loc;
|
|
this.func = func;
|
|
}
|
|
|
|
override string toInterpretedString(PrototypeObject sc) {
|
|
return interpret(sc).value.get!string;
|
|
}
|
|
|
|
override InterpretResult interpret(PrototypeObject sc) {
|
|
if(auto asrt = cast(AssertKeyword) func) {
|
|
auto assertExpression = arguments[0];
|
|
Expression assertString;
|
|
if(arguments.length > 1)
|
|
assertString = arguments[1];
|
|
|
|
var v = assertExpression.interpret(sc).value;
|
|
|
|
if(!v)
|
|
throw new ScriptException(
|
|
var(this.toString() ~ " failed, got: " ~ assertExpression.toInterpretedString(sc)),
|
|
ScriptLocation(asrt.token.scriptFilename, asrt.token.lineNumber));
|
|
|
|
return InterpretResult(v, sc);
|
|
}
|
|
|
|
auto f = func.interpret(sc).value;
|
|
bool isMacro = (f.payloadType == var.Type.Object && ((cast(MacroPrototype) f._payload._object) !is null));
|
|
var[] args;
|
|
foreach(argument; arguments)
|
|
if(argument !is null) {
|
|
if(isMacro) // macro, pass the argument as an expression object
|
|
args ~= argument.toScriptExpressionObject(sc);
|
|
else // regular function, interpret the arguments
|
|
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.e1.interpret(sc).value;
|
|
} else if(auto ide = cast(IndexExpression) func) {
|
|
_this = ide.interpret(sc).value;
|
|
} else if(auto se = cast(SuperExpression) func) {
|
|
// super things are passed this object despite looking things up on the prototype
|
|
// so it calls the correct instance
|
|
_this = sc._getMember("this", true, true);
|
|
}
|
|
|
|
try {
|
|
return InterpretResult(f.apply(_this, args), sc);
|
|
} catch(DynamicTypeException dte) {
|
|
dte.callStack ~= loc;
|
|
throw dte;
|
|
} catch(ScriptException se) {
|
|
se.callStack ~= loc;
|
|
throw se;
|
|
}
|
|
}
|
|
}
|
|
|
|
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", null, 0, file, line);
|
|
auto next = tokens.front;
|
|
if(next.type != type || (str !is null && next.str != str))
|
|
throw new ScriptCompileException("unexpected '"~next.str~"' while expecting " ~ to!string(type) ~ " " ~ str, next.scriptFilename, 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, ScriptLocation(token.scriptFilename, token.lineNumber));
|
|
}
|
|
throw new ScriptCompileException("Found "~token.str~" when expecting identifier", token.scriptFilename, token.lineNumber);
|
|
}
|
|
|
|
Expression parseDottedVariableName(MyTokenStreamHere)(ref MyTokenStreamHere tokens) {
|
|
assert(!tokens.empty);
|
|
|
|
auto ve = parseVariableName(tokens);
|
|
|
|
auto token = tokens.front;
|
|
if(token.type == ScriptToken.Type.symbol && token.str == ".") {
|
|
tokens.popFront();
|
|
return new DotVarExpression(ve, parseVariableName(tokens));
|
|
}
|
|
return ve;
|
|
}
|
|
|
|
|
|
Expression parsePart(MyTokenStreamHere)(ref MyTokenStreamHere tokens) {
|
|
if(!tokens.empty) {
|
|
auto token = tokens.front;
|
|
|
|
Expression e;
|
|
|
|
if(token.str == "super") {
|
|
tokens.popFront();
|
|
VariableExpression dot;
|
|
if(!tokens.empty && tokens.front.str == ".") {
|
|
tokens.popFront();
|
|
dot = parseVariableName(tokens);
|
|
}
|
|
e = new SuperExpression(dot);
|
|
}
|
|
else if(token.type == ScriptToken.Type.identifier)
|
|
e = parseVariableName(tokens);
|
|
else if(token.type == ScriptToken.Type.symbol && (token.str == "-" || token.str == "+" || token.str == "!" || token.str == "~")) {
|
|
auto op = token.str;
|
|
tokens.popFront();
|
|
|
|
e = parsePart(tokens);
|
|
if(op == "-")
|
|
e = new NegationExpression(e);
|
|
else if(op == "!")
|
|
e = new NotExpression(e);
|
|
else if(op == "~")
|
|
e = new BitFlipExpression(e);
|
|
} else {
|
|
tokens.popFront();
|
|
|
|
if(token.type == ScriptToken.Type.int_number)
|
|
e = new IntLiteralExpression(token.str, 10);
|
|
else if(token.type == ScriptToken.Type.oct_number)
|
|
e = new IntLiteralExpression(token.str, 8);
|
|
else if(token.type == ScriptToken.Type.hex_number)
|
|
e = new IntLiteralExpression(token.str, 16);
|
|
else if(token.type == ScriptToken.Type.binary_number)
|
|
e = new IntLiteralExpression(token.str, 2);
|
|
else if(token.type == ScriptToken.Type.float_number)
|
|
e = new FloatLiteralExpression(token.str);
|
|
else if(token.type == ScriptToken.Type.string)
|
|
e = new StringLiteralExpression(token);
|
|
else if(token.type == ScriptToken.Type.symbol || token.type == ScriptToken.Type.keyword) {
|
|
switch(token.str) {
|
|
case "true":
|
|
case "false":
|
|
e = new BoolLiteralExpression(token.str);
|
|
break;
|
|
case "new":
|
|
// FIXME: why is this needed here? maybe it should be here instead of parseExpression
|
|
tokens.pushFront(token);
|
|
return parseExpression(tokens);
|
|
case "(":
|
|
//tokens.popFront();
|
|
auto parenthetical = new ParentheticalExpression(parseExpression(tokens));
|
|
tokens.requireNextToken(ScriptToken.Type.symbol, ")");
|
|
|
|
return parenthetical;
|
|
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.scriptFilename, 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;
|
|
case "json!q{":
|
|
case "#{":
|
|
// 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.scriptFilename, 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.scriptFilename, key.lineNumber);
|
|
|
|
}
|
|
|
|
tokens.requireNextToken(ScriptToken.Type.symbol, ":");
|
|
|
|
auto value = parseExpression(tokens);
|
|
if(tokens.empty)
|
|
throw new ScriptCompileException("unclosed object literal", key.scriptFilename, key.lineNumber);
|
|
|
|
if(tokens.peekNextToken(ScriptToken.Type.symbol, ","))
|
|
tokens.popFront();
|
|
|
|
obj.elements[key.str] = value;
|
|
|
|
goto moreKeys;
|
|
case "macro":
|
|
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);
|
|
exp.isMacro = token.str == "macro";
|
|
|
|
e = exp;
|
|
break;
|
|
case "null":
|
|
e = new NullLiteralExpression();
|
|
break;
|
|
case "mixin":
|
|
case "eval":
|
|
tokens.requireNextToken(ScriptToken.Type.symbol, "(");
|
|
e = new MixinExpression(parseExpression(tokens));
|
|
tokens.requireNextToken(ScriptToken.Type.symbol, ")");
|
|
break;
|
|
default:
|
|
goto unknown;
|
|
}
|
|
} else {
|
|
unknown:
|
|
throw new ScriptCompileException("unexpected '"~token.str~"' when reading ident", token.scriptFilename, 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();
|
|
auto e1 = parseExpression(tokens);
|
|
if(tokens.peekNextToken(ScriptToken.Type.symbol, "..")) {
|
|
tokens.popFront();
|
|
e = new SliceExpression(e, e1, parseExpression(tokens));
|
|
} else {
|
|
e = new IndexExpression(e, e1);
|
|
}
|
|
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;
|
|
}
|
|
|
|
throw new ScriptCompileException("Ran out of tokens when trying to parsePart", null, 0);
|
|
}
|
|
|
|
Expression parseArguments(MyTokenStreamHere)(ref MyTokenStreamHere tokens, Expression exp, ref Expression[] where) {
|
|
// arguments.
|
|
auto 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();
|
|
where ~= null;
|
|
} else {
|
|
where ~= parseExpression(tokens);
|
|
}
|
|
|
|
if(tokens.empty)
|
|
throw new ScriptCompileException("unexpected end of file when parsing call expression", peek.scriptFilename, 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.scriptFilename, peek.lineNumber);
|
|
|
|
}
|
|
|
|
Expression parseFunctionCall(MyTokenStreamHere)(ref MyTokenStreamHere tokens, Expression e) {
|
|
assert(!tokens.empty);
|
|
auto peek = tokens.front;
|
|
auto exp = new CallExpression(ScriptLocation(peek.scriptFilename, peek.lineNumber), e);
|
|
tokens.popFront();
|
|
if(tokens.empty)
|
|
throw new ScriptCompileException("unexpected end of file when parsing call expression", peek.scriptFilename, peek.lineNumber);
|
|
return parseArguments(tokens, exp, exp.arguments);
|
|
}
|
|
|
|
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 ">>":
|
|
case ">>>":
|
|
case "*":
|
|
case "/":
|
|
case "%":
|
|
tokens.popFront();
|
|
e1 = new BinaryExpression(peek.str, e1, parsePart(tokens));
|
|
break;
|
|
default:
|
|
break loop;
|
|
}
|
|
} else throw new Exception("Got " ~ peek.str ~ " when expecting symbol");
|
|
}
|
|
|
|
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
|
|
case ",": // possible FIXME these are passed on to the next thing
|
|
case ";":
|
|
case ":": // idk
|
|
case "?":
|
|
return e1;
|
|
|
|
case "|>":
|
|
tokens.popFront();
|
|
e1 = new PipelineExpression(ScriptLocation(peek.scriptFilename, peek.lineNumber), e1, parseFactor(tokens));
|
|
break;
|
|
case ".":
|
|
tokens.popFront();
|
|
e1 = new DotVarExpression(e1, parseVariableName(tokens));
|
|
break;
|
|
case "=":
|
|
tokens.popFront();
|
|
return new AssignExpression(e1, parseExpression(tokens));
|
|
case "&&": // thanks to mzfhhhh for fix
|
|
case "||":
|
|
tokens.popFront();
|
|
e1 = new BinaryExpression(peek.str, e1, parseExpression(tokens));
|
|
break;
|
|
case "~":
|
|
// FIXME: make sure this has the right associativity
|
|
|
|
case "&":
|
|
case "|":
|
|
case "^":
|
|
|
|
case "&=":
|
|
case "|=":
|
|
case "^=":
|
|
|
|
case "+":
|
|
case "-":
|
|
|
|
case "==":
|
|
case "!=":
|
|
case "<=":
|
|
case ">=":
|
|
case "<":
|
|
case ">":
|
|
tokens.popFront();
|
|
e1 = new BinaryExpression(peek.str, e1, parseAddend(tokens));
|
|
break;
|
|
case "+=":
|
|
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.scriptFilename, 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.scriptFilename, peek.lineNumber);
|
|
}
|
|
|
|
return e1;
|
|
}
|
|
|
|
Expression parseExpression(MyTokenStreamHere)(ref MyTokenStreamHere tokens, bool consumeEnd = false) {
|
|
Expression ret;
|
|
ScriptToken first;
|
|
string expectedEnd = ";";
|
|
//auto e1 = parseFactor(tokens);
|
|
|
|
while(tokens.peekNextToken(ScriptToken.Type.symbol, ";")) {
|
|
tokens.popFront();
|
|
}
|
|
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.scriptFilename, 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);
|
|
ret = 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, "new")) {
|
|
auto start = tokens.front;
|
|
tokens.popFront();
|
|
|
|
auto expr = parseDottedVariableName(tokens);
|
|
auto ne = new NewExpression(expr);
|
|
if(tokens.peekNextToken(ScriptToken.Type.symbol, "(")) {
|
|
tokens.popFront();
|
|
parseArguments(tokens, ne, ne.args);
|
|
}
|
|
|
|
ret = ne;
|
|
} else if(tokens.peekNextToken(ScriptToken.Type.keyword, "class")) {
|
|
auto start = tokens.front;
|
|
tokens.popFront();
|
|
|
|
Expression[] expressions;
|
|
|
|
// the way classes work is they are actually object literals with a different syntax. new foo then just copies it
|
|
/*
|
|
we create a prototype object
|
|
we create an object, with that prototype
|
|
|
|
set all functions and static stuff to the prototype
|
|
the rest goes to the object
|
|
|
|
the expression returns the object we made
|
|
*/
|
|
|
|
auto vars = new VariableDeclaration();
|
|
vars.identifiers = ["__proto", "__obj"];
|
|
|
|
auto staticScopeBacking = new PrototypeObject();
|
|
auto instanceScopeBacking = new PrototypeObject();
|
|
|
|
vars.initializers = [new ObjectLiteralExpression(staticScopeBacking), new ObjectLiteralExpression(instanceScopeBacking)];
|
|
expressions ~= vars;
|
|
|
|
// FIXME: operators need to have their this be bound somehow since it isn't passed
|
|
// OR the op rewrite could pass this
|
|
|
|
expressions ~= new AssignExpression(
|
|
new DotVarExpression(new VariableExpression("__obj"), new VariableExpression("prototype")),
|
|
new VariableExpression("__proto"));
|
|
|
|
auto classIdent = tokens.requireNextToken(ScriptToken.Type.identifier);
|
|
|
|
expressions ~= new AssignExpression(
|
|
new DotVarExpression(new VariableExpression("__proto"), new VariableExpression("__classname")),
|
|
new StringLiteralExpression(classIdent.str));
|
|
|
|
if(tokens.peekNextToken(ScriptToken.Type.symbol, ":")) {
|
|
tokens.popFront();
|
|
auto inheritFrom = tokens.requireNextToken(ScriptToken.Type.identifier);
|
|
|
|
// we set our prototype to the Foo prototype, thereby inheriting any static data that way (includes functions)
|
|
// the inheritFrom object itself carries instance data that we need to copy onto our instance
|
|
expressions ~= new AssignExpression(
|
|
new DotVarExpression(new VariableExpression("__proto"), new VariableExpression("prototype")),
|
|
new DotVarExpression(new VariableExpression(inheritFrom.str), new VariableExpression("prototype")));
|
|
|
|
expressions ~= new AssignExpression(
|
|
new DotVarExpression(new VariableExpression("__proto"), new VariableExpression("super")),
|
|
new VariableExpression(inheritFrom.str)
|
|
);
|
|
|
|
// and copying the instance initializer from the parent
|
|
expressions ~= new ShallowCopyExpression(new VariableExpression("__obj"), new VariableExpression(inheritFrom.str));
|
|
}
|
|
|
|
tokens.requireNextToken(ScriptToken.Type.symbol, "{");
|
|
|
|
void addVarDecl(VariableDeclaration decl, string o) {
|
|
foreach(i, ident; decl.identifiers) {
|
|
// FIXME: make sure this goes on the instance, never the prototype!
|
|
expressions ~= new AssignExpression(
|
|
new DotVarExpression(
|
|
new VariableExpression(o),
|
|
new VariableExpression(ident),
|
|
false),
|
|
decl.initializers[i],
|
|
true // no overloading because otherwise an early opIndexAssign can mess up the decls
|
|
);
|
|
}
|
|
}
|
|
|
|
// FIXME: we could actually add private vars and just put them in this scope. maybe
|
|
|
|
while(!tokens.peekNextToken(ScriptToken.Type.symbol, "}")) {
|
|
if(tokens.peekNextToken(ScriptToken.Type.symbol, ";")) {
|
|
tokens.popFront();
|
|
continue;
|
|
}
|
|
|
|
if(tokens.peekNextToken(ScriptToken.Type.identifier, "this")) {
|
|
// ctor
|
|
tokens.popFront();
|
|
tokens.requireNextToken(ScriptToken.Type.symbol, "(");
|
|
auto args = parseVariableDeclaration(tokens, ")");
|
|
tokens.requireNextToken(ScriptToken.Type.symbol, ")");
|
|
auto bod = parseExpression(tokens);
|
|
|
|
expressions ~= new AssignExpression(
|
|
new DotVarExpression(
|
|
new VariableExpression("__proto"),
|
|
new VariableExpression("__ctor")),
|
|
new FunctionLiteralExpression(args, bod, staticScopeBacking));
|
|
} else if(tokens.peekNextToken(ScriptToken.Type.keyword, "var")) {
|
|
// instance variable
|
|
auto decl = parseVariableDeclaration(tokens, ";");
|
|
addVarDecl(decl, "__obj");
|
|
} else if(tokens.peekNextToken(ScriptToken.Type.keyword, "static")) {
|
|
// prototype var
|
|
tokens.popFront();
|
|
auto decl = parseVariableDeclaration(tokens, ";");
|
|
addVarDecl(decl, "__proto");
|
|
} else if(tokens.peekNextToken(ScriptToken.Type.keyword, "function")) {
|
|
// prototype function
|
|
tokens.popFront();
|
|
auto ident = tokens.requireNextToken(ScriptToken.Type.identifier);
|
|
|
|
tokens.requireNextToken(ScriptToken.Type.symbol, "(");
|
|
VariableDeclaration args;
|
|
if(!tokens.peekNextToken(ScriptToken.Type.symbol, ")"))
|
|
args = parseVariableDeclaration(tokens, ")");
|
|
tokens.requireNextToken(ScriptToken.Type.symbol, ")");
|
|
auto bod = parseExpression(tokens);
|
|
|
|
expressions ~= new FunctionDeclaration(
|
|
new DotVarExpression(
|
|
new VariableExpression("__proto"),
|
|
new VariableExpression(ident.str),
|
|
false),
|
|
ident.str,
|
|
new FunctionLiteralExpression(args, bod, staticScopeBacking)
|
|
);
|
|
} else throw new ScriptCompileException("Unexpected " ~ tokens.front.str ~ " when reading class decl", tokens.front.scriptFilename, tokens.front.lineNumber);
|
|
}
|
|
|
|
tokens.requireNextToken(ScriptToken.Type.symbol, "}");
|
|
|
|
// returning he object from the scope...
|
|
expressions ~= new VariableExpression("__obj");
|
|
|
|
auto scopeExpr = new ScopeExpression(expressions);
|
|
auto classVarExpr = new VariableDeclaration();
|
|
classVarExpr.identifiers = [classIdent.str];
|
|
classVarExpr.initializers = [scopeExpr];
|
|
|
|
ret = classVarExpr;
|
|
} 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, "switch")) {
|
|
tokens.popFront();
|
|
auto e = new SwitchExpression();
|
|
tokens.requireNextToken(ScriptToken.Type.symbol, "(");
|
|
e.expr = parseExpression(tokens);
|
|
tokens.requireNextToken(ScriptToken.Type.symbol, ")");
|
|
|
|
tokens.requireNextToken(ScriptToken.Type.symbol, "{");
|
|
|
|
while(!tokens.peekNextToken(ScriptToken.Type.symbol, "}")) {
|
|
|
|
if(tokens.peekNextToken(ScriptToken.Type.keyword, "case")) {
|
|
auto start = tokens.front;
|
|
tokens.popFront();
|
|
auto c = new CaseExpression(parseExpression(tokens));
|
|
e.cases ~= c;
|
|
tokens.requireNextToken(ScriptToken.Type.symbol, ":");
|
|
|
|
while(!tokens.peekNextToken(ScriptToken.Type.keyword, "default") && !tokens.peekNextToken(ScriptToken.Type.keyword, "case") && !tokens.peekNextToken(ScriptToken.Type.symbol, "}")) {
|
|
c.expressions ~= parseStatement(tokens);
|
|
while(tokens.peekNextToken(ScriptToken.Type.symbol, ";"))
|
|
tokens.popFront();
|
|
}
|
|
} else if(tokens.peekNextToken(ScriptToken.Type.keyword, "default")) {
|
|
tokens.popFront();
|
|
tokens.requireNextToken(ScriptToken.Type.symbol, ":");
|
|
|
|
auto c = new CaseExpression(null);
|
|
|
|
while(!tokens.peekNextToken(ScriptToken.Type.keyword, "case") && !tokens.peekNextToken(ScriptToken.Type.symbol, "}")) {
|
|
c.expressions ~= parseStatement(tokens);
|
|
while(tokens.peekNextToken(ScriptToken.Type.symbol, ";"))
|
|
tokens.popFront();
|
|
}
|
|
|
|
e.cases ~= c;
|
|
e.default_ = c;
|
|
} else throw new ScriptCompileException("A switch statement must consists of cases and a default, nothing else ", tokens.front.scriptFilename, tokens.front.lineNumber);
|
|
}
|
|
|
|
tokens.requireNextToken(ScriptToken.Type.symbol, "}");
|
|
expectedEnd = "";
|
|
|
|
ret = e;
|
|
|
|
} else if(tokens.peekNextToken(ScriptToken.Type.keyword, "foreach")) {
|
|
tokens.popFront();
|
|
auto e = new ForeachExpression();
|
|
tokens.requireNextToken(ScriptToken.Type.symbol, "(");
|
|
e.decl = parseVariableDeclaration(tokens, ";");
|
|
tokens.requireNextToken(ScriptToken.Type.symbol, ";");
|
|
e.subject = parseExpression(tokens);
|
|
|
|
if(tokens.peekNextToken(ScriptToken.Type.symbol, "..")) {
|
|
tokens.popFront;
|
|
e.subject2 = parseExpression(tokens);
|
|
}
|
|
|
|
tokens.requireNextToken(ScriptToken.Type.symbol, ")");
|
|
e.loopBody = parseExpression(tokens);
|
|
ret = e;
|
|
|
|
expectedEnd = "";
|
|
} else if(tokens.peekNextToken(ScriptToken.Type.keyword, "cast")) {
|
|
tokens.popFront();
|
|
auto e = new CastExpression();
|
|
|
|
tokens.requireNextToken(ScriptToken.Type.symbol, "(");
|
|
e.type = tokens.requireNextToken(ScriptToken.Type.identifier).str;
|
|
if(tokens.peekNextToken(ScriptToken.Type.symbol, "[")) {
|
|
e.type ~= "[]";
|
|
tokens.popFront();
|
|
tokens.requireNextToken(ScriptToken.Type.symbol, "]");
|
|
}
|
|
tokens.requireNextToken(ScriptToken.Type.symbol, ")");
|
|
|
|
e.e1 = 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, "break") || tokens.peekNextToken(ScriptToken.Type.keyword, "continue")) {
|
|
auto token = tokens.front;
|
|
tokens.popFront();
|
|
ret = new LoopControlExpression(token.str);
|
|
} else if(tokens.peekNextToken(ScriptToken.Type.keyword, "return")) {
|
|
tokens.popFront();
|
|
Expression retVal;
|
|
if(tokens.peekNextToken(ScriptToken.Type.symbol, ";"))
|
|
retVal = new NullLiteralExpression();
|
|
else
|
|
retVal = parseExpression(tokens);
|
|
ret = new ReturnExpression(retVal);
|
|
} 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, true);
|
|
|
|
bool hadFinally = false;
|
|
while(tokens.peekNextToken(ScriptToken.Type.keyword, "catch")) {
|
|
if(hadFinally)
|
|
throw new ScriptCompileException("Catch must come before finally", tokens.front.scriptFilename, tokens.front.lineNumber);
|
|
tokens.popFront();
|
|
tokens.requireNextToken(ScriptToken.Type.symbol, "(");
|
|
if(tokens.peekNextToken(ScriptToken.Type.keyword, "var"))
|
|
tokens.popFront();
|
|
|
|
auto ident = tokens.requireNextToken(ScriptToken.Type.identifier);
|
|
if(tokens.empty) throw new ScriptCompileException("Catch specifier not closed", ident.scriptFilename, ident.lineNumber);
|
|
auto next = tokens.front;
|
|
if(next.type == ScriptToken.Type.identifier) {
|
|
auto type = ident;
|
|
ident = next;
|
|
|
|
e.catchVarTypeSpecifiers ~= type.str;
|
|
e.catchVarDecls ~= ident.str;
|
|
|
|
tokens.popFront();
|
|
|
|
tokens.requireNextToken(ScriptToken.Type.symbol, ")");
|
|
} else {
|
|
e.catchVarTypeSpecifiers ~= null;
|
|
e.catchVarDecls ~= ident.str;
|
|
if(next.type != ScriptToken.Type.symbol || next.str != ")")
|
|
throw new ScriptCompileException("ss Unexpected " ~ next.str ~ " when expecting ')'", next.scriptFilename, next.lineNumber);
|
|
tokens.popFront();
|
|
}
|
|
e.catchExpressions ~= parseExpression(tokens);
|
|
}
|
|
while(tokens.peekNextToken(ScriptToken.Type.keyword, "finally")) {
|
|
hadFinally = 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);
|
|
}
|
|
|
|
if(!tokens.empty && tokens.peekNextToken(ScriptToken.Type.symbol, "?")) {
|
|
auto e = new TernaryExpression();
|
|
e.condition = ret;
|
|
tokens.requireNextToken(ScriptToken.Type.symbol, "?");
|
|
e.ifTrue = parseExpression(tokens);
|
|
tokens.requireNextToken(ScriptToken.Type.symbol, ":");
|
|
e.ifFalse = parseExpression(tokens);
|
|
ret = e;
|
|
}
|
|
} else {
|
|
//assert(0);
|
|
// return null;
|
|
throw new ScriptCompileException("Parse error, unexpected end of input when reading expression", null, 0);//token.lineNumber);
|
|
}
|
|
|
|
//writeln("parsed expression ", ret.toString());
|
|
|
|
if(expectedEnd.length && tokens.empty && consumeEnd) // going loose on final ; at the end of input for repl convenience
|
|
throw new ScriptCompileException("Parse error, unexpected end of input when reading expression, expecting " ~ expectedEnd, first.scriptFilename, first.lineNumber);
|
|
|
|
if(expectedEnd.length && consumeEnd) {
|
|
if(tokens.peekNextToken(ScriptToken.Type.symbol, expectedEnd))
|
|
tokens.popFront();
|
|
// 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.scriptFilename, firstToken.lineNumber);
|
|
|
|
string type;
|
|
|
|
auto next = tokens.front;
|
|
tokens.popFront;
|
|
if(tokens.empty)
|
|
throw new ScriptCompileException("Parse error, incomplete var declaration at end of file", firstToken.scriptFilename, firstToken.lineNumber);
|
|
auto next2 = tokens.front;
|
|
|
|
ScriptToken typeSpecifier;
|
|
|
|
/* if there's two identifiers back to back, it is a type specifier. otherwise just a name */
|
|
|
|
if(next.type == ScriptToken.Type.identifier && next2.type == ScriptToken.Type.identifier) {
|
|
// type ident;
|
|
typeSpecifier = next;
|
|
next = next2;
|
|
// get past the type
|
|
tokens.popFront();
|
|
} else {
|
|
// no type, just carry on with the next thing
|
|
}
|
|
|
|
Expression initializer;
|
|
auto identifier = next;
|
|
if(identifier.type != ScriptToken.Type.identifier)
|
|
throw new ScriptCompileException("Parse error, found '"~identifier.str~"' when expecting var identifier", identifier.scriptFilename, identifier.lineNumber);
|
|
|
|
//tokens.popFront();
|
|
|
|
tryTermination:
|
|
if(tokens.empty)
|
|
throw new ScriptCompileException("Parse error, missing ; after var declaration at end of file", firstToken.scriptFilename, 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.scriptFilename, peek.lineNumber);
|
|
equalOk = false;
|
|
tokens.popFront();
|
|
initializer = parseExpression(tokens);
|
|
goto tryTermination;
|
|
} else if(peek.str == ",") {
|
|
tokens.popFront();
|
|
decl.identifiers ~= identifier.str;
|
|
decl.initializers ~= initializer;
|
|
decl.typeSpecifiers ~= typeSpecifier.str;
|
|
goto anotherVar;
|
|
} else if(peek.str == termination) {
|
|
decl.identifiers ~= identifier.str;
|
|
decl.initializers ~= initializer;
|
|
decl.typeSpecifiers ~= typeSpecifier.str;
|
|
//tokens = tokens[1 .. $];
|
|
// we're done!
|
|
} else
|
|
throw new ScriptCompileException("Parse error, unexpected '"~peek.str~"' when reading var declaration symbol", peek.scriptFilename, peek.lineNumber);
|
|
} else
|
|
throw new ScriptCompileException("Parse error, unexpected non-symbol '"~peek.str~"' when reading var declaration", peek.scriptFilename, 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) {
|
|
// assert
|
|
case "assert":
|
|
tokens.popFront();
|
|
|
|
return parseFunctionCall(tokens, new AssertKeyword(token));
|
|
|
|
//break;
|
|
// declarations
|
|
case "var":
|
|
return parseVariableDeclaration(tokens, ";");
|
|
case ";":
|
|
tokens.popFront(); // FIXME
|
|
goto skip;
|
|
// literals
|
|
case "function":
|
|
case "macro":
|
|
// 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
|
|
|
|
exp.isMacro = token.str == "macro";
|
|
|
|
auto e = new FunctionDeclaration(null, ident.str, 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 "true":
|
|
case "false":
|
|
|
|
case "json!{":
|
|
case "#{":
|
|
case "[":
|
|
case "(":
|
|
case "null":
|
|
|
|
// scope
|
|
case "{":
|
|
case "scope":
|
|
|
|
case "cast":
|
|
|
|
// classes
|
|
case "class":
|
|
case "new":
|
|
|
|
case "super":
|
|
|
|
// flow control
|
|
case "if":
|
|
case "while":
|
|
case "for":
|
|
case "foreach":
|
|
case "switch":
|
|
|
|
// exceptions
|
|
case "try":
|
|
case "throw":
|
|
|
|
// evals
|
|
case "eval":
|
|
case "mixin":
|
|
|
|
// flow
|
|
case "continue":
|
|
case "break":
|
|
case "return":
|
|
return parseExpression(tokens);
|
|
// unary prefix operators
|
|
case "!":
|
|
case "~":
|
|
case "-":
|
|
return parseExpression(tokens);
|
|
|
|
// 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.scriptFilename, token.lineNumber);
|
|
}
|
|
// break;
|
|
case ScriptToken.Type.identifier:
|
|
case ScriptToken.Type.string:
|
|
case ScriptToken.Type.int_number:
|
|
case ScriptToken.Type.float_number:
|
|
case ScriptToken.Type.binary_number:
|
|
case ScriptToken.Type.hex_number:
|
|
case ScriptToken.Type.oct_number:
|
|
return parseExpression(tokens);
|
|
}
|
|
|
|
assert(0);
|
|
}
|
|
|
|
// FIXME someday this should work, my parser is so bad
|
|
// until then put parens around your == stuff.
|
|
version(none)
|
|
unittest {
|
|
interpret(q{
|
|
var a = 5;
|
|
var b = false;
|
|
assert(a == 5 || b);
|
|
});
|
|
}
|
|
version(none)
|
|
unittest {
|
|
interpret(q{
|
|
var a = 5;
|
|
var b = false;
|
|
assert(((a == 5) || b));
|
|
});
|
|
}
|
|
|
|
|
|
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, null, startingLine);
|
|
}
|
|
|
|
if(terminatingSymbol !is null) {
|
|
assert(tokens.front.str == terminatingSymbol);
|
|
tokens.skipNext++;
|
|
}
|
|
|
|
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);
|
|
// this is an entry point that all others lead to, right before getting to interpretExpressions...
|
|
|
|
return interpretExpressions(parseScript(tokens), variables);
|
|
}
|
|
|
|
var interpretStream(MyTokenStreamHere)(MyTokenStreamHere tokens, var variables) if(is(ElementType!MyTokenStreamHere == ScriptToken)) {
|
|
return interpretStream(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);
|
|
}
|
|
|
|
/++
|
|
This is likely your main entry point to the interpreter. It will interpret the script code
|
|
given, with the given global variable object (which will be modified by the script, meaning
|
|
you can pass it to subsequent calls to `interpret` to store context), and return the result
|
|
of the last expression given.
|
|
|
|
---
|
|
var globals = var.emptyObject; // the global object must be an object of some type
|
|
globals.x = 10;
|
|
globals.y = 15;
|
|
// you can also set global functions through this same style, etc
|
|
|
|
var result = interpret(`x + y`, globals);
|
|
assert(result == 25);
|
|
---
|
|
|
|
|
|
$(TIP
|
|
If you want to just call a script function, interpret the definition of it,
|
|
then just call it through the `globals` object you passed to it.
|
|
|
|
---
|
|
var globals = var.emptyObject;
|
|
interpret(`function foo(name) { return "hello, " ~ name ~ "!"; }`, globals);
|
|
var result = globals.foo()("world");
|
|
assert(result == "hello, world!");
|
|
---
|
|
)
|
|
|
|
Params:
|
|
code = the script source code you want to interpret
|
|
scriptFilename = the filename of the script, if you want to provide it. Gives nicer error messages if you provide one.
|
|
variables = The global object of the script context. It will be modified by the user script.
|
|
|
|
Returns:
|
|
the result of the last expression evaluated by the script engine
|
|
+/
|
|
var interpret(string code, var variables = null, string scriptFilename = null, string file = __FILE__, size_t line = __LINE__) {
|
|
if(scriptFilename is null)
|
|
scriptFilename = file ~ "@" ~ to!string(line);
|
|
return interpretStream(
|
|
lexScript(repeat(code, 1), scriptFilename),
|
|
(variables.payloadType() == var.Type.Object && variables._payload._object !is null) ? variables._payload._object : new PrototypeObject());
|
|
}
|
|
|
|
///
|
|
var interpretFile(File file, var globals) {
|
|
import std.algorithm;
|
|
return interpretStream(lexScript(file.byLine.map!((a) => a.idup), file.name),
|
|
(globals.payloadType() == var.Type.Object && globals._payload._object !is null) ? globals._payload._object : new PrototypeObject());
|
|
}
|
|
|
|
/// Enhanced repl uses arsd.terminal for better ux. Added April 26, 2020. Default just uses std.stdio.
|
|
void repl(bool enhanced = false)(var globals) {
|
|
static if(enhanced) {
|
|
import arsd.terminal;
|
|
Terminal terminal = Terminal(ConsoleOutputMode.linear);
|
|
auto lines() {
|
|
struct Range {
|
|
string line;
|
|
string front() { return line; }
|
|
bool empty() { return line is null; }
|
|
void popFront() { line = terminal.getline(": "); terminal.writeln(); }
|
|
}
|
|
Range r;
|
|
r.popFront();
|
|
return r;
|
|
|
|
}
|
|
|
|
void writeln(T...)(T t) {
|
|
terminal.writeln(t);
|
|
terminal.flush();
|
|
}
|
|
} else {
|
|
import std.stdio;
|
|
auto lines() { return stdin.byLine; }
|
|
}
|
|
|
|
bool exited;
|
|
if(globals == null)
|
|
globals = var.emptyObject;
|
|
globals.exit = () { exited = true; };
|
|
|
|
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)(lines))
|
|
, "stdin");
|
|
auto expressions = parseScript(tokens);
|
|
|
|
while(!exited && !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);
|
|
}
|
|
}
|
|
}
|
|
|
|
class ScriptFunctionMetadata : VarMetadata {
|
|
FunctionLiteralExpression fle;
|
|
this(FunctionLiteralExpression fle) {
|
|
this.fle = fle;
|
|
}
|
|
|
|
string convertToString() {
|
|
return fle.toString();
|
|
}
|
|
}
|