Skip to content

Commit

Permalink
Add Invoke op to optimize method calls; require explicit this
Browse files Browse the repository at this point in the history
  • Loading branch information
mustafaquraish committed Apr 28, 2024
1 parent 5b91e5d commit 5190099
Show file tree
Hide file tree
Showing 8 changed files with 165 additions and 62 deletions.
3 changes: 2 additions & 1 deletion compiler/bytecode.oc
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ enum OpCode {
SetMember
GetMember
Method
Invoke
}

struct DebugLocRun {
Expand Down Expand Up @@ -173,7 +174,7 @@ def Chunk::disassemble_inst(&this, off: u32, found_chunks: &Vector<&Chunk> = nul
LessThan => println("LessThan")
GreaterThan => println("GreaterThan")
Equal => println("Equal")
SetMember | GetMember | Class | GetGlobal | SetGlobal | Method => {
SetMember | GetMember | Class | GetGlobal | SetGlobal | Method | Invoke => {
let name = .read_literal(&off).as_obj() as &String
println(f"{op} {name.data}")
}
Expand Down
30 changes: 25 additions & 5 deletions compiler/compiler.oc
Original file line number Diff line number Diff line change
Expand Up @@ -267,15 +267,34 @@ def Compiler::compile_expression(&this, node: &AST) {
Identifier => .compile_variable(node.u.ident.name, node.span)
Call => {
let callee = node.u.call.callee
let args = node.u.call.args

.compile_expression(callee)
match callee.type {
// Special case for `a.b()`, which generates
// GET a, ... INVOKE b
// instead of
// GET a, GET_MEMBER b ... CALL
Member => {
let member = callee.u.member
.compile_expression(member.lhs)

for arg in node.u.call.args.iter() {
.compile_expression(arg.expr)
}

let args = node.u.call.args
for arg in args.iter() {
.compile_expression(arg.expr)
let name = .make_str(member.rhs_name)
.chunk.push_with_literal(.vm, Invoke, name, node.span)
.chunk.push_u8(args.size as u8, node.span)
}
else => {
.compile_expression(callee)
for arg in node.u.call.args.iter() {
.compile_expression(arg.expr)
}
.chunk.push_with_arg_u8(Call, args.size as u8, node.span)
}
}

.chunk.push_with_arg_u8(Call, args.size as u8, node.span)
}
Member => {
let member = node.u.member
Expand Down Expand Up @@ -499,6 +518,7 @@ def Compiler::compile_statement(&this, node: &AST) {
}
// TODO: Disallow return in constructor
// TODO: Disallow return in global scope, maybe?
// TODO: If ArrowReturn in constructor, treat as statement
Return | ArrowReturn => {
if node.u.child? {
.compile_expression(node.u.child)
Expand Down
20 changes: 15 additions & 5 deletions compiler/parser.oc
Original file line number Diff line number Diff line change
Expand Up @@ -1197,6 +1197,7 @@ def Parser::parse_function(&this, type: FunctionType): &AST {
let start = .consume(Def)

let name = .consume(Identifier)
let expect_this_arg = (type == Method)

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

.consume(OpenParen)
let found_this_arg = false
while not .token_is(CloseParen) {
let var_name = .consume(Identifier)

let var = Variable::new()
var.sym = Symbol::from_variable(var_name.text, var, var_name.span)
func.params.push(var)
.add_doc_comment(var.sym, var_name)
if func.params.size == 0 and var_name.text == "this" {
// Don't actually need to add `this` to the params list, since
// we special-case this when binding methods.
found_this_arg = true
} else {
let var = Variable::new()
var.sym = Symbol::from_variable(var_name.text, var, var_name.span)
func.params.push(var)
.add_doc_comment(var.sym, var_name)
}

if not .token_is(CloseParen) {
.consume(Comma)
}
}
if expect_this_arg and not found_this_arg {
.error(Error::new(func.sym.span, "Method must have a `this` as first argument"))
}
let end_span = .consume(CloseParen).span

.cur_func = func
Expand Down
125 changes: 79 additions & 46 deletions compiler/vm/mod.oc
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,12 @@ def VM::add(&this, a: Value, b: Value): Value => if {
// allocate the new one (which can trigger a GC collection)
.tmp.push(a)
.tmp.push(b)
let res = .take_string(`{a_str.data}{b_str.data}`, a_str.len + b_str.len)
let s = mem::alloc<char>(a_str.len + b_str.len + 1)
import std::libc::memcpy
memcpy(s , a_str.data, a_str.len)
memcpy(s + a_str.len, b_str.data, b_str.len)
s[a_str.len + b_str.len] = '\0'
let res = .take_string(s, a_str.len + b_str.len)
.tmp.pop() // b
.tmp.pop() // a
yield res
Expand Down Expand Up @@ -407,7 +412,7 @@ def VM::bind_method(&this, instance: Value, func: &Function) {
.stack.push(Value::Object(&bound.obj))
}

def VM::call(&this, arity: u32, func: &Function) {
def VM::call_function(&this, arity: u32, func: &Function) {
if func.code.arity as u32 != arity {
.error(f"Function {func.code.name.data} expected {func.code.arity} arguments, got {arity}")
}
Expand All @@ -417,13 +422,58 @@ def VM::call(&this, arity: u32, func: &Function) {
.tmp.pop()
}

def VM::call_value(&this, arity: u32, val: Value) {
if not val.is_obj() {
.error(f"Can't call object of type {val.type_str()}")
}

match val.as_obj().type {
Function => .call_function(arity, val.as_function())
NativeFunction => {
let func = val.as_native_function()
let res = func.func(this, arity, &.stack.data[.stack.size - arity])
.stack.size -= arity as u32 + 1 // Pop the arguments and the function
.stack.push(res)
}
Class => {
let class = val.as_class()
let instance = gc::allocate_object<Instance>(ObjectType::Instance, this)
let val = Value::Object(&instance.obj)
instance.fields = null

// instance.init allocates memory, so we need to push this on the stack
// so the GC can find it if we collect
.stack.push(val)
instance.init(class)
.stack.pop()

.stack.data[.stack.size - arity - 1] = val

let init_it = class.methods.get_item(.init_string)
if init_it? {
.call_function(arity, init_it.value)

} else if arity != 0 {
.error(f"Can't provide arguments to class without init()")
}
}
Method => {
let method = val.as_method()
.stack.data[.stack.size - arity - 1] = method.this_val
.call_function(arity, method.func)
}
FunctionCode => .error("Function should have been a func")
else => .error(f"Can't call object of type {val.type_str()}")
}
}

def VM::main_loop(&this): i32 {
while true {
// FIXME: For `tests/native_fn.td`, having this line here
// causes perf to go from 1.2s -> 1.6s, _even if_
// the debug == false. Saving it into a local variable
// from the global one doesn't help either.
if debug then .debug_dump_inst(print_stack: true)
// if debug then .debug_dump_inst(print_stack: true)

.cur_inst_ip = .ip
let op = .read_u8() as OpCode
Expand Down Expand Up @@ -518,49 +568,7 @@ def VM::main_loop(&this): i32 {
Call => {
let arity = .read_u8() as u32
let val = .stack.back(arity as u32)

if not val.is_obj() {
.error(f"Can't call object of type {val.type_str()}")
}

match val.as_obj().type {
Function => .call(arity, val.as_function())
NativeFunction => {
let func = val.as_native_function()
let res = func.func(this, arity, &.stack.data[.stack.size - arity])
.stack.size -= arity as u32 + 1 // Pop the arguments and the function
.stack.push(res)
}
Class => {
let class = val.as_class()
let instance = gc::allocate_object<Instance>(ObjectType::Instance, this)
let val = Value::Object(&instance.obj)
instance.fields = null

// instance.init allocates memory, so we need to push this on the stack
// so the GC can find it if we collect
.stack.push(val)
instance.init(class)
.stack.pop()

.stack.data[.stack.size - arity - 1] = val

let init_it = class.methods.get_item(.init_string)
if init_it? {
.call(arity, init_it.value)

} else if arity != 0 {
.error(f"Can't provide arguments to class without init()")
}
}
Method => {
let method = val.as_method()
.stack.data[.stack.size - arity - 1] = method.this_val
.call(arity, method.func)
}
FunctionCode => .error("Function should have been a func")
else => .error(f"Can't call object of type {val.type_str()}")
}
.call_value(arity, val)
}
GetGlobal => {
let name = .read_literal().as_string()
Expand Down Expand Up @@ -617,6 +625,31 @@ def VM::main_loop(&this): i32 {

.error(f"Member {name.data} not found")
}
Invoke => {
let name = .read_literal().as_string()
let arity = .read_u8() as u32

let obj = .stack.back(arity)
if not obj.is_instance() {
.error(f"Can't access a field/member on object of type {obj.type_str()}")
}
let instance = obj.as_instance()
let func = instance.class.methods.get(name, null)
if func? {
// NOTE: the instance is already on the stack in the
// correct position to bind `this`, so we don't
// need to do anything here.
.call_function(arity, func)

} else {
let it = instance.fields.get_item(name)
if not it? {
.error(f"Could not find field/method with name {name}")
}
.call_value(arity, it.value)
}

}
Null => .stack.push(Value::Null())
True => .stack.push(Value::True())
False => .stack.push(Value::False())
Expand Down
6 changes: 3 additions & 3 deletions tests/class_init.td
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
/// out: Person: John 30 JohnJohnJohn

class Person {
def init(name, age) {
def init(this, name, age) {
.name = name
.age = age
}

def print() {
def print(this) {
print("Person: " + .name, .age)
}
def bar() => .name + .name + .name
def bar(this) => .name + .name + .name
}


Expand Down
4 changes: 2 additions & 2 deletions tests/class_methods.td
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
/// out: <instance Foo> 5 8

class Foo {
def bar() => .x = 5
def test(a, b) => .x + a + b
def bar(this) => .x = 5
def test(this, a, b) => .x + a + b
}

let f = Foo()
Expand Down
17 changes: 17 additions & 0 deletions tests/field_func_call.td
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/// out: not a method

// Makes sure we haven't broken this case with the
// INVOKE opcode.

class Oops {
def init(this) {
def f() {
print("not a method")
}

this.field = f
}
}

let oops = Oops()
oops.field()
22 changes: 22 additions & 0 deletions tests/method_bench.td
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/// out: OK

class Foo {
def init(this) {
.name = "foo"
}
def bar(this) => .name + "hello"
}

let foo = Foo()

let start = clock()

for let i = 0; i < 10; i = i + 1 {
for let i = 0; i < 10000; i = i + 1 {
foo.bar()
}
}

let end = clock()
print("Took", end-start, "seconds")
print("OK")

0 comments on commit 5190099

Please sign in to comment.