On Thu, Mar 03, 2011 at 09:03:39AM +0000, Andrew S. Townley wrote:
> Lazy or not, I think it's better to fail silently now and then try and build
> better checks into the clownfish compiler or some kind of clownlint tool
> later than imposing exception handling overhead on potentially every method
> call (unless I'm misunderstanding the impact of option B).
The host language's OO hierarchy is only walked once per subclass. Once Lucy
creates a VTable singleton and stores it away, that VTable will not be
modified. Thus, there is no extra overhead imposed on every method call
within the Lucy core -- but we don't support all the features of dynamic
languages.
Generally speaking, the VTable singleton for a host-defined subclass is
created the first time a constructor for that subclass is called[1]. It is at
this moment that we would attempt to detect illegal overriding of "final"
methods. Here's the code from from VTable_singleton() in
core/Lucy/Object/VTable.c which enables host method overrides, modified to
enforce "final" methods:
// Allow host methods to override.
novel_host_methods = VTable_novel_host_methods(subclass_name);
num_novel = VA_Get_Size(novel_host_methods);
if (num_novel) {
Hash *meths = Hash_new(num_novel);
uint32_t i;
CharBuf *scrunched = CB_new(0);
ZombieCharBuf *callback_name = ZCB_BLANK();
for (i = 0; i < num_novel; i++) {
CharBuf *meth = (CharBuf*)VA_fetch(novel_host_methods, i);
S_scrunch_charbuf(meth, scrunched);
Hash_Store(meths, (Obj*)scrunched, INCREF(&EMPTY));
}
cfish_Callback **callbacks
= (cfish_Callback**)singleton->callbacks;
for (i = 0; callbacks[i] != NULL; i++) {
cfish_Callback *const callback = callbacks[i];
ZCB_Assign_Str(callback_name, callback->name,
callback->name_len);
S_scrunch_charbuf((CharBuf*)callback_name, scrunched);
if (Hash_Fetch(meths, (Obj*)scrunched)) {
+ if (callback->method_is_final) {
+ THROW(ERR,
+ "Can't override final method '%o' in class '%o'",
+ meth, class_name);
+ }
VTable_Override(singleton, callback->func,
callback->offset);
}
}
DECREF(scrunched);
DECREF(meths);
}
DECREF(novel_host_methods);
Once a VTable is created, if you subsequently attempt to modify behavior using
dynamic features such as Ruby's singleton methods, you will get inconsistent
results. The behavior will be visible from the host language, but it might
not be visible from inside the Lucy core. You only get one shot to tell Lucy
that it needs to call back into the host. If a method was not overridden at
the moment the VTable was created, Lucy won't notice if it gets overridden
later.
class MyQueryParser < Lucy::Search::QueryParser
end
query_parser = MyQueryParser.new(schema)
def query_parser.expand_leaf
puts "This code will not be called from within the Lucy core."
end
Clownfish's design is a compromise which lets us support a simple class-based
single-inheritance OO model without sacrificing speed. Method dispatch in
Clownfish uses a vtable-based double dereference mechanism[2] -- typical for
C++ or Java. It's less flexible than the dispatch techniques used by dynamic
languages like Ruby, Python and Perl, but until the host callback mechanism
gets invoked, it's much, much faster.
Marvin Humphrey
[1] All parent VTables must be created first, so if the VTable for a host
subclass two generations removed from its Lucy ancestor is needed before
its parent, the parent VTable will be initialized first as a side effect.
[2] http://en.wikipedia.org/wiki/Virtual_method_table