Progress Report #10 – Filling in Gaps

New changes this week in Copper fill in some of the gaps that show up when trying to use Copper for real-world projects. The first of these is the indication of the support of multiple interfaces, which solves the dilemma of distinguishing between polymorphic objects turned into Copper objects. The second is the addition of a system function that enables the sharing of user-created function bodies.

In other notes, the string_map extension received a bug fix for not correctly returning when “exit” was called in the middle of the callback function it was given.

Supporting Multiple Interfaces

One of the big problems in C++ is inheritance. The creator of C++, Bjarne Stroustrup, noted that class method calls are ambiguous when a class inherits the same method from two different classes. Consequently, this is one of the reasons he is a proponent of the universal call syntax. There are problems with this, but in relation to Copper, it comes in a slightly different flavor than the context in which Stroustrup made his argument.

For the Copper virtual machine, classes inheriting Object can be used by the virtual machine like any other object, and like other objects, they must implement certain virtual methods and set their own object type. Internally, Copper doesn’t recognize multiple types; there is only one type: “object-function” or “function-object”. The other types that get tossed around are abstract and only have meaning within calls to system functions or foreign functions. This means Copper as a language is incredibly flexible. Unfortunately, it’s can be difficult to take advantage of this flexibility on the C++ side, at least in a way that’s safe.

Some classes in C++ can be the descendants of multiple classes. Ideally, these classes form a nice chain like “extends” in Java, but even in this case, we encounter a dilemma created by having Copper objects return a single type value. In my experience, I’ve never once encountered a programming language that allowed for an object to have more than one type. Its odd. But in the context of Copper, it makes perfect sense.

Consider the following scenario: There is a class called “StringObject” and a descendant class called “UnicodeStringObject”. StringObject inherits Cu::Object (and thus is a Copper object), and thus, so does UnicodeStringObject. The question arises, “How are these classes supposed to be distinguished?”

Initially, I considered distinguishing between descendant classes using a subType value, which is what numbers were doing. However, this required an extra type check and would, in more complex scenarios, lead to subSubType values or complicated casting rules known only to the developer of those objects and used only in specific circumstances. It would also encourage bloating a single interface to “prepare for the future”.

The solution to the problem turned out to be simple and elegant, fitting in perfectly with the existing system with little modification. Moreover, it did not involve templates (which don’t work for solving a run-time problem) nor function overrides (which don’t work unless you already know what to cast to). Instead, the solution was to add another method to Cu::Object, supportsInterface( ObjectType ).

This particular method returns a boolean value and is to be implemented by all object classes to indicate the type of object to which they can be casted. Recall, the Copper virtual machine uses an enumeration to distinguish between object types. It’s not perfectly safe because it expects that custom object type values will not overlap with the built-in type values, but it’s relatively safe. Consequently, it is treated, in a sense, as the word from heaven as far as the virtual machine is concerned.

Assuming the ObjectType value is reliable, then it can be used for every interface to which a descendant of Cu::Object can be casted.

In the previous example, StringObject might be assigned the unique custom object type value of “StringType”, whereas for UnicodeStringObject, the value might be “UnicodeType”. StringObject::supportsInterface() would return true if the argument it is given is of the value “StringType”, whereas UnicodeStringObject::supportsInterface() would return true for a given argument value of either “StringType” or “UnicodeType”. Thus, a foreign function call() method using UnicodeStringObject might begin as follows:

ForeignFunc::Result  MyForeignFunc( Cu::FFIServices&  ffi ) {
UnicodeStringObject*  obj;
if ( ffi.demandArgType(0, static_cast<Cu::ObjectType::Value>( UnicodeType )) ) {
    obj = (UnicodeStringObject*)(ffi.arg(0));
    // do stuff with obj
}
//...
}

Simple, no?

Within the foreign function interface, FFIServices, the changes were so easy to implement that only a couple extensions needed to be changed, and in that regard, they were only changed for the removal of the sub-type system.

Sharing Function Bodies

Some months ago, I fixed a bug that caused the assignment of non-function objects to a variable to NOT override the existing function. The language specification said otherwise, and the spec was correct because technically, the assignment of a non-function object to a variable was meant to be a shortcut for writing out a function body that returned that specific object.

Ironically, fixing this bug created a need. Consider the following situation: Suppose you wanted to create a function that represented an enumeration, having members with unique values. You might do something like the following:


enum = [parent, values]{
	i = 0, l = length(values:)
	loop {
		if ( gte(i: l:) ) { stop }
		name = item_at(member(values: i:))
		if ( are_string(name:) ) {
			m ~ member(parent, name:)
			m = i: # PROBLEM: This changes the entire function, dropping the reference. #
		}
	}
}

enum(Fruit list(
	"Apple"
	"Banana"
	"Pear"
))

print(Fruit.Apple:)

You may note that Copper has no way of setting the result of a function. Whenever you assign to a variable, the entire function is replaced. There are certain system functions for incrementing values returned by functions and doing other nifty things, but up until now, none of them have replaced the value.

Of course, replacing the shortcut return from a function is against the language specification and would create an apparent difference between functions with bodies returning a value and functions with no body returning a value. This, compounded by the fact that you might want to replace the entire body anyways, has led to the implementation of a new system function that does just that: share_body().

share_body() accepts a function and shares its body with all of the other functions that are given as arguments to this system function. The system function will replace both the body and the direct result cache, thereby making it irrelevant how values are stored in the body. shared_body() will not accept anything other than functions, and this should make sense now that I’ve repeatedly mentioned the language standard.

Example usage of share_body():


a = { print("hello") }
share_body(a b)
b: # Prints "hello" #

Your solution to the enumeration problem thus becomes:


enum = [parent, values]{
	i = 0, l = length(values:)
	loop {
		if ( gte(i: l:) ) { stop }
		name = item_at(member(values: i:))
		if ( are_string(name:) ) {
			m ~ member(parent, name:)
			share_body(i m)
		}
	}
}

Note that “i” is not called here because the return of “i” is a number, not a function body.

Concluding Remarks

The changes made are not necessarily perfect solutions, but they did fit very nicely within the existing framework.

I should note that, when I forgot to implement supportsInterface(), I got a segfault (*gasp*)… in my code for Copper Bridge, despite the fact that Object::supportsInterface() returns false and do nothing. (Obviously, the fault is with Copper Bridge code in this case.) While this doesn’t mean anything is wrong with the Copper virtual machine per se, it does hint at the ease in which things might go wrong when working with this interface. As a general rule of thumb, be sure to implement the virtual functions Object::copy(), Object::typename(), and Object::supportsInterface(). Of these three, Object::typename() is the least important, but it is used for the system function are_same_type() to provide the most accurate type matching.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s