Skip to content

Commit

Permalink
Implement method_missing. Required a couple ugly tweaks in the byteco…
Browse files Browse the repository at this point in the history
…de execution. I'm starting to hate the design for method lookup -> call... Need something better!

LOOKUP instruction will now store the call site on the C stack, not in the regs. I'm not sure if this is bad or not, but since LOOKUP/CALL instructions are always next to each other there's no way this could not work, right, riiiight?
Also, I found out that a CallSite was created on heap (and never freed) each time a polymorphic call is made, this is an important bug and will require rethinking the method lookup structure in the VM.
RE method_missing, I got it to work by pading the arguments w/ an empty slot first.

Normal call:
 reg   value
 ===========
 0     receiver
 1     nil
 2     arg macournoyer#1
 3     arg macournoyer#2

Method missing call:
 reg   value
 ===========
 0     receiver
 1     method name (replace by CALL instruction)
 2     arg macournoyer#1
 3     arg macournoyer#2

You see that most of the time, reg[1] is not used, that's not a big deal since we got plenty of registers left. The compiler needs to set the maximum number of register to be used so we have to set it to the max possible (+1, max number of args).

That is all...

w/ love,
m-a
  • Loading branch information
macournoyer committed May 3, 2009
1 parent 74d7a3f commit 4065c45
Show file tree
Hide file tree
Showing 8 changed files with 68 additions and 31 deletions.
2 changes: 1 addition & 1 deletion TODO
Expand Up @@ -6,11 +6,11 @@
* Kernel#require
* Fix {...} for blocks
* namespace constants in Module (A::B)
* method_missing
* break
* next

@Later
* Fix CallSite leak (see TODO in TrVM_lookup)
* begin...rescue...ensure
* def...rescue...ensure
* Proc
Expand Down
6 changes: 3 additions & 3 deletions test/method_cache.rb
@@ -1,11 +1,11 @@
def twice(x)
puts x + x
puts x.to_s
end

# This caches the lookup for `Fixnum + Fixnum`
twice 1
# => 2
# => 1

# This will skip the previous cache
twice "yo"
# => yoyo
# => yo
15 changes: 15 additions & 0 deletions test/method_missing.rb
@@ -0,0 +1,15 @@
def method_missing(name, *args)
puts "called method_missing"
puts name
end

ohaie!
# => called method_missing
# => ohaie!

def ohaie!
puts "ok"
end

ohaie!
# => ok
3 changes: 0 additions & 3 deletions vm/config.h
Expand Up @@ -19,9 +19,6 @@

#endif

/* Portable optimizations */
#define TR_CALL_SITE 1 /* enable caching method lookup at the call site */

/* Various limits */
#define TR_MAX_FRAMES 255
#define MAX_INT (INT_MAX-2) /* maximum value of an int (-2 for safety) */
Expand Down
18 changes: 12 additions & 6 deletions vm/object.c
Expand Up @@ -23,17 +23,22 @@ OBJ TrObject_method(VM, OBJ self, OBJ name) {
return TrModule_instance_method(vm, TR_CLASS(self), name);
}

OBJ TrObject_lookup(VM, OBJ self, OBJ name) {
OBJ method = TrModule_instance_method(vm, TR_CLASS(self), name);
if (!method) tr_raise(NoMethodError, "Method not found: `%s'", TR_STR_PTR(name));
return method;
OBJ TrObject_method_missing(VM, OBJ self, int argc, OBJ argv[]) {
UNUSED(self);
assert(argc > 0);
tr_raise(NoMethodError, "Method not found: `%s'", TR_STR_PTR(argv[0]));
}

OBJ TrObject_send(VM, OBJ self, int argc, OBJ argv[]) {
if (unlikely(argc == 0))
tr_raise(ArgumentError, "wrong number of arguments (%d for 1)", argc);
OBJ method = TrObject_lookup(vm, self, argv[0]);
return TrMethod_call(vm, method, self, argc-1, argv+1, 0, 0);
OBJ method = TrObject_method(vm, self, argv[0]);
if (unlikely(method == TR_NIL)) {
method = TrObject_method(vm, self, tr_intern("method_missing"));
return TrMethod_call(vm, method, self, argc, argv, 0, 0);
} else {
return TrMethod_call(vm, method, self, argc-1, argv+1, 0, 0);
}
}

/* TODO respect namespace */
Expand Down Expand Up @@ -96,6 +101,7 @@ void TrObject_init(VM) {
OBJ c = TR_CORE_CLASS(Object);
tr_def(c, "class", TrObject_class, 0);
tr_def(c, "method", TrObject_method, 1);
tr_def(c, "method_missing", TrObject_method_missing, -1);
tr_def(c, "send", TrObject_send, -1);
tr_def(c, "object_id", TrObject_object_id, 0);
tr_def(c, "instance_eval", TrObject_instance_eval, 1);
Expand Down
4 changes: 2 additions & 2 deletions vm/opcode.h
Expand Up @@ -85,8 +85,8 @@ enum TrInstCode {
TR_OP_NIL, /* A R[A] = nil */
TR_OP_SELF, /* A put self in R[A] */
TR_OP_LOOKUP, /* A Bx R[A+1] = lookup method K[Bx] on R[A] and store */
TR_OP_CACHE, /* A B C if sites[C] matches R[A].type, jmp +B and R[A+1] = sites[C].method */
TR_OP_CALL, /* A B C call method R[A+1] on R[A] with B>>1 args starting at R[A+2],
TR_OP_CACHE, /* A B C if sites[C] matches R[A].type, jmp +B and next call will be on sites[C] */
TR_OP_CALL, /* A B C call last looked up method on R[A] with B>>1 args starting at R[A+2],
if B & 1, splat last arg,
if C > 0 pass block[C-1] */
TR_OP_JMP, /* sBx jump sBx instructions */
Expand Down
3 changes: 2 additions & 1 deletion vm/tr.h
Expand Up @@ -175,6 +175,8 @@ struct TrFrame;
typedef struct {
OBJ class;
OBJ method;
OBJ message;
int method_missing:1;
size_t miss;
} TrCallSite;

Expand Down Expand Up @@ -359,7 +361,6 @@ TrClosure *TrClosure_new(VM, TrBlock *b, OBJ self, OBJ class, TrClosure *parent)
OBJ TrObject_alloc(VM, OBJ class);
int TrObject_type(VM, OBJ obj);
OBJ TrObject_method(VM, OBJ self, OBJ name);
OBJ TrObject_lookup(VM, OBJ self, OBJ name);
OBJ TrObject_send(VM, OBJ self, int argc, OBJ argv[]);
OBJ TrObject_const_set(VM, OBJ self, OBJ name, OBJ value);
OBJ TrObject_const_get(VM, OBJ self, OBJ name);
Expand Down
48 changes: 33 additions & 15 deletions vm/vm.c
Expand Up @@ -12,25 +12,37 @@
static OBJ TrVM_interpret(VM, TrFrame *f, TrBlock *b, int start, int argc, OBJ argv[], TrClosure *closure);

static OBJ TrVM_lookup(VM, TrBlock *b, OBJ receiver, OBJ msg, TrInst *ip) {
OBJ method = TrObject_lookup(vm, receiver, msg);
OBJ method = TrObject_method(vm, receiver, msg);
RETHROW(method);

#if TR_CALL_SITE
TrInst *boing = (ip-1);
/* TODO do not prealloc TrCallSite here, every one is a memory leak and a new
one is created on polymorphic calls. */
TrCallSite *s = (kv_pushp(TrCallSite, b->sites));
s->class = TR_CLASS(receiver);
s->method = method;
s->miss = 0;
s->method = method;
s->message = msg;
if (unlikely(method == TR_NIL)) {
s->method = TrObject_method(vm, receiver, tr_intern("method_missing"));
s->method_missing = 1;
}

/* Implement Monomorphic method cache by replacing the previous instruction (BOING)
w/ CACHE that uses the CallSite to find the method instead of doing a full lookup. */
SET_OPCODE(*boing, TR_OP_CACHE);
SETARG_A(*boing, GETARG_A(*ip)); /* receiver register */
SETARG_B(*boing, 1); /* jmp */
SETARG_C(*boing, kv_size(b->sites)-1); /* CallSite index */
#endif
if (GET_OPCODE(*boing) == TR_OP_CACHE) {
/* Existing call site */
/* TODO maybe take existing call site hit miss into consideration to replace it with this one.
For now, we just don't replace it, the first one is always the cached one. */
} else {
/* New call site, we cache it fo shizzly! */
SET_OPCODE(*boing, TR_OP_CACHE);
SETARG_A(*boing, GETARG_A(*ip)); /* receiver register */
SETARG_B(*boing, 1); /* jmp */
SETARG_C(*boing, kv_size(b->sites)-1); /* CallSite index */
}

return method;
return (OBJ)s;
}

static OBJ TrVM_defclass(VM, OBJ name, TrBlock *b, int module, OBJ super) {
Expand Down Expand Up @@ -140,7 +152,7 @@ static inline OBJ TrVM_yield(VM, TrFrame *f, int argc, OBJ argv[]) {
#define RETURN(V) \
/* TODO GC release everything on the stack before returning */ \
return (V)
#define VM_RETHROW(R) if (unlikely((R) == TR_UNDEF)) RETURN(TR_UNDEF)
#define VM_RETHROW(R) if (unlikely((OBJ)(R) == TR_UNDEF)) RETURN(TR_UNDEF)

/* Interprets the code in b->code.
Returns TR_UNDEF on error. */
Expand All @@ -163,6 +175,7 @@ static OBJ TrVM_interpret(VM, register TrFrame *f, TrBlock *b, int start, int ar
f->line = b->line;
f->filename = b->filename;
TrUpval *upvals = closure ? closure->upvals : 0;
TrCallSite *call = 0;

/* transfer locals */
if (argc > 0) {
Expand Down Expand Up @@ -206,12 +219,12 @@ static OBJ TrVM_interpret(VM, register TrFrame *f, TrBlock *b, int start, int ar
OP(GETGLOBAL): R[A] = TR_KH_GET(vm->globals, k[Bx]); DISPATCH;

/* method calling */
OP(LOOKUP): VM_RETHROW(R[A+1] = TrVM_lookup(vm, b, R[A], k[Bx], ip)); DISPATCH;
OP(LOOKUP): VM_RETHROW(call = (TrCallSite*)TrVM_lookup(vm, b, R[A], k[Bx], ip)); DISPATCH;
OP(CACHE):
/* TODO how to expire cache? */
assert(&SITE[C] && "Method cached but no CallSite found");
if (likely(SITE[C].class == TR_CLASS((R[A])))) {
R[A+1] = SITE[C].method;
call = &SITE[C];
ip += B;
} else {
/* TODO invalidate CallSite if too much miss. */
Expand Down Expand Up @@ -240,14 +253,19 @@ static OBJ TrVM_interpret(VM, register TrFrame *f, TrBlock *b, int start, int ar
}
}
}
int argc = GETARG_B(ci) >> 1;
OBJ *argv = &R[GETARG_A(ci)+2];
if (unlikely(call->method_missing)) {
argc++;
*(--argv) = call->message;
}
OBJ ret = TrMethod_call(vm,
R[GETARG_A(ci)+1], /* method */
call->method,
R[GETARG_A(ci)], /* receiver */
GETARG_B(ci) >> 1, &R[GETARG_A(ci)+2], /* args */
argc, argv,
GETARG_B(ci) & 1, /* splat */
cl /* closure */
);

/* Handle throw if some.
A "throw" is done by returning TR_UNDEF to exit a current call frame (TrFrame)
until one handle it by returning are real value or continuing execution.
Expand Down

0 comments on commit 4065c45

Please sign in to comment.