Skip to content

Commit 5190099

Browse files
Add Invoke op to optimize method calls; require explicit this
1 parent 5b91e5d commit 5190099

File tree

8 files changed

+165
-62
lines changed

8 files changed

+165
-62
lines changed

compiler/bytecode.oc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ enum OpCode {
3838
SetMember
3939
GetMember
4040
Method
41+
Invoke
4142
}
4243

4344
struct DebugLocRun {
@@ -173,7 +174,7 @@ def Chunk::disassemble_inst(&this, off: u32, found_chunks: &Vector<&Chunk> = nul
173174
LessThan => println("LessThan")
174175
GreaterThan => println("GreaterThan")
175176
Equal => println("Equal")
176-
SetMember | GetMember | Class | GetGlobal | SetGlobal | Method => {
177+
SetMember | GetMember | Class | GetGlobal | SetGlobal | Method | Invoke => {
177178
let name = .read_literal(&off).as_obj() as &String
178179
println(f"{op} {name.data}")
179180
}

compiler/compiler.oc

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -267,15 +267,34 @@ def Compiler::compile_expression(&this, node: &AST) {
267267
Identifier => .compile_variable(node.u.ident.name, node.span)
268268
Call => {
269269
let callee = node.u.call.callee
270+
let args = node.u.call.args
270271

271-
.compile_expression(callee)
272+
match callee.type {
273+
// Special case for `a.b()`, which generates
274+
// GET a, ... INVOKE b
275+
// instead of
276+
// GET a, GET_MEMBER b ... CALL
277+
Member => {
278+
let member = callee.u.member
279+
.compile_expression(member.lhs)
280+
281+
for arg in node.u.call.args.iter() {
282+
.compile_expression(arg.expr)
283+
}
272284

273-
let args = node.u.call.args
274-
for arg in args.iter() {
275-
.compile_expression(arg.expr)
285+
let name = .make_str(member.rhs_name)
286+
.chunk.push_with_literal(.vm, Invoke, name, node.span)
287+
.chunk.push_u8(args.size as u8, node.span)
288+
}
289+
else => {
290+
.compile_expression(callee)
291+
for arg in node.u.call.args.iter() {
292+
.compile_expression(arg.expr)
293+
}
294+
.chunk.push_with_arg_u8(Call, args.size as u8, node.span)
295+
}
276296
}
277297

278-
.chunk.push_with_arg_u8(Call, args.size as u8, node.span)
279298
}
280299
Member => {
281300
let member = node.u.member
@@ -499,6 +518,7 @@ def Compiler::compile_statement(&this, node: &AST) {
499518
}
500519
// TODO: Disallow return in constructor
501520
// TODO: Disallow return in global scope, maybe?
521+
// TODO: If ArrowReturn in constructor, treat as statement
502522
Return | ArrowReturn => {
503523
if node.u.child? {
504524
.compile_expression(node.u.child)

compiler/parser.oc

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1197,6 +1197,7 @@ def Parser::parse_function(&this, type: FunctionType): &AST {
11971197
let start = .consume(Def)
11981198

11991199
let name = .consume(Identifier)
1200+
let expect_this_arg = (type == Method)
12001201

12011202
if type == Method and name.text == "init" {
12021203
type = Constructor
@@ -1210,18 +1211,27 @@ def Parser::parse_function(&this, type: FunctionType): &AST {
12101211
.add_doc_comment(func.sym, start)
12111212

12121213
.consume(OpenParen)
1214+
let found_this_arg = false
12131215
while not .token_is(CloseParen) {
12141216
let var_name = .consume(Identifier)
1215-
1216-
let var = Variable::new()
1217-
var.sym = Symbol::from_variable(var_name.text, var, var_name.span)
1218-
func.params.push(var)
1219-
.add_doc_comment(var.sym, var_name)
1217+
if func.params.size == 0 and var_name.text == "this" {
1218+
// Don't actually need to add `this` to the params list, since
1219+
// we special-case this when binding methods.
1220+
found_this_arg = true
1221+
} else {
1222+
let var = Variable::new()
1223+
var.sym = Symbol::from_variable(var_name.text, var, var_name.span)
1224+
func.params.push(var)
1225+
.add_doc_comment(var.sym, var_name)
1226+
}
12201227

12211228
if not .token_is(CloseParen) {
12221229
.consume(Comma)
12231230
}
12241231
}
1232+
if expect_this_arg and not found_this_arg {
1233+
.error(Error::new(func.sym.span, "Method must have a `this` as first argument"))
1234+
}
12251235
let end_span = .consume(CloseParen).span
12261236

12271237
.cur_func = func

compiler/vm/mod.oc

Lines changed: 79 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,12 @@ def VM::add(&this, a: Value, b: Value): Value => if {
166166
// allocate the new one (which can trigger a GC collection)
167167
.tmp.push(a)
168168
.tmp.push(b)
169-
let res = .take_string(`{a_str.data}{b_str.data}`, a_str.len + b_str.len)
169+
let s = mem::alloc<char>(a_str.len + b_str.len + 1)
170+
import std::libc::memcpy
171+
memcpy(s , a_str.data, a_str.len)
172+
memcpy(s + a_str.len, b_str.data, b_str.len)
173+
s[a_str.len + b_str.len] = '\0'
174+
let res = .take_string(s, a_str.len + b_str.len)
170175
.tmp.pop() // b
171176
.tmp.pop() // a
172177
yield res
@@ -407,7 +412,7 @@ def VM::bind_method(&this, instance: Value, func: &Function) {
407412
.stack.push(Value::Object(&bound.obj))
408413
}
409414

410-
def VM::call(&this, arity: u32, func: &Function) {
415+
def VM::call_function(&this, arity: u32, func: &Function) {
411416
if func.code.arity as u32 != arity {
412417
.error(f"Function {func.code.name.data} expected {func.code.arity} arguments, got {arity}")
413418
}
@@ -417,13 +422,58 @@ def VM::call(&this, arity: u32, func: &Function) {
417422
.tmp.pop()
418423
}
419424

425+
def VM::call_value(&this, arity: u32, val: Value) {
426+
if not val.is_obj() {
427+
.error(f"Can't call object of type {val.type_str()}")
428+
}
429+
430+
match val.as_obj().type {
431+
Function => .call_function(arity, val.as_function())
432+
NativeFunction => {
433+
let func = val.as_native_function()
434+
let res = func.func(this, arity, &.stack.data[.stack.size - arity])
435+
.stack.size -= arity as u32 + 1 // Pop the arguments and the function
436+
.stack.push(res)
437+
}
438+
Class => {
439+
let class = val.as_class()
440+
let instance = gc::allocate_object<Instance>(ObjectType::Instance, this)
441+
let val = Value::Object(&instance.obj)
442+
instance.fields = null
443+
444+
// instance.init allocates memory, so we need to push this on the stack
445+
// so the GC can find it if we collect
446+
.stack.push(val)
447+
instance.init(class)
448+
.stack.pop()
449+
450+
.stack.data[.stack.size - arity - 1] = val
451+
452+
let init_it = class.methods.get_item(.init_string)
453+
if init_it? {
454+
.call_function(arity, init_it.value)
455+
456+
} else if arity != 0 {
457+
.error(f"Can't provide arguments to class without init()")
458+
}
459+
}
460+
Method => {
461+
let method = val.as_method()
462+
.stack.data[.stack.size - arity - 1] = method.this_val
463+
.call_function(arity, method.func)
464+
}
465+
FunctionCode => .error("Function should have been a func")
466+
else => .error(f"Can't call object of type {val.type_str()}")
467+
}
468+
}
469+
420470
def VM::main_loop(&this): i32 {
421471
while true {
422472
// FIXME: For `tests/native_fn.td`, having this line here
423473
// causes perf to go from 1.2s -> 1.6s, _even if_
424474
// the debug == false. Saving it into a local variable
425475
// from the global one doesn't help either.
426-
if debug then .debug_dump_inst(print_stack: true)
476+
// if debug then .debug_dump_inst(print_stack: true)
427477

428478
.cur_inst_ip = .ip
429479
let op = .read_u8() as OpCode
@@ -518,49 +568,7 @@ def VM::main_loop(&this): i32 {
518568
Call => {
519569
let arity = .read_u8() as u32
520570
let val = .stack.back(arity as u32)
521-
522-
if not val.is_obj() {
523-
.error(f"Can't call object of type {val.type_str()}")
524-
}
525-
526-
match val.as_obj().type {
527-
Function => .call(arity, val.as_function())
528-
NativeFunction => {
529-
let func = val.as_native_function()
530-
let res = func.func(this, arity, &.stack.data[.stack.size - arity])
531-
.stack.size -= arity as u32 + 1 // Pop the arguments and the function
532-
.stack.push(res)
533-
}
534-
Class => {
535-
let class = val.as_class()
536-
let instance = gc::allocate_object<Instance>(ObjectType::Instance, this)
537-
let val = Value::Object(&instance.obj)
538-
instance.fields = null
539-
540-
// instance.init allocates memory, so we need to push this on the stack
541-
// so the GC can find it if we collect
542-
.stack.push(val)
543-
instance.init(class)
544-
.stack.pop()
545-
546-
.stack.data[.stack.size - arity - 1] = val
547-
548-
let init_it = class.methods.get_item(.init_string)
549-
if init_it? {
550-
.call(arity, init_it.value)
551-
552-
} else if arity != 0 {
553-
.error(f"Can't provide arguments to class without init()")
554-
}
555-
}
556-
Method => {
557-
let method = val.as_method()
558-
.stack.data[.stack.size - arity - 1] = method.this_val
559-
.call(arity, method.func)
560-
}
561-
FunctionCode => .error("Function should have been a func")
562-
else => .error(f"Can't call object of type {val.type_str()}")
563-
}
571+
.call_value(arity, val)
564572
}
565573
GetGlobal => {
566574
let name = .read_literal().as_string()
@@ -617,6 +625,31 @@ def VM::main_loop(&this): i32 {
617625

618626
.error(f"Member {name.data} not found")
619627
}
628+
Invoke => {
629+
let name = .read_literal().as_string()
630+
let arity = .read_u8() as u32
631+
632+
let obj = .stack.back(arity)
633+
if not obj.is_instance() {
634+
.error(f"Can't access a field/member on object of type {obj.type_str()}")
635+
}
636+
let instance = obj.as_instance()
637+
let func = instance.class.methods.get(name, null)
638+
if func? {
639+
// NOTE: the instance is already on the stack in the
640+
// correct position to bind `this`, so we don't
641+
// need to do anything here.
642+
.call_function(arity, func)
643+
644+
} else {
645+
let it = instance.fields.get_item(name)
646+
if not it? {
647+
.error(f"Could not find field/method with name {name}")
648+
}
649+
.call_value(arity, it.value)
650+
}
651+
652+
}
620653
Null => .stack.push(Value::Null())
621654
True => .stack.push(Value::True())
622655
False => .stack.push(Value::False())

tests/class_init.td

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
/// out: Person: John 30 JohnJohnJohn
22

33
class Person {
4-
def init(name, age) {
4+
def init(this, name, age) {
55
.name = name
66
.age = age
77
}
88

9-
def print() {
9+
def print(this) {
1010
print("Person: " + .name, .age)
1111
}
12-
def bar() => .name + .name + .name
12+
def bar(this) => .name + .name + .name
1313
}
1414

1515

tests/class_methods.td

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
/// out: <instance Foo> 5 8
22

33
class Foo {
4-
def bar() => .x = 5
5-
def test(a, b) => .x + a + b
4+
def bar(this) => .x = 5
5+
def test(this, a, b) => .x + a + b
66
}
77

88
let f = Foo()

tests/field_func_call.td

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/// out: not a method
2+
3+
// Makes sure we haven't broken this case with the
4+
// INVOKE opcode.
5+
6+
class Oops {
7+
def init(this) {
8+
def f() {
9+
print("not a method")
10+
}
11+
12+
this.field = f
13+
}
14+
}
15+
16+
let oops = Oops()
17+
oops.field()

tests/method_bench.td

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/// out: OK
2+
3+
class Foo {
4+
def init(this) {
5+
.name = "foo"
6+
}
7+
def bar(this) => .name + "hello"
8+
}
9+
10+
let foo = Foo()
11+
12+
let start = clock()
13+
14+
for let i = 0; i < 10; i = i + 1 {
15+
for let i = 0; i < 10000; i = i + 1 {
16+
foo.bar()
17+
}
18+
}
19+
20+
let end = clock()
21+
print("Took", end-start, "seconds")
22+
print("OK")

0 commit comments

Comments
 (0)