mirror of
https://github.com/open-goal/jak-project
synced 2026-07-07 14:13:45 -04:00
Add more array stuff and clean up field access (#80)
* implement some array stuff and clean up field access * update goal change log
This commit is contained in:
@@ -285,6 +285,13 @@ class Object {
|
||||
return integer_obj.value;
|
||||
}
|
||||
|
||||
const IntType& as_int() const {
|
||||
if (type != ObjectType::INTEGER) {
|
||||
throw std::runtime_error("as_int called on a " + object_type_to_string(type) + " " + print());
|
||||
}
|
||||
return integer_obj.value;
|
||||
}
|
||||
|
||||
FloatType& as_float() {
|
||||
if (type != ObjectType::FLOAT) {
|
||||
throw std::runtime_error("as_float called on a " + object_type_to_string(type) + " " +
|
||||
|
||||
+4
-1
@@ -18,4 +18,7 @@
|
||||
- Using `&+` and `&+!` now produces a pointer with the same type as the original.
|
||||
- There is a `&-` which returns a `uint` and works with basically any input types
|
||||
- The `&` operator works on fields and elements in arrays
|
||||
- The `&->` operator has been added
|
||||
- The `&->` operator has been added
|
||||
- The `new` operator can create arrays and inline arrays on heaps
|
||||
- The value of `deftype` is now `none`
|
||||
- Creating a method with more than 8 arguments is an error instead of a crash.
|
||||
@@ -72,7 +72,7 @@
|
||||
(define-extern loado (function string kheap object))
|
||||
(define-extern unload (function string none))
|
||||
(define-extern _format (function _varargs_ object))
|
||||
(define-extern malloc (function kheap int pointer))
|
||||
(define-extern malloc (function symbol int pointer))
|
||||
(define-extern kmalloc (function kheap int int string))
|
||||
(define-extern new-dynamic-structure (function kheap type int structure))
|
||||
(define-extern method-set! (function type int function none)) ;; may actually return function.
|
||||
|
||||
@@ -50,6 +50,12 @@ class Compiler {
|
||||
Val* compile_string(const std::string& str, Env* env, int seg = MAIN_SEGMENT);
|
||||
Val* compile_get_symbol_value(const std::string& name, Env* env);
|
||||
Val* compile_function_or_method_call(const goos::Object& form, Env* env);
|
||||
|
||||
Val* get_field_of_structure(const StructureType* type,
|
||||
Val* object,
|
||||
const std::string& field_name,
|
||||
Env* env);
|
||||
|
||||
SymbolVal* compile_get_sym_obj(const std::string& name, Env* env);
|
||||
void color_object_file(FileEnv* env);
|
||||
std::vector<u8> codegen_object_file(FileEnv* env);
|
||||
@@ -84,6 +90,8 @@ class Compiler {
|
||||
Env* env,
|
||||
const std::string& method_type_name = "");
|
||||
|
||||
bool try_getting_constant_integer(const goos::Object& in, int64_t* out, Env* env);
|
||||
|
||||
TypeSystem m_ts;
|
||||
std::unique_ptr<GlobalEnv> m_global_env = nullptr;
|
||||
std::unique_ptr<None> m_none = nullptr;
|
||||
|
||||
@@ -184,4 +184,15 @@ bool Compiler::is_none(Val* in) {
|
||||
|
||||
bool Compiler::is_basic(const TypeSpec& ts) {
|
||||
return m_ts.typecheck(m_ts.make_typespec("basic"), ts, "", false, false);
|
||||
}
|
||||
|
||||
bool Compiler::try_getting_constant_integer(const goos::Object& in, int64_t* out, Env* env) {
|
||||
(void)env;
|
||||
if (in.is_int()) {
|
||||
*out = in.as_int();
|
||||
return true;
|
||||
}
|
||||
|
||||
// todo, try more things before giving up.
|
||||
return false;
|
||||
}
|
||||
@@ -2,6 +2,10 @@
|
||||
#include "common/type_system/deftype.h"
|
||||
|
||||
namespace {
|
||||
|
||||
/*!
|
||||
* Given a method id, get the offset in bytes from the GOAL type to the method pointer.
|
||||
*/
|
||||
int get_offset_of_method(int id) {
|
||||
// todo - something that looks at the type system?
|
||||
// this will need changing if the layout of type ever changes.
|
||||
@@ -9,6 +13,10 @@ int get_offset_of_method(int id) {
|
||||
}
|
||||
} // namespace
|
||||
|
||||
/*!
|
||||
* Given a type and method name (known at compile time), get the method.
|
||||
* This can be used for method calls where the type is unknown at run time (non-virtual method call)
|
||||
*/
|
||||
RegVal* Compiler::compile_get_method_of_type(const TypeSpec& type,
|
||||
const std::string& method_name,
|
||||
Env* env) {
|
||||
@@ -34,6 +42,11 @@ RegVal* Compiler::compile_get_method_of_type(const TypeSpec& type,
|
||||
return deref->to_reg(env);
|
||||
}
|
||||
|
||||
/*!
|
||||
* Given an object, get a method. If at compile time we know it's a basic, we use its runtime
|
||||
* type to look up the method at runtime (virtual call). If we don't know it's a basic, we get the
|
||||
* method from the compile-time type. (fixed type non-virtual call)
|
||||
*/
|
||||
RegVal* Compiler::compile_get_method_of_object(RegVal* object,
|
||||
const std::string& method_name,
|
||||
Env* env) {
|
||||
@@ -72,27 +85,41 @@ RegVal* Compiler::compile_get_method_of_object(RegVal* object,
|
||||
return deref->to_reg(env);
|
||||
}
|
||||
|
||||
/*!
|
||||
* Compile a (deftype ... form)
|
||||
*/
|
||||
Val* Compiler::compile_deftype(const goos::Object& form, const goos::Object& rest, Env* env) {
|
||||
(void)form;
|
||||
(void)env;
|
||||
|
||||
// parse the type definition and add to the type system
|
||||
auto result = parse_deftype(rest, &m_ts);
|
||||
|
||||
// look up the type name
|
||||
auto kv = m_symbol_types.find(result.type.base_type());
|
||||
if (kv != m_symbol_types.end() && kv->second.base_type() != "type") {
|
||||
// we already have something that's not a type with the same name, this is bad.
|
||||
fmt::print("[Warning] deftype will redefined {} from {} to a type.\n", result.type.base_type(),
|
||||
kv->second.print());
|
||||
}
|
||||
// remember that this is a type
|
||||
m_symbol_types[result.type.base_type()] = m_ts.make_typespec("type");
|
||||
|
||||
// get the new method of type object. this is new_type in kscheme.cpp
|
||||
auto new_type_method = compile_get_method_of_type(m_ts.make_typespec("type"), "new", env);
|
||||
// call (new 'type 'type-name parent-type flags)
|
||||
auto new_type_symbol = compile_get_sym_obj(result.type.base_type(), env)->to_gpr(env);
|
||||
auto parent_type = compile_get_symbol_value(result.type_info->get_parent(), env)->to_gpr(env);
|
||||
auto flags_int = compile_integer(result.flags.flag, env)->to_gpr(env);
|
||||
return compile_real_function_call(form, new_type_method,
|
||||
{new_type_symbol, parent_type, flags_int}, env);
|
||||
compile_real_function_call(form, new_type_method, {new_type_symbol, parent_type, flags_int}, env);
|
||||
|
||||
// return none, making the value of (deftype..) unusable
|
||||
return get_none();
|
||||
}
|
||||
|
||||
/*!
|
||||
* Compile a (defmethod ...) form
|
||||
*/
|
||||
Val* Compiler::compile_defmethod(const goos::Object& form, const goos::Object& _rest, Env* env) {
|
||||
auto fe = get_parent_env_of_type<FunctionEnv>(env);
|
||||
auto* rest = &_rest;
|
||||
@@ -115,20 +142,24 @@ Val* Compiler::compile_defmethod(const goos::Object& form, const goos::Object& _
|
||||
auto& lambda = place->lambda;
|
||||
auto lambda_ts = m_ts.make_typespec("function");
|
||||
|
||||
// parse the argument list.
|
||||
// parse the argument list. todo, we could check the type of the first argument here?
|
||||
for_each_in_list(arg_list, [&](const goos::Object& o) {
|
||||
if (o.is_symbol()) {
|
||||
// if it has no type, assume object.
|
||||
lambda.params.push_back({symbol_string(o), m_ts.make_typespec("object")});
|
||||
lambda_ts.add_arg(m_ts.make_typespec("object"));
|
||||
} else {
|
||||
// type of argument is specified
|
||||
auto param_args = get_va(o, o);
|
||||
va_check(o, param_args, {goos::ObjectType::SYMBOL, goos::ObjectType::SYMBOL}, {});
|
||||
|
||||
GoalArg parm;
|
||||
parm.name = symbol_string(param_args.unnamed.at(0));
|
||||
parm.type = parse_typespec(param_args.unnamed.at(1));
|
||||
// before substituting _type_
|
||||
lambda_ts.add_arg(parm.type);
|
||||
|
||||
// replace _type_ as needed for inside this function.
|
||||
parm.type = parm.type.substitute_for_method_call(symbol_string(type_name));
|
||||
lambda.params.push_back(parm);
|
||||
}
|
||||
@@ -150,7 +181,9 @@ Val* Compiler::compile_defmethod(const goos::Object& form, const goos::Object& _
|
||||
new_func_env->method_of_type_name = symbol_string(type_name);
|
||||
|
||||
// set up arguments
|
||||
assert(lambda.params.size() < 8); // todo graceful error
|
||||
if (lambda.params.size() > 8) {
|
||||
throw_compile_error(form, "Methods cannot have more than 8 arguments");
|
||||
}
|
||||
std::vector<RegVal*> args_for_coloring;
|
||||
for (u32 i = 0; i < lambda.params.size(); i++) {
|
||||
IRegConstraint constr;
|
||||
@@ -210,6 +243,47 @@ Val* Compiler::compile_defmethod(const goos::Object& form, const goos::Object& _
|
||||
return compile_real_function_call(form, method_set_val, {type_obj, id_val, method_val}, env);
|
||||
}
|
||||
|
||||
/*!
|
||||
* Given a type, object, and field name, get the value of the field.
|
||||
*/
|
||||
Val* Compiler::get_field_of_structure(const StructureType* type,
|
||||
Val* object,
|
||||
const std::string& field_name,
|
||||
Env* env) {
|
||||
auto fe = get_parent_env_of_type<FunctionEnv>(env);
|
||||
Val* result = nullptr;
|
||||
int offset = -type->get_offset();
|
||||
auto field = m_ts.lookup_field_info(type->get_name(), field_name);
|
||||
if (field.needs_deref) {
|
||||
TypeSpec loc_type = m_ts.make_pointer_typespec(field.type);
|
||||
auto loc =
|
||||
fe->alloc_val<MemoryOffsetConstantVal>(loc_type, object, field.field.offset() + offset);
|
||||
auto di = m_ts.get_deref_info(loc_type);
|
||||
assert(di.can_deref);
|
||||
assert(di.mem_deref);
|
||||
result = fe->alloc_val<MemoryDerefVal>(di.result_type, loc, MemLoadInfo(di));
|
||||
result->mark_as_settable();
|
||||
} else {
|
||||
result =
|
||||
fe->alloc_val<MemoryOffsetConstantVal>(field.type, object, field.field.offset() + offset);
|
||||
result->mark_as_settable();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/*!
|
||||
* Compile the (-> ...) form.
|
||||
* This is kind of a mess because of the huge number of things you can do with this form:
|
||||
* - dereference a pointer
|
||||
* - Access a field of a type
|
||||
* - Access an element of an array or inline-array
|
||||
* and they nest in confusing and ambiguous ways.
|
||||
* The current behavior is that there is exactly one dereference performed per thing in the list.
|
||||
* so if field x has type (pointer y), (-> obj x) gives a (pointer y), not a y.
|
||||
*
|
||||
* The result of this should give something that has enough information to read/write the original
|
||||
* location. Otherwise set! or & won't work.
|
||||
*/
|
||||
Val* Compiler::compile_deref(const goos::Object& form, const goos::Object& _rest, Env* env) {
|
||||
auto fe = get_parent_env_of_type<FunctionEnv>(env);
|
||||
if (_rest.is_empty_list()) {
|
||||
@@ -251,23 +325,7 @@ Val* Compiler::compile_deref(const goos::Object& form, const goos::Object& _rest
|
||||
auto struct_type = dynamic_cast<StructureType*>(type_info);
|
||||
|
||||
if (struct_type) {
|
||||
int offset = -struct_type->get_offset();
|
||||
auto field = m_ts.lookup_field_info(type_info->get_name(), field_name);
|
||||
if (field.needs_deref) {
|
||||
TypeSpec loc_type = m_ts.make_pointer_typespec(field.type);
|
||||
auto loc = fe->alloc_val<MemoryOffsetConstantVal>(loc_type, result,
|
||||
field.field.offset() + offset);
|
||||
auto di = m_ts.get_deref_info(loc_type);
|
||||
assert(di.can_deref);
|
||||
assert(di.mem_deref);
|
||||
result = fe->alloc_val<MemoryDerefVal>(di.result_type, loc, MemLoadInfo(di));
|
||||
result->mark_as_settable();
|
||||
} else {
|
||||
result = fe->alloc_val<MemoryOffsetConstantVal>(field.type, result,
|
||||
field.field.offset() + offset);
|
||||
result->mark_as_settable();
|
||||
// assert(false);
|
||||
}
|
||||
result = get_field_of_structure(struct_type, result, field_name, env);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -299,6 +357,9 @@ Val* Compiler::compile_deref(const goos::Object& form, const goos::Object& _rest
|
||||
return result;
|
||||
}
|
||||
|
||||
/*!
|
||||
* Compile the (& x) form.
|
||||
*/
|
||||
Val* Compiler::compile_addr_of(const goos::Object& form, const goos::Object& rest, Env* env) {
|
||||
auto args = get_va(form, rest);
|
||||
va_check(form, args, {{}}, {});
|
||||
@@ -310,6 +371,10 @@ Val* Compiler::compile_addr_of(const goos::Object& form, const goos::Object& res
|
||||
return as_mem_deref->base;
|
||||
}
|
||||
|
||||
/*!
|
||||
* Compile the (the-as x y) form. Like a reinterpret cast.
|
||||
* Will always produce a alias (so setting the result of this affects the base).
|
||||
*/
|
||||
Val* Compiler::compile_the_as(const goos::Object& form, const goos::Object& rest, Env* env) {
|
||||
auto args = get_va(form, rest);
|
||||
va_check(form, args, {{}, {}}, {});
|
||||
@@ -322,6 +387,11 @@ Val* Compiler::compile_the_as(const goos::Object& form, const goos::Object& rest
|
||||
return result;
|
||||
}
|
||||
|
||||
/*!
|
||||
* Compile the (the x y) form. Like reinterpret case, but numbers (int bint float) will be
|
||||
* converted. In the case of numeric version it won't alias. But all other cases alias, which is
|
||||
* confusing.
|
||||
*/
|
||||
Val* Compiler::compile_the(const goos::Object& form, const goos::Object& rest, Env* env) {
|
||||
auto args = get_va(form, rest);
|
||||
va_check(form, args, {{}, {}}, {});
|
||||
@@ -351,6 +421,9 @@ Val* Compiler::compile_the(const goos::Object& form, const goos::Object& rest, E
|
||||
return result;
|
||||
}
|
||||
|
||||
/*!
|
||||
* Debug util (print-type x) to compile x then print the type name at compile time.
|
||||
*/
|
||||
Val* Compiler::compile_print_type(const goos::Object& form, const goos::Object& rest, Env* env) {
|
||||
auto args = get_va(form, rest);
|
||||
va_check(form, args, {{}}, {});
|
||||
@@ -359,6 +432,8 @@ Val* Compiler::compile_print_type(const goos::Object& form, const goos::Object&
|
||||
}
|
||||
|
||||
Val* Compiler::compile_new(const goos::Object& form, const goos::Object& _rest, Env* env) {
|
||||
// todo - support compound types.
|
||||
|
||||
auto allocation = quoted_sym_as_string(pair_car(_rest));
|
||||
auto rest = &pair_cdr(_rest);
|
||||
|
||||
@@ -367,10 +442,50 @@ Val* Compiler::compile_new(const goos::Object& form, const goos::Object& _rest,
|
||||
rest = &pair_cdr(*rest);
|
||||
|
||||
if (allocation == "global" || allocation == "debug") {
|
||||
if (type_as_string == "inline-array") {
|
||||
assert(false);
|
||||
} else if (type_as_string == "array") {
|
||||
assert(false);
|
||||
// allocate on a named heap
|
||||
|
||||
if (type_as_string == "inline-array" || type_as_string == "array") {
|
||||
bool is_inline = type_as_string == "inline-array";
|
||||
auto elt_type = quoted_sym_as_string(pair_car(*rest));
|
||||
rest = &pair_cdr(*rest);
|
||||
|
||||
auto count_obj = pair_car(*rest);
|
||||
rest = &pair_cdr(*rest);
|
||||
// try to get the size as a compile time constant.
|
||||
int64_t constant_count = 0;
|
||||
bool is_constant_size = try_getting_constant_integer(count_obj, &constant_count, env);
|
||||
|
||||
if (!rest->is_empty_list()) {
|
||||
// got extra arguments
|
||||
throw_compile_error(form, "new array form got more arguments than expected");
|
||||
}
|
||||
|
||||
auto ts = is_inline ? m_ts.make_inline_array_typespec(elt_type)
|
||||
: m_ts.make_pointer_typespec(elt_type);
|
||||
auto info = m_ts.get_deref_info(ts);
|
||||
if (!info.can_deref) {
|
||||
throw_compile_error(form,
|
||||
fmt::format("Cannot make an {} of {}\n", type_as_string, ts.print()));
|
||||
}
|
||||
|
||||
auto malloc_func = compile_get_symbol_value("malloc", env)->to_reg(env);
|
||||
std::vector<RegVal*> args;
|
||||
args.push_back(compile_get_sym_obj(allocation, env)->to_reg(env));
|
||||
|
||||
if (is_constant_size) {
|
||||
auto array_size = constant_count * info.stride;
|
||||
args.push_back(compile_integer(array_size, env)->to_reg(env));
|
||||
} else {
|
||||
auto array_size = compile_integer(info.stride, env)->to_reg(env);
|
||||
env->emit(
|
||||
std::make_unique<IR_IntegerMath>(IntegerMathKind::IMUL_32, array_size,
|
||||
compile_error_guard(count_obj, env)->to_gpr(env)));
|
||||
args.push_back(array_size);
|
||||
}
|
||||
|
||||
auto array = compile_real_function_call(form, malloc_func, args, env);
|
||||
array->set_type(ts);
|
||||
return array;
|
||||
} else {
|
||||
auto type_of_obj = m_ts.make_typespec(type_as_string);
|
||||
std::vector<RegVal*> args;
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
(start-test "new-array")
|
||||
|
||||
;; align the heap.
|
||||
(new 'global 'array 'uint8 16)
|
||||
|
||||
;; get the current alignment
|
||||
(let ((current (-> global current)))
|
||||
(expect-true (= current (new 'global 'array 'uint16 3))) ;; 6 bytes
|
||||
(expect-true (= (&+ current 6) (-> global current)))
|
||||
)
|
||||
|
||||
;; align the heap.
|
||||
(new 'global 'array 'uint8 16)
|
||||
|
||||
;; get the current alignment
|
||||
(let ((current (-> global current)))
|
||||
(expect-true (= current (new 'global 'array 'bfloat 3))) ;; 3 * 4 = 12 bytes
|
||||
(expect-true (= (&+ current 12) (-> global current)))
|
||||
)
|
||||
|
||||
;; align the heap.
|
||||
(new 'global 'array 'uint8 16)
|
||||
|
||||
;; get the current alignment
|
||||
(let ((current (-> global current)))
|
||||
(expect-true (= current (new 'global 'inline-array 'bfloat 3))) ;; 3*4 = 12 bytes
|
||||
(expect-true (= (&+ current 48) (-> global current)))
|
||||
)
|
||||
|
||||
;; get the current alignment
|
||||
(let ((current (-> global current))
|
||||
(three 3))
|
||||
(expect-true (= current (new 'global 'inline-array 'bfloat (* three 4)))) ;; 12*16 = 36 bytes
|
||||
(expect-true (= (&+ current 192) (-> global current)))
|
||||
)
|
||||
|
||||
(finish-test)
|
||||
@@ -62,6 +62,12 @@ std::thread WithGameTests::runtime_thread;
|
||||
Compiler WithGameTests::compiler;
|
||||
GoalTest::CompilerTestRunner WithGameTests::runner;
|
||||
|
||||
namespace {
|
||||
std::vector<std::string> get_test_pass_string(const std::string& name, int count) {
|
||||
return {fmt::format("Test \"{}\": {} Passes\n0\n", name, count)};
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// TODO - havn't done anything with these really yet
|
||||
TEST_F(WithGameTests, All) {
|
||||
runner.run_static_test(env, testCategory, "defun-return-constant.static.gc", {"12\n"});
|
||||
@@ -109,15 +115,16 @@ TEST_F(WithGameTests, All) {
|
||||
{"Test \"test-type-arrays\": 3 Passes\n0\n"});
|
||||
runner.run_static_test(env, testCategory, "test-number-comparison.gc",
|
||||
{"Test \"number-comparison\": 14 Passes\n0\n"});
|
||||
/*runner.run_static_test(env, testCategory, "test-approx-pi.gc",
|
||||
runner.run_static_test(env, testCategory, "test-approx-pi.gc",
|
||||
get_test_pass_string("approx-pi", 4));
|
||||
runner.run_static_test(env, testCategory, "test-dynamic-type.gc",
|
||||
get_test_pass_string("dynamic-type", 4));
|
||||
runner.run_static_test(env, testCategory, "test-string-type.gc",
|
||||
get_test_pass_string("string-type", 4));
|
||||
runner.run_static_test(env, testCategory, "test-new-string.gc",
|
||||
get_test_pass_string("new-string", 5));*/
|
||||
// runner.run_static_test(env, testCategory, "test-addr-of.gc", get_test_pass_string("addr-of",
|
||||
// 2));
|
||||
get_test_pass_string("new-string", 5));
|
||||
runner.run_static_test(env, testCategory, "test-addr-of.gc", get_test_pass_string("addr-of", 2));
|
||||
runner.run_static_test(env, testCategory, "test-set-self.gc", {"#t\n0\n"});
|
||||
runner.run_static_test(env, testCategory, "test-new-array.gc",
|
||||
get_test_pass_string("new-array", 8));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user