Further developments

Published March 30, 2017
Advertisement

Been working hard on Om and added a few new features.

Figured out a way to have implicit [font='courier new']this[/font] in functions, as long as they are defined inside the object of which they are methods. Name lookup rules similar to C++, so a parameter or a local variable is considered first, then an object member, then variables in the outer scope.

var x = "foo";var o ={ f = function { out x; // outputs "bar" if(true) { var x = "fum"; out x; // outputs "fum"; } }; g = function(x) { out x; // outputs whatever the parameter is }; x = "bar";};out x; // outputs "foo"This was easier than I expected to implement. I added a [font='courier new']containers[/font] stack to the compiler [font='courier new']Context[/font] object which is passed around to all the compilation methods, and when compiling [font='courier new']ObjectNodes[/font] and [font='courier new']FunctionNodes[/font], it just pushes the node onto this [font='courier new']containers[/font] stack, then pops it off at the end.

When the [font='courier new']SymbolNode[/font] does its lookup, after it has checked the local symbol table, it just checks to see if the node second-to-top on the container stack is an [font='courier new']ObjectNode[/font] and, if so, uses the [font='courier new']NodeQuery[/font] system to ask it if it has a member with the same name as the symbol.

If so, it just emits the code to push the current [font='courier new']this[/font] pointer onto the stack, and calls the [font='courier new']OpCode::Type::GetMb[/font] or [font='courier new']OpCode::Type::SetMb[/font], essentially emitting exactly the same code that an explicit [font='courier new']ThisNode[/font] would.

I realised shortly afterwards that I then needed some syntax to explicitly refer to the outer scope in an object method, so came up with prefixing a symbol with dots, the number of dots representing how many scopes to jump up to find the symbol. Probably easier explained with an example:

var x = function(a, b){ return a + b; };var o ={ f = function { return "flibble"; };};var f = function{ var x = "bar"; var o = { f = function { out x; // outputs "paul" via property below out .x; // outputs "bar" out ..x(1, 3); // outputs "4" }; n = "paul"; x = property { get { return n; }; set(a) { n = a; }; }; }; o.f(); out .o.f(); // outputs "flibble"};f();This is all compile-time only stuff. I just added a "dots" member to the [font='courier new']SymbolNode[/font] and the compiler just handles a primary expression starting with a dot like:

NodePtr dot(Context &c, bool get){ uint dots = 1; c.scanner.next(get); while(c.scanner.type() == Scanner::TokenType::Dot) { ++dots; c.scanner.next(true); } c.scanner.match(Scanner::TokenType::Id, false); NodePtr n(new SymbolNode(c, c.scanner.token(), dots)); return c.next(n, true);}When [font='courier new']SymbolNode[/font] is generated now, if it has a non-zero dots value, it ignores the local symbol table and the object member lookup and passes its dots value to the method that searches in enclosing scopes, using this to start the searching at the correct level.

All seems to work well and was simple anough to implement.

Have also now got subscript string access working on object members, including the new properties.

var o ={ name = "foo"; func = function { return "bar"; }; prop = property { get { return name; }; set(a) { name = a; }; };};out o["name"]; // outputs "foo"var s = "fun";out o[s + "c"](); // outputs "bar";o["prop"] = "flibble"; // calls the property setterObviously this is a bit less efficient but allows for dynamic name lookup as in the second example above.

Actually, the first and third examples are optimised back to a dot access, since the strings are constant. This was pretty trivial now all the optimisation infrastructure is in place:

void SubscriptNode::optimise(Context &c, Node *parent){ target->optimise(c, this); expr->optimise(c, this); Variant v = expr->query(NodeQuery::AsVariant); if(v.type() == Om::Type::String) { parent->replace(this, new DotNode(c, target, v.toString())); }}Essentially, the node asks if its expression inside the subscripts is a constant of string type and, if so, asks its parent to replace it with a [font='courier new']DotNode[/font]. The generated code is then exactly the same as if you used a dot expression.

out o["fish"];// becomesout o.fish;Because the optimisation system deals with addition of string constants at compile time, even this is equivalent:

out o["f" + "i" + "s" + "h"];// becomesout o.fish;Which is rather cool, but probably not that useful in the real world. Effortless to implement though so may as well do it.

The subscript stuff also takes account of the prototype system of course as indeed do properties.

var b ={ p = property { get { return "foo"; }; };};var o ={ prototype = b;};out o["p"]; // outputs "foo" via b's propertyWhich brings us to an interesting point about properties in a prototype-based-inheritance environment that occured to me the other day.

For those not familiar with languages like Javascript, the prototype system is a nice, simple way to have object inheritance in a dynamic, duck-typed language. The object's [font='courier new']prototype[/font] member points to another object, and when you read a property, the system searches first in the object itself, then up through the prototype chain, looking for the property in each "parent" object.

However, when you write a property, if the object does not have its own copy of the property, a new one is created (the same as if no prototypes are involved) and from then on, the object has its own copy.

Example (in Om, not Javascript :)):
var b ={ name = "base";};var o ={ prototype = b;};out o.name; // outputs "base"o.name = "foo"; // "name" added to o as a new propertyout o.name; // outputs "foo"Now, this is all fine and dandy, but consider the following:

var b ={ name = property { get { return n; }; set(a) { if(a == "") { out "error: name must not be blank"; return; } n = a; }; }; n = "foo";};So we have a base class that uses the properties to "enforce" that the name is not empty. Now:

var o ={ prototype = base;};out o.name; // outputs "foo"o.name = ""; // adds new "name" to oout o.name; // outputs ""Should this be the case? It is an interesting question. I can see arguments both way.

I've just been experimenting with QScript (ECMAScript compliant scripting) and it seems in their implementation, the prototype's setter is called when you assign to the "derived" object instance. Presumably to replace it in a child, you have to use [font='courier new']Object.defineProperty()[/font] or delete the member first and assign the new one.

I decided to go the same way. So when writing, we have to look up the prototype chain and check to see if a base object has a property before we can just add the new value to the child object.

var b ={ n = "foo"; name = property { get { return n; }; set(x) { out "setter ", n, " == ", x; n = x; }; };};var x ={ prototype = b;};var o ={ prototype = x;};out o.name; // calls b name gettero.name = "bar"; // calls b name setterout o.name; // calls b name gettervar s = "name"; // to supress the optimisationout o[s]; // calls b name gettero[s] = "bar"; // calls b name setterout o[s]; // calls b name getterCan't imagine the runtime cost is relevant. The implementation now looks like this:

TypedValue &Imp::findProperty(Machine &machine, uint object, uint id){ auto &e = machine.state.entity(object); if(e.properties.find(id) == e.properties.end()) { uint p = nextObject(e.properties); while(p != invalid_uint) { auto &m = machine.state.entity(p).properties; auto i = m.find(id); if(i != m.end() && i->value.type() == Om::Type::Property) { return m[id]; } p = nextObject(m); } machine.state.tc.inc(id); e.trefs.push_back(id); } return e.properties[id];}So one extra lookup for the majority of cases where the object has no prototype, and a quick run up the chain for those that do.

All working from the C++ side to, via [font='courier new']Om::Value::property()[/font] and [font='courier new']Om::Value::setProperty()[/font]. Sharing a bit of new code between these methods and the virtual machine has actually simplified the code a bit now too.

What this does imply is we now need a way, both in the C++ API and the script, to actually explicitly overwrite a property and add it to the local object.

So the C++ API [font='courier new']Om::Value::setProperty()[/font] now takes an [font='courier new']Om::Value::UpdateType[/font] that defaults to [font='courier new']Om::Value::UpdateType::Normal[/font], but you can pass [font='courier new']Om::Value::UpdateType::Overwrite[/font] to explicitly ignore properties either in the local class or in a prototype parent.

In the script, I decided on adding a new assignment operator: .= (pronounced dot-assign)

var b = { x = property { get { return "getter; "}; set(x) { out "setter"; }; };};var o ={ prototype = b;};o.x = "foo"; // calls b's settero.x .= "bar"; // adds a new x member to oThe new operator works in all the relevant contexts i.e. with the subscript access, and when referencing a member in an object method using the new implicit this e.g.

var o = { n = property { get { return "getter"; }; set(x) { out "setter"; }; }; f = function { n = "foo"; // calls the setter n .= "foo"; // replaces the n property with a string member };};Final little point. I've just implemented for loops in the new version, by copying from the old version and modifying the code to the new system and its a great example of why using exceptions to handle compiler and run-time errors has massively simplified this code base.

This was the function that compiled a for loop in the old version:

bool forStatement(Context &c, BlockNode *block, bool get){ TRACE; bool any = true; if(!c.sc.match(c, Scanner::TokenType::LeftParen, get)) return false; NodePtr init = optional(c, conditionalExpr, { Scanner::TokenType::SemiColon }, any, true); if(any && !init) return false; if(!c.sc.match(c, Scanner::TokenType::SemiColon, false)) return false; ForNode *f = new ForNode(c); block->nodes.push_back(f); if(init) f->init = init.release(); NodePtr cond = optional(c, expr, { Scanner::TokenType::SemiColon }, any, true); if(any && !cond) return false; if(!c.sc.match(c, Scanner::TokenType::SemiColon, false)) return false; if(cond) f->cond = cond.release(); NodePtr post = optional(c, expr, { Scanner::TokenType::RightParen }, any, true); if(any && !post) return false; if(!c.sc.match(c, Scanner::TokenType::RightParen, false)) return false; if(post) f->post = post.release(); NodePtr b = statementBlock(c, true); if(!b) return false; f->block = b.release(); return true;}In the new version, this becomes:

void forStatement(Context &c, BlockNode *block, bool get){ c.scanner.match(Scanner::TokenType::LeftParen, get); NodePtr init = optional(c, conditionalExpr, { Scanner::TokenType::SemiColon }, true); ForNode *f = new ForNode(c); block->nodes.push_back(f); f->init = init; f->cond = optional(c, expr, { Scanner::TokenType::SemiColon }, true); f->post = optional(c, expr, { Scanner::TokenType::RightParen }, true); f->block = statementBlock(c, true);}I think that is enough for one entry anyway. Thanks for stopping by.

Previous Entry Implementing Properties
Next Entry Hey
1 likes 1 comments

Comments

Embassy of Time

What is Om designed to achieve? I can't find a red thread here, but you seem to have some clear idea of what it's for?

April 07, 2017 11:26 AM
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Profile
Author
Advertisement
Advertisement