Merge pull request #2 from water111/w/type-system

Implement common type system library
This commit is contained in:
water111 2020-08-28 14:39:23 -04:00 committed by GitHub
commit 90060dbc21
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 701 additions and 480 deletions

View file

@ -1,10 +1,24 @@
#ifndef JAK_GOAL_CONSTANTS_H
#define JAK_GOAL_CONSTANTS_H
#include "common_types.h"
constexpr s32 BINTEGER_OFFSET = 0;
constexpr s32 PAIR_OFFSET = 2;
constexpr int POINTER_SIZE = 4;
constexpr int BASIC_OFFSET = 4;
constexpr int STRUCTURE_ALIGNMENT = 16;
enum class RegKind { GPR_64, FLOAT, INT_128, FLOAT_4X, INVALID };
constexpr u32 GOAL_NEW_METHOD = 0; // method ID of GOAL new
constexpr u32 GOAL_DEL_METHOD = 1; // method ID of GOAL delete
constexpr u32 GOAL_PRINT_METHOD = 2; // method ID of GOAL print
constexpr u32 GOAL_INSPECT_METHOD = 3; // method ID of GOAL inspect
constexpr u32 GOAL_LENGTH_METHOD = 4; // method ID of GOAL length
constexpr u32 GOAL_ASIZE_METHOD = 5; // method ID of GOAL size
constexpr u32 GOAL_COPY_METHOD = 6; // method ID of GOAL copy
constexpr u32 GOAL_RELOC_METHOD = 7; // method ID of GOAL relocate
constexpr u32 GOAL_MEMUSAGE_METHOD = 8; // method ID of GOAL mem-usage
#endif // JAK_GOAL_CONSTANTS_H

View file

@ -108,6 +108,11 @@ std::string Type::get_name() const {
}
std::string Type::get_runtime_name() const {
if (!m_allow_in_runtime) {
fmt::print("[TypeSystem] Tried to use type {} as a runtime type, which is not allowed.\n",
get_name());
throw std::runtime_error("get_runtime_name");
}
return m_runtime_name;
}
@ -137,7 +142,7 @@ bool Type::is_equal(const Type& other) const {
* parents.
*/
bool Type::has_parent() const {
return m_parent != "object" && !m_parent.empty();
return m_name != "object" && !m_parent.empty();
}
/*!
@ -218,52 +223,53 @@ std::string Type::print_method_info() const {
}
/////////////
// NoneType
// NullType
/////////////
// Special Type representing nothing.
// it's an error to try to do anything with None.
// Special Type for both "none" and "_type_" types
// it's an error to try to do anything with Null.
NoneType::NoneType() : Type("", "none", false) {}
NullType::NullType(std::string name) : Type("", std::move(name), false) {}
bool NoneType::is_reference() const {
throw std::runtime_error("is_reference called on NoneType");
bool NullType::is_reference() const {
throw std::runtime_error("is_reference called on NullType");
}
int NoneType::get_load_size() const {
throw std::runtime_error("get_load_size called on NoneType");
int NullType::get_load_size() const {
throw std::runtime_error("get_load_size called on NullType");
}
bool NoneType::get_load_signed() const {
throw std::runtime_error("get_load_size called on NoneType");
bool NullType::get_load_signed() const {
throw std::runtime_error("get_load_size called on NullType");
}
int NoneType::get_size_in_memory() const {
throw std::runtime_error("get_size_in_memory called on NoneType");
int NullType::get_size_in_memory() const {
throw std::runtime_error("get_size_in_memory called on NullType");
}
RegKind NoneType::get_preferred_reg_kind() const {
throw std::runtime_error("get_preferred_reg_kind called on NoneType");
RegKind NullType::get_preferred_reg_kind() const {
throw std::runtime_error("get_preferred_reg_kind called on NullType");
}
int NoneType::get_offset() const {
int NullType::get_offset() const {
throw std::runtime_error("get_offset called on NoneType");
}
int NoneType::get_in_memory_alignment() const {
throw std::runtime_error("get_in_memory_alignment called on NoneType");
int NullType::get_in_memory_alignment() const {
throw std::runtime_error("get_in_memory_alignment called on NullType");
}
int NoneType::get_inline_array_alignment() const {
throw std::runtime_error("get_inline_array_alignment called on NoneType");
int NullType::get_inline_array_alignment() const {
throw std::runtime_error("get_inline_array_alignment called on NullType");
}
std::string NoneType::print() const {
return "none";
std::string NullType::print() const {
return m_name;
}
bool NoneType::operator==(const Type& other) const {
// there should be only one none type, so this is safe.
bool NullType::operator==(const Type& other) const {
// any redefinition by the user should be invalid, so this will always return false unless
// you're calling it on the same object.
return this == &other;
}
@ -484,7 +490,7 @@ void StructureType::override_size_in_memory(int size) {
}
int StructureType::get_offset() const {
return 0;
return m_offset;
}
int StructureType::get_in_memory_alignment() const {

View file

@ -68,6 +68,8 @@ class Type {
const MethodInfo& add_new_method(const MethodInfo& info);
std::string print_method_info() const;
void disallow_in_runtime() { m_allow_in_runtime = false; }
virtual ~Type() = default;
protected:
@ -79,6 +81,7 @@ class Type {
std::string m_parent; // the parent type (is empty for none and object)
std::string m_name;
bool m_allow_in_runtime = true;
std::string m_runtime_name;
bool m_is_boxed = false; // does this have runtime type information?
};
@ -87,9 +90,9 @@ class Type {
* Used only for "none" - this is a type that the compiler can use for "this has no value".
* Attempting to do anything with a NoneType is an error.
*/
class NoneType : public Type {
class NullType : public Type {
public:
NoneType();
NullType(std::string name);
bool is_reference() const override;
int get_load_size() const override;
bool get_load_signed() const override;
@ -100,7 +103,7 @@ class NoneType : public Type {
int get_in_memory_alignment() const override;
std::string print() const override;
bool operator==(const Type& other) const override;
~NoneType() = default;
~NullType() = default;
};
/*!
@ -217,10 +220,12 @@ class StructureType : public ReferenceType {
int get_in_memory_alignment() const override;
int get_inline_array_alignment() const override;
bool lookup_field(const std::string& name, Field* out);
bool is_dynamic() const { return m_dynamic; }
~StructureType() = default;
protected:
friend class TypeSystem;
void override_offset(int offset) { m_offset = offset; }
void override_size_in_memory(
int size); // only to be used for setting up weird types like "structure"
void add_field(const Field& f, int new_size_in_mem) {
@ -234,6 +239,7 @@ class StructureType : public ReferenceType {
bool m_dynamic = false;
int m_size_in_mem = 0;
bool m_pack = false;
int m_offset = 0;
};
class BasicType : public StructureType {

View file

@ -34,4 +34,13 @@ bool TypeSpec::operator==(const TypeSpec& other) const {
}
return true;
}
TypeSpec TypeSpec::substitute_for_method_call(const std::string& method_type) const {
TypeSpec result;
result.m_type = (m_type == "_type_") ? method_type : m_type;
for (const auto& x : m_arguments) {
result.m_arguments.push_back(x.substitute_for_method_call(method_type));
}
return result;
}

View file

@ -33,12 +33,18 @@ class TypeSpec {
void add_arg(const TypeSpec& ts) { m_arguments.push_back(ts); }
const std::string base_type() const { return m_type; }
bool has_single_arg() const { return m_arguments.size() == 1; }
const TypeSpec& get_single_arg() const {
assert(m_arguments.size() == 1);
return m_arguments.front();
}
TypeSpec substitute_for_method_call(const std::string& method_type) const;
private:
friend class TypeSystem;
std::string m_type;
std::vector<TypeSpec> m_arguments;
};

View file

@ -5,8 +5,9 @@
#include <cassert>
TypeSystem::TypeSystem() {
// the "none" type is included by default.
add_type("none", std::make_unique<NoneType>());
// the "none" and "_type_" types are included by default.
add_type("none", std::make_unique<NullType>("none"));
add_type("_type_", std::make_unique<NullType>("_type_"));
}
/*!
@ -39,7 +40,7 @@ Type* TypeSystem::add_type(const std::string& name, std::unique_ptr<Type> type)
// newly defined!
// none/object get to skip these checks because they are roots.
if (name != "object" && name != "none") {
if (name != "object" && name != "none" && name != "_type_") {
if (m_forward_declared_types.find(type->get_parent()) != m_forward_declared_types.end()) {
fmt::print("[TypeSystem] Type {} has incompletely defined parent {}\n", type->get_name(),
type->get_parent());
@ -83,11 +84,24 @@ std::string TypeSystem::get_runtime_type(const TypeSpec& ts) {
DerefInfo TypeSystem::get_deref_info(const TypeSpec& ts) {
DerefInfo info;
if (!ts.has_single_arg()) {
// not enough info.
info.can_deref = false;
return info;
}
// default to GPR
info.reg = RegKind::GPR_64;
info.mem_deref = true;
if (ts.base_type() == "inline-array") {
auto result_type = lookup_type(ts.get_single_arg());
auto result_structure_type = dynamic_cast<StructureType*>(result_type);
if (!result_structure_type || result_structure_type->is_dynamic()) {
info.can_deref = false;
return info;
}
// it's an inline array of structures. We can "dereference". But really we don't do a memory
// dereference, we just add stride*idx to the pointer.
info.can_deref = true; // deref operators should work...
@ -95,7 +109,6 @@ DerefInfo TypeSystem::get_deref_info(const TypeSpec& ts) {
info.result_type = ts.get_single_arg(); // what we're an inline-array of
info.sign_extend = false; // not applicable anyway
auto result_type = lookup_type(info.result_type);
if (result_type->is_reference()) {
info.stride =
align(result_type->get_size_in_memory(), result_type->get_inline_array_alignment());
@ -111,11 +124,13 @@ DerefInfo TypeSystem::get_deref_info(const TypeSpec& ts) {
// in memory, an array of pointers
info.stride = POINTER_SIZE;
info.sign_extend = false;
info.load_size = POINTER_SIZE;
} else {
// an array of values, which should be loaded in the correct way to the correct register
info.stride = result_type->get_size_in_memory();
info.sign_extend = result_type->get_load_signed();
info.reg = result_type->get_preferred_reg_kind();
info.load_size = result_type->get_load_size();
assert(result_type->get_size_in_memory() == result_type->get_load_size());
}
} else {
@ -187,7 +202,7 @@ TypeSpec TypeSystem::make_inline_array_typespec(const TypeSpec& type) {
* possible, don't store a Type* and store a TypeSpec instead. The TypeSpec can then be used with
* lookup_type to find the most up-to-date type information.
*/
Type* TypeSystem::lookup_type(const std::string& name) {
Type* TypeSystem::lookup_type(const std::string& name) const {
auto kv = m_types.find(name);
if (kv != m_types.end()) {
return kv->second.get();
@ -208,7 +223,7 @@ Type* TypeSystem::lookup_type(const std::string& name) {
* possible, don't store a Type* and store a TypeSpec instead. The TypeSpec can then be used with
* lookup_type to find the most up-to-date type information.
*/
Type* TypeSystem::lookup_type(const TypeSpec& ts) {
Type* TypeSystem::lookup_type(const TypeSpec& ts) const {
return lookup_type(ts.base_type());
}
@ -487,7 +502,7 @@ int TypeSystem::add_field_to_type(StructureType* type,
* Add types which are built-in to GOAL.
*/
void TypeSystem::add_builtin_types() {
// some of the basic types having confusing circular dependencies, so this is done manually.
// some of the basic types have confusing circular dependencies, so this is done manually.
// there are no inlined things so its ok to do some things out of order because the actual size
// doesn't really matter.
@ -505,48 +520,53 @@ void TypeSystem::add_builtin_types() {
auto link_block_type = add_builtin_basic("basic", "link-block");
auto kheap_type = add_builtin_structure("structure", "kheap");
auto array_type = add_builtin_basic("basic", "array");
auto pair_type = add_builtin_structure("object", "pair");
auto pair_type = add_builtin_structure("object", "pair", true);
auto process_tree_type = add_builtin_basic("basic", "process-tree");
auto process_type = add_builtin_basic("process-tree", "process");
auto thread_type = add_builtin_basic("basic", "thread");
auto connectable_type = add_builtin_structure("structure", "connectable");
auto stack_frame_type = add_builtin_basic("basic", "stack-frame");
auto file_stream_type = add_builtin_basic("basic", "file-stream");
auto pointer_type = add_builtin_value_type("object", "pointer", 4);
auto number_type = add_builtin_value_type("object", "number", 8); // sign extend?
auto float_type = add_builtin_value_type("number", "float", 4, false, false, RegKind::FLOAT);
auto integer_type = add_builtin_value_type("number", "integer", 8, false, false); // sign extend?
auto binteger_type =
add_builtin_value_type("integer", "binteger", 8, true, false); // sign extend?
auto sinteger_type = add_builtin_value_type("integer", "sinteger", 8, false, true);
auto int8_type = add_builtin_value_type("sinteger", "int8", 1, false, true);
auto int16_type = add_builtin_value_type("sinteger", "int16", 2, false, true);
auto int32_type = add_builtin_value_type("sinteger", "int32", 4, false, true);
auto int64_type = add_builtin_value_type("sinteger", "int64", 8, false, true);
auto int128_type =
add_builtin_value_type("sinteger", "int128", 16, false, true, RegKind::INT_128);
auto uinteger_type = add_builtin_value_type("integer", "uinteger", 8);
auto uint8_type = add_builtin_value_type("uinteger", "uint8", 1);
auto uint16_type = add_builtin_value_type("uinteger", "uint16", 2);
auto uint32_type = add_builtin_value_type("uinteger", "uint32", 4);
auto uint64_type = add_builtin_value_type("uinteger", "uint64", 81);
auto uint128_type =
add_builtin_value_type("uinteger", "uint128", 16, false, false, RegKind::INT_128);
add_builtin_value_type("object", "pointer", 4);
auto inline_array_type = add_builtin_value_type("object", "inline-array", 4);
inline_array_type->set_runtime_type("pointer");
add_builtin_value_type("object", "number", 8); // sign extend?
add_builtin_value_type("number", "float", 4, false, false, RegKind::FLOAT);
add_builtin_value_type("number", "integer", 8, false, false); // sign extend?
add_builtin_value_type("integer", "binteger", 8, true, false); // sign extend?
add_builtin_value_type("integer", "sinteger", 8, false, true);
add_builtin_value_type("sinteger", "int8", 1, false, true);
add_builtin_value_type("sinteger", "int16", 2, false, true);
add_builtin_value_type("sinteger", "int32", 4, false, true);
add_builtin_value_type("sinteger", "int64", 8, false, true);
add_builtin_value_type("sinteger", "int128", 16, false, true, RegKind::INT_128);
add_builtin_value_type("integer", "uinteger", 8);
add_builtin_value_type("uinteger", "uint8", 1);
add_builtin_value_type("uinteger", "uint16", 2);
add_builtin_value_type("uinteger", "uint32", 4);
add_builtin_value_type("uinteger", "uint64", 81);
add_builtin_value_type("uinteger", "uint128", 16, false, false, RegKind::INT_128);
auto int_type = add_builtin_value_type("integer", "int", 8, false, true);
int_type->disallow_in_runtime();
auto uint_type = add_builtin_value_type("uinteger", "uint", 8, false, false);
uint_type->disallow_in_runtime();
// Methods and Fields
// OBJECT
add_method(obj_type, "new", make_function_typespec({"symbol", "type", "int32"}, "object"));
add_method(obj_type, "delete", make_function_typespec({"object"}, "none"));
add_method(obj_type, "print", make_function_typespec({"object"}, "object"));
add_method(obj_type, "inspect", make_function_typespec({"object"}, "object"));
add_method(obj_type, "new", make_function_typespec({"symbol", "type", "int32"}, "_type_"));
add_method(obj_type, "delete", make_function_typespec({"_type_"}, "none"));
add_method(obj_type, "print", make_function_typespec({"_type_"}, "_type_"));
add_method(obj_type, "inspect", make_function_typespec({"_type_"}, "_type_"));
add_method(obj_type, "length",
make_function_typespec({"object"}, "int32")); // todo - this integer type?
add_method(obj_type, "asize-of", make_function_typespec({"object"}, "int32"));
add_method(obj_type, "copy", make_function_typespec({"object", "symbol"}, "object"));
add_method(obj_type, "relocate", make_function_typespec({"object", "int32"}, "object"));
make_function_typespec({"_type_"}, "int32")); // todo - this integer type?
add_method(obj_type, "asize-of", make_function_typespec({"_type_"}, "int32"));
add_method(obj_type, "copy", make_function_typespec({"_type_", "symbol"}, "_type_"));
add_method(obj_type, "relocate", make_function_typespec({"_type_", "int32"}, "_type_"));
add_method(obj_type, "mem-usage",
make_function_typespec({"object"}, "int32")); // todo - this is a guess.
make_function_typespec({"_type_"}, "int32")); // todo - this is a guess.
// STRUCTURE
// structure new doesn't support dynamic sizing, which is kinda weird - it grabs the size from
@ -590,6 +610,38 @@ void TypeSystem::add_builtin_types() {
// VU FUNCTION
// don't inherit
add_field_to_type(vu_function_type, "length", make_typespec("int32")); // todo integer type
add_field_to_type(vu_function_type, "origin", make_typespec("pointer")); // todo sign extend?
add_field_to_type(vu_function_type, "qlength", make_typespec("int32")); // todo integer type
// link block
builtin_structure_inherit(link_block_type);
add_field_to_type(link_block_type, "allocated-length",
make_typespec("int32")); // todo integer type
add_field_to_type(link_block_type, "version", make_typespec("int32")); // todo integer type
// there's probably some dynamically sized stuff after this...
// kheap
add_field_to_type(kheap_type, "base", make_typespec("pointer"));
add_field_to_type(kheap_type, "top", make_typespec("pointer"));
add_field_to_type(kheap_type, "current", make_typespec("pointer"));
add_field_to_type(kheap_type, "top-base", make_typespec("pointer"));
// todo
(void)array_type;
// pair
pair_type->override_offset(2);
add_field_to_type(pair_type, "car", make_typespec("object"));
add_field_to_type(pair_type, "cdr", make_typespec("object"));
// todo, with kernel
(void)process_tree_type;
(void)process_type;
(void)thread_type;
(void)connectable_type;
(void)stack_frame_type;
(void)file_stream_type;
}
/*!
@ -624,23 +676,6 @@ int TypeSystem::get_next_method_id(Type* type) {
}
}
/*!
* For debugging, todo remove.
*/
int TypeSystem::manual_add_field_to_type(StructureType* type,
const std::string& field_name,
const TypeSpec& field_type,
int offset,
int size,
int alignment) {
Field field(field_name, field_type);
field.set_alignment(alignment);
field.set_offset(offset);
int new_size = type->get_size_in_memory() + size;
type->add_field(field, new_size);
return offset;
}
/*!
* Lookup a field of a type by name
*/
@ -724,8 +759,9 @@ int TypeSystem::get_size_in_type(const Field& field) {
* things in the wrong order.
*/
StructureType* TypeSystem::add_builtin_structure(const std::string& parent,
const std::string& type_name) {
add_type(type_name, std::make_unique<StructureType>(parent, type_name));
const std::string& type_name,
bool boxed) {
add_type(type_name, std::make_unique<StructureType>(parent, type_name, boxed));
return get_type_of_type<StructureType>(type_name);
}
@ -758,4 +794,172 @@ ValueType* TypeSystem::add_builtin_value_type(const std::string& parent,
*/
void TypeSystem::builtin_structure_inherit(StructureType* st) {
st->inherit(get_type_of_type<StructureType>(st->get_parent()));
}
/*!
* Main compile-time type check!
* @param expected - the expected type
* @param actual - the actual type (can be more specific)
* @param error_source_name - optional, can provide a name for where the error comes from
* @param print_on_error - print a message explaining the type error, if there is one
* @param throw_on_error - throw a std::runtime_error on failure if set.
* @return if the type check passes
*/
bool TypeSystem::typecheck(const TypeSpec& expected,
const TypeSpec& actual,
const std::string& error_source_name,
bool print_on_error,
bool throw_on_error) const {
bool success = true;
// first, typecheck the base types:
if (!typecheck_base_types(expected.base_type(), actual.base_type())) {
success = false;
}
// next argument checks:
if (expected.m_arguments.size() == actual.m_arguments.size()) {
for (size_t i = 0; i < expected.m_arguments.size(); i++) {
// don't print/throw because the error would be confusing. Better to fail only the
// outer most check and print a single error message.
if (!typecheck(expected.m_arguments[i], actual.m_arguments[i], "", false, false)) {
success = false;
break;
}
}
} else {
// different sizes of arguments.
if (expected.m_arguments.empty()) {
// we expect zero arguments, but got some. The actual type is more specific, so this is fine.
} else {
// different sizes, and we expected arguments. No good!
success = false;
}
}
if (!success) {
if (print_on_error) {
if (error_source_name.empty()) {
fmt::print("[TypeSystem] Got type \"{}\" when expecting \"{}\"\n", actual.print(),
expected.print());
} else {
fmt::print("[TypeSystem] For {}, got type \"{}\" when expecting \"{}\"\n",
error_source_name, actual.print(), expected.print());
}
}
if (throw_on_error) {
throw std::runtime_error("typecheck failed");
}
}
return success;
}
/*!
* Is actual of type expected? For base types.
*/
bool TypeSystem::typecheck_base_types(const std::string& expected,
const std::string& actual) const {
// just to make sure it exists. (note - could there be a case when it just has to be forward
// declared, but not defined?)
lookup_type(expected);
if (expected == actual) {
lookup_type(actual); // make sure it exists
return true;
}
std::string actual_name = actual;
auto actual_type = lookup_type(actual_name);
while (actual_type->has_parent()) {
actual_name = actual_type->get_parent();
actual_type = lookup_type(actual_name);
if (expected == actual_name) {
return true;
}
}
return false;
}
/*!
* Get a path from type to object.
*/
std::vector<std::string> TypeSystem::get_path_up_tree(const std::string& type) {
auto parent = lookup_type(type)->get_parent();
std::vector<std::string> path = {type};
path.push_back(parent);
auto parent_type = lookup_type(parent);
while (parent_type->has_parent()) {
parent = parent_type->get_parent();
parent_type = lookup_type(parent);
path.push_back(parent);
}
return path;
}
/*!
* Lowest common ancestor of two base types.
*/
std::string TypeSystem::lca_base(const std::string& a, const std::string& b) {
if (a == b) {
return a;
}
auto a_up = get_path_up_tree(a);
auto b_up = get_path_up_tree(b);
int ai = a_up.size() - 1;
int bi = b_up.size() - 1;
std::string* result = nullptr;
while (ai >= 0 && bi >= 0) {
if (a_up.at(ai) == b_up.at(bi)) {
result = &a_up.at(ai);
} else {
break;
}
ai--;
bi--;
}
assert(result);
return *result;
}
/*!
* Lowest common ancestor of two typespecs. Will recursively apply to arguments, if compatible.
* Otherwise arguments are stripped off.
* In a situation like lca("(a b)", "(c d)"), the result will be
* (lca(a, b) lca(b, d)).
*/
TypeSpec TypeSystem::lowest_common_ancestor(const TypeSpec& a, const TypeSpec& b) {
auto result = make_typespec(lca_base(a.base_type(), b.base_type()));
if (!a.m_arguments.empty() && !b.m_arguments.empty() &&
a.m_arguments.size() == b.m_arguments.size()) {
// recursively add arguments
for (size_t i = 0; i < a.m_arguments.size(); i++) {
result.add_arg(lowest_common_ancestor(a.m_arguments.at(i), b.m_arguments.at(i)));
}
}
return result;
}
/*!
* Lowest common ancestor of multiple (or at least one) type.
*/
TypeSpec TypeSystem::lowest_common_ancestor(const std::vector<TypeSpec>& types) {
assert(!types.empty());
if (types.size() == 1) {
return types.front();
}
auto result = lowest_common_ancestor(types.at(0), types.at(1));
for (size_t i = 2; i < types.size(); i++) {
result = lowest_common_ancestor(result, types.at(i));
}
return result;
}

View file

@ -23,6 +23,7 @@ struct DerefInfo {
bool sign_extend = false;
RegKind reg = RegKind::INVALID;
int stride = -1;
int load_size = -1;
TypeSpec result_type;
};
@ -45,8 +46,8 @@ class TypeSystem {
TypeSpec make_inline_array_typespec(const std::string& type);
TypeSpec make_inline_array_typespec(const TypeSpec& type);
Type* lookup_type(const TypeSpec& ts);
Type* lookup_type(const std::string& name);
Type* lookup_type(const TypeSpec& ts) const;
Type* lookup_type(const std::string& name) const;
MethodInfo add_method(Type* type, const std::string& method_name, const TypeSpec& ts);
MethodInfo add_new_method(Type* type, const TypeSpec& ts);
@ -67,6 +68,12 @@ class TypeSystem {
void add_builtin_types();
std::string print_all_type_information() const;
bool typecheck(const TypeSpec& expected,
const TypeSpec& actual,
const std::string& error_source_name = "",
bool print_on_error = true,
bool throw_on_error = true) const;
std::vector<std::string> get_path_up_tree(const std::string& type);
/*!
* Get a type by name and cast to a child class of Type*. Must succeed.
@ -81,19 +88,19 @@ class TypeSystem {
return result;
}
TypeSpec lowest_common_ancestor(const TypeSpec& a, const TypeSpec& b);
TypeSpec lowest_common_ancestor(const std::vector<TypeSpec>& types);
private:
std::string lca_base(const std::string& a, const std::string& b);
bool typecheck_base_types(const std::string& expected, const std::string& actual) const;
int get_size_in_type(const Field& field);
int get_alignment_in_type(const Field& field);
Field lookup_field(const std::string& type_name, const std::string& field_name);
int get_next_method_id(Type* type);
int manual_add_field_to_type(StructureType* type,
const std::string& field_name,
const TypeSpec& field_type,
int offset,
int size,
int alignment);
StructureType* add_builtin_structure(const std::string& parent, const std::string& type_name);
StructureType* add_builtin_structure(const std::string& parent,
const std::string& type_name,
bool boxed = false);
BasicType* add_builtin_basic(const std::string& parent, const std::string& type_name);
ValueType* add_builtin_value_type(const std::string& parent,
const std::string& type_name,
@ -104,7 +111,6 @@ class TypeSystem {
void builtin_structure_inherit(StructureType* st);
std::unordered_map<std::string, std::unique_ptr<Type>> m_types;
std::unordered_map<std::string, Type*> m_global_types;
std::unordered_set<std::string> m_forward_declared_types;
std::vector<std::unique_ptr<Type>> m_old_types;

View file

@ -6,7 +6,7 @@ This document explains the GOAL type system. The GOAL type system supports runt
Some objects have runtime type information, and others don't. Objects which have runtime type information can have their type identified at runtime, and are called "boxed objects". Objects without runtime type information are called "unboxed objects". An unboxed object cannot reliably be detected as a unboxed object - you can't write a function that takes an arbitrary object and tells you if its boxed or not. However, boxed objects can always be recognized as boxed.
All types have a parent type, and all types descend from the parent type `object`, except for the special type `none`. The `none` type doesn't exist in the runtime and is used to represent an invalid value that the compiler should not use. For example, the return type of a function which doesn't return anything is `none`, and attempting to use this value should cause an error.
All types have a parent type, and all types descend from the parent type `object`, except for the special type `none` (and maybe `_type_`, but more on this later). The `none` type doesn't exist in the runtime and is used to represent an invalid value that the compiler should not use. For example, the return type of a function which doesn't return anything is `none`, and attempting to use this value should cause an error.
Here are some important special types:
- `object` - the parent of all types
@ -15,7 +15,7 @@ This document explains the GOAL type system. The GOAL type system supports runt
All types have methods. Objects have access to all of their parents methods, and may override parent methods. All types have these 9 methods:
- `new` - like a constructor, returns a new object. It's not used in all cases, and on all types, and needs more documentation.
- `new` - like a constructor, returns a new object. It's not used in all cases, and on all types, and needs more documentation on when specifically it is used.
- `delete` - basically unused, but like a destructor. Often calls `kfree`, which does nothing.
- `print` - prints a short, one line representation of the object to the `PrintBuffer`
- `inspect` - prints a multi-line description of the object to the `PrintBuffer`. Usually auto-generated by the compiler and prints out the name and value of each field.
@ -25,7 +25,7 @@ This document explains the GOAL type system. The GOAL type system supports runt
- `relocate` - Some GOAL objects will be moved in memory by the kernel as part of the compacting actor heap system. After being moved, the `relocate` method will be called with the offset of the move, and the object should fix up any internal pointers which may point to the old location. It's also called on v2 objects loaded by the linker when they are first loaded into memory.
- `memusage` - Not understood yet, but probably returns how much memory in bytes the object uses. Not supported by all objects.
Usually a method which overrides a parent method will have the same argument and return types. The only exception is `new` methods, which can have different argument/return types from the parent.
Usually a method which overrides a parent method must have the same argument and return types. The only exception is `new` methods, which can have different argument/return types from the parent. (Dee the later section on `_type_` for another exception)
The compiler's implementation for calling a method is:
- Is the type a basic?
@ -118,9 +118,8 @@ There's a single type system library, located in `common/type_system`. It will
The main features are:
- `TypeSystem` stores all type information and provides a convenient way to add new types or request information about existing types.
- `Type` a GOAL Type. A `Type` is identified by a single unique string. Examples: `function`, `string`, `vector3h`.
- `Type` information about a GOAL Type. A "base GOAL type" is identified by a single unique string. Examples: `function`, `string`, `vector3h`.
- `TypeSpec` a way to specify either `Type` or a "compound type". Compound types are used to create types which represent specific function types (function which takes two integer arguments and returns a string), or specific pointer/array types (pointer to an integer). These can be represented as (possibly nested) lists, like `(pointer integer)` or `(function (integer integer) string)`.
- `FunctionSpec` (unimplemented) - contains a `TypeSpec` plus some additional information about a function (like if it is a global function, a method, names of the arguments, etc)
- Type Checking for compiler
- Parsing of type definitions for compiler
- Lowest common ancestor implementation for compiler to figure out return types for branching forms.
@ -187,33 +186,14 @@ Method System
All type definitions should also define all the methods, in the order they appear in the vtable. I suspect GOAL had this as well because the method ordering otherwise seems random, and in some cases impossible to get right unless (at least) the number of methods was specified in the type declaration.
Todo
---------
- [x] Difference between "runtime" and "compile time" types?
- [ ] `inline-array` and `pointer`
- [x] Arrays which aren't `array`s and aren't fields.
- [x] `lookup_field_info` (returning the correct field type for arrays/dynamics, info about how to deref)
- [x] `deref_info`
- [ ] Finish builtin types
- [ ] Tests for...
- [ ] Builtin types
- [ ] Methods
- [ ] Multiple definition checks
- [ ] Deref
- [ ] Array access
- [ ] Field creation
- [ ] Support for `_type_` / method specific stuff. (maybe this should live outside the type system?)
- [ ] Ability to export type in `deftype` form.
- [x] Multiple definition checks
- [ ] Reverse field (offset/deref to fields)
- [ ] Type Checking
- [ ] Function Specs
- [ ] Lowest Common Ancestor
- [x] Document `:inline`, `:dynamic:` and field arrays.
- [x] Document alignment rules
- [ ] Structure type with itself as a field
- [ ] Ability to read a `deftype` form.
- [ ] In the decompiler
- [ ] In the compiler, with the ability to do constant propagation and put things like `(+ 1 2)` or `MY_CONSTANT` as compile-time array size constants by providing a function evaluating an `Object` to an `int`.
- [ ] Bitfield types
-----
- [ ] Kernel types that are built-in
- [ ] Signed/unsigned for a few built-in type fields
- [ ] Tests for field placement logic (probably a full compiler test?)
- [ ] Bitfield types
- [ ] Type redefinition tests (these are a pain and probably useless, might just wait for full compiler tests?)
- [ ] Stuff for decompiler
- [ ] What field is here?
- [ ] Export all deftypes

View file

@ -14,6 +14,7 @@
#include "kboot.h"
#include "kprint.h"
#include "common/symbols.h"
#include "common/goal_constants.h"
namespace {
// turn on printf's for debugging linking issues.

View file

@ -8,6 +8,7 @@
#include <stdarg.h>
#include <stdio.h>
#include "common/goal_constants.h"
#include "common/common_types.h"
#include "kprint.h"
#include "kmachine.h"
@ -880,7 +881,7 @@ s32 format_impl(uint64_t* args) {
if (sym.offset) {
Ptr<Type> type = *sym.cast<Ptr<Type>>();
if (type.offset) {
call_method_of_type(in, type, GOAL_PRINT_FUNC);
call_method_of_type(in, type, GOAL_PRINT_METHOD);
}
} else {
throw std::runtime_error("failed to find symbol in format!");
@ -901,7 +902,7 @@ s32 format_impl(uint64_t* args) {
if (sym.offset) {
Ptr<Type> type = *sym.cast<Ptr<Type>>();
if (type.offset) {
call_method_of_type(in, type, GOAL_INSPECT_FUNC);
call_method_of_type(in, type, GOAL_INSPECT_METHOD);
}
} else {
throw std::runtime_error("failed to find symbol in format!");

View file

@ -18,6 +18,7 @@
#include "klink.h"
#include "common/symbols.h"
#include "common/versions.h"
#include "common/goal_constants.h"
//! Controls link mode when EnableMethodSet = 0, MasterDebug = 1, DiskBoot = 0. Will enable a
//! warning message if EnableMethodSet = 1
@ -986,7 +987,7 @@ u64 print_object(u32 obj) {
} else if ((obj & OFFSET_MASK) == PAIR_OFFSET) {
return print_pair(obj);
} else if ((obj & OFFSET_MASK) == BASIC_OFFSET) {
return call_method_of_type(obj, Ptr<Type>(*Ptr<u32>(obj - 4)), GOAL_PRINT_FUNC);
return call_method_of_type(obj, Ptr<Type>(*Ptr<u32>(obj - 4)), GOAL_PRINT_METHOD);
} else {
cprintf("#<unknown type %d @ #x%x>", obj & OFFSET_MASK, obj);
}
@ -1194,7 +1195,7 @@ u64 copy_structure(u32 it, u32 unknown) {
u64 copy_basic(u32 obj, u32 heap) {
// determine size of basic. We call a method instead of using asize_of_basic in case the type has
// overridden the default asize_of method.
u32 size = call_method_of_type(obj, Ptr<Type>(*Ptr<u32>(obj - BASIC_OFFSET)), GOAL_ASIZE_FUNC);
u32 size = call_method_of_type(obj, Ptr<Type>(*Ptr<u32>(obj - BASIC_OFFSET)), GOAL_ASIZE_METHOD);
u32 result;
if (*Ptr<u32>(heap - 4) == *(s7 + FIX_SYM_SYMBOL_TYPE)) {
@ -1224,7 +1225,8 @@ u64 inspect_object(u32 obj) {
} else if ((obj & OFFSET_MASK) == PAIR_OFFSET) {
return inspect_pair(obj);
} else if ((obj & OFFSET_MASK) == BASIC_OFFSET) {
return call_method_of_type(obj, Ptr<Type>(*Ptr<u32>(obj - BASIC_OFFSET)), GOAL_INSPECT_FUNC);
return call_method_of_type(obj, Ptr<Type>(*Ptr<u32>(obj - BASIC_OFFSET)),
GOAL_INSPECT_METHOD);
} else {
cprintf("#<unknown type %d @ #x%x>", obj & OFFSET_MASK, obj);
}

View file

@ -18,23 +18,12 @@ extern Ptr<u32> SymbolTable2;
extern Ptr<u32> LastSymbol;
constexpr s32 GOAL_MAX_SYMBOLS = 0x2000;
constexpr s32 BINTEGER_OFFSET = 0;
constexpr s32 PAIR_OFFSET = 2;
constexpr s32 BASIC_OFFSET = 4;
constexpr s32 SYM_INFO_OFFSET = 0xff34;
constexpr u32 EMPTY_HASH = 0x8454B6E6;
constexpr u32 OFFSET_MASK = 7;
constexpr u32 CRC_POLY = 0x04c11db7;
constexpr u32 GOAL_NEW_FUNC = 0; // method ID of GOAL new
constexpr u32 GOAL_DEL_FUNC = 1; // method ID of GOAL delete
constexpr u32 GOAL_PRINT_FUNC = 2; // method ID of GOAL print
constexpr u32 GOAL_INSPECT_FUNC = 3; // method ID of GOAL inspect
constexpr u32 GOAL_LENGTH_FUNC = 4; // method ID of GOAL length
constexpr u32 GOAL_ASIZE_FUNC = 5; // method ID of GOAL size
constexpr u32 GOAL_COPY_FUNC = 6; // method ID of GOAL copy
constexpr u32 GOAL_RELOC_FUNC = 7; // method ID of GOAL relocate
constexpr u32 DEFAULT_METHOD_COUNT = 12;
constexpr u32 FALLBACK_UNKNOWN_METHOD_COUNT = 44;

View file

@ -3,6 +3,7 @@ add_subdirectory(goos)
add_subdirectory(listener)
add_subdirectory(emitter)
add_executable(goalc main.cpp)
add_executable(goalc main.cpp
compiler/Compiler.cpp)
target_link_libraries(goalc util goos)
target_link_libraries(goalc util goos type_system)

View file

@ -0,0 +1 @@
#include "Compiler.h"

12
goalc/compiler/Compiler.h Normal file
View file

@ -0,0 +1,12 @@
#ifndef JAK_COMPILER_H
#define JAK_COMPILER_H
#include "common/type_system/TypeSystem.h"
class Compiler {
public:
private:
TypeSystem m_ts;
};
#endif // JAK_COMPILER_H

View file

@ -1,2 +1,2 @@
add_library(listener SHARED
Listener.cpp Deci2Server.cpp Deci2Server.h)
Listener.cpp)

View file

@ -1,264 +0,0 @@
/*!
* @file Deci2Server.cpp
* Basic implementation of a DECI2 server.
* Works with deci2.cpp (sceDeci2) to implement the networking on target
*/
#include <cstdio>
#include <sys/socket.h>
#include <netinet/tcp.h>
#include <unistd.h>
#include <cassert>
#include <utility>
#include "common/listener_common.h"
#include "common/versions.h"
#include "Deci2Server.h"
Deci2Server::Deci2Server(std::function<bool()> shutdown_callback) {
buffer = new char[BUFFER_SIZE];
want_exit = std::move(shutdown_callback);
}
Deci2Server::~Deci2Server() {
// if accept thread is running, kill it
if (accept_thread_running) {
kill_accept_thread = true;
accept_thread.join();
accept_thread_running = false;
}
delete[] buffer;
if (server_fd >= 0) {
close(server_fd);
}
if (new_sock >= 0) {
close(new_sock);
}
}
/*!
* Start waiting for the Listener to connect
*/
bool Deci2Server::init() {
server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd < 0) {
server_fd = -1;
return false;
}
int opt = 1;
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {
printf("[Deci2Server] Failed to setsockopt 1\n");
close(server_fd);
server_fd = -1;
return false;
}
int one = 1;
if (setsockopt(server_fd, SOL_TCP, TCP_NODELAY, &one, sizeof(one))) {
printf("[Deci2Server] Failed to setsockopt 2\n");
close(server_fd);
server_fd = -1;
return false;
}
timeval timeout = {};
timeout.tv_sec = 0;
timeout.tv_usec = 100000;
if (setsockopt(server_fd, SOL_SOCKET, SO_RCVTIMEO, (char*)&timeout, sizeof(timeout)) < 0) {
printf("[Deci2Server] Failed to setsockopt 3\n");
close(server_fd);
server_fd = -1;
return false;
}
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons(DECI2_PORT);
if (bind(server_fd, (sockaddr*)&addr, sizeof(addr)) < 0) {
printf("[Deci2Server] Failed to bind\n");
close(server_fd);
server_fd = -1;
return false;
}
if (listen(server_fd, 0) < 0) {
printf("[Deci2Server] Failed to listen\n");
close(server_fd);
server_fd = -1;
return false;
}
server_initialized = true;
accept_thread_running = true;
kill_accept_thread = false;
accept_thread = std::thread(&Deci2Server::accept_thread_func, this);
return true;
}
/*!
* Return true if the listener is connected.
*/
bool Deci2Server::check_for_listener() {
if (server_connected) {
if (accept_thread_running) {
accept_thread.join();
accept_thread_running = false;
}
return true;
} else {
return false;
}
}
/*!
* Send data from buffer. User must provide appropriate headers.
*/
void Deci2Server::send_data(void* buf, u16 len) {
lock();
if (!server_connected) {
printf("[DECI2] send while not connected, not sending!\n");
} else {
uint16_t prog = 0;
while (prog < len) {
auto wrote = write(new_sock, (char*)(buf) + prog, len - prog);
prog += wrote;
if (!server_connected || want_exit()) {
unlock();
return;
}
}
}
unlock();
}
/*!
* Lock the DECI mutex. Should be done before modifying protocols.
*/
void Deci2Server::lock() {
deci_mutex.lock();
}
/*!
* Unlock the DECI mutex. Should be done after modifying protocols.
*/
void Deci2Server::unlock() {
deci_mutex.unlock();
}
/*!
* Wait for protocols to become ready.
* This avoids the case where we receive messages before protocol handlers are set up.
*/
void Deci2Server::wait_for_protos_ready() {
if (protocols_ready)
return;
std::unique_lock<std::mutex> lk(deci_mutex);
cv.wait(lk, [&] { return protocols_ready; });
}
/*!
* Inform server that protocol handlers are ready.
* Will unblock wait_for_protos_ready and incoming messages will be dispatched to these
* protocols. You can change the protocol handlers, but you should lock the mutex before
* doing so.
*/
void Deci2Server::send_proto_ready(Deci2Driver* drivers, int* driver_count) {
lock();
d2_drivers = drivers;
d2_driver_count = driver_count;
protocols_ready = true;
unlock();
cv.notify_all();
}
void Deci2Server::run() {
int desired_size = (int)sizeof(Deci2Header);
int got = 0;
while (got < desired_size) {
assert(got + desired_size < BUFFER_SIZE);
auto x = read(new_sock, buffer + got, desired_size - got);
if (want_exit()) {
return;
}
got += x > 0 ? x : 0;
}
auto* hdr = (Deci2Header*)(buffer);
printf("[DECI2] Got message:\n");
printf(" %d %d 0x%x %c -> %c\n", hdr->len, hdr->rsvd, hdr->proto, hdr->src, hdr->dst);
hdr->rsvd = got;
// see what protocol we got:
lock();
int handler = -1;
for (int i = 0; i < *d2_driver_count; i++) {
auto& prot = d2_drivers[i];
if (prot.active && prot.protocol) {
if (handler != -1) {
printf("[DECI2] Warning: more than on protocol handler for this message!\n");
}
handler = i;
}
}
if (handler == -1) {
printf("[DECI2] Warning: no handler for this message, ignoring...\n");
unlock();
return;
// throw std::runtime_error("no handler!");
}
auto& driver = d2_drivers[handler];
int sent_to_program = 0;
while (!want_exit() && (hdr->rsvd < hdr->len || sent_to_program < hdr->rsvd)) {
// send what we have to the program
if (sent_to_program < hdr->rsvd) {
// driver.next_recv_size = 0;
// driver.next_recv = nullptr;
driver.recv_buffer = buffer + sent_to_program;
driver.available_to_receive = hdr->rsvd - sent_to_program;
(driver.handler)(DECI2_READ, driver.available_to_receive, driver.opt);
// memcpy(driver.next_recv, buffer + sent_to_program, driver.next_recv_size);
sent_to_program += driver.recv_size;
}
// receive from network
if (hdr->rsvd < hdr->len) {
auto x = read(new_sock, buffer + hdr->rsvd, hdr->len - hdr->rsvd);
if (want_exit()) {
return;
}
got += x > 0 ? x : 0;
hdr->rsvd += got;
}
}
(driver.handler)(DECI2_READDONE, 0, driver.opt);
unlock();
}
/*!
* Background thread for waiting for the listener.
*/
void Deci2Server::accept_thread_func() {
socklen_t l = sizeof(addr);
while (!kill_accept_thread) {
new_sock = accept(server_fd, (sockaddr*)&addr, &l);
if (new_sock >= 0) {
u32 versions[2] = {versions::GOAL_VERSION_MAJOR, versions::GOAL_VERSION_MINOR};
send(new_sock, &versions, 8, 0); // todo, check result?
server_connected = true;
return;
}
}
}

View file

@ -1,53 +0,0 @@
/*!
* @file Deci2Server.h
* Basic implementation of a DECI2 server.
* Works with deci2.cpp (sceDeci2) to implement the networking on target
*/
#ifndef JAK1_DECI2SERVER_H
#define JAK1_DECI2SERVER_H
#include <netinet/in.h>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <functional>
#include "game/system/deci_common.h" // todo, move me!
class Deci2Server {
public:
static constexpr int BUFFER_SIZE = 32 * 1024 * 1024;
Deci2Server(std::function<bool()> shutdown_callback);
~Deci2Server();
bool init();
bool check_for_listener();
void send_data(void* buf, u16 len);
void lock();
void unlock();
void wait_for_protos_ready();
void send_proto_ready(Deci2Driver* drivers, int* driver_count);
void run();
private:
void accept_thread_func();
bool kill_accept_thread = false;
char* buffer = nullptr;
int server_fd;
sockaddr_in addr;
int new_sock;
bool server_initialized = false;
bool accept_thread_running = false;
bool server_connected = false;
std::function<bool()> want_exit;
std::thread accept_thread;
std::condition_variable cv;
bool protocols_ready = false;
std::mutex deci_mutex;
Deci2Driver* d2_drivers = nullptr;
int* d2_driver_count = nullptr;
};
#endif // JAK1_DECI2SERVER_H

View file

@ -4,4 +4,4 @@
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
export NEXT_DIR=$DIR
$DIR/build/test/goalc-test --gtest_color=yes --gtest_brief=1 "$@"
$DIR/build/test/goalc-test --gtest_color=yes "$@"

View file

@ -3,6 +3,7 @@
#include <unordered_set>
#include "gtest/gtest.h"
#include "common/symbols.h"
#include "common/goal_constants.h"
#include "game/kernel/fileio.h"
#include "game/kernel/kboot.h"
#include "game/kernel/kprint.h"

View file

@ -1,6 +1,6 @@
#include "gtest/gtest.h"
#include "goalc/listener/Listener.h"
#include "goalc/listener/Deci2Server.h"
#include "game/system/Deci2Server.h"
using namespace listener;
@ -34,6 +34,11 @@ TEST(Listener, DeciInit) {
*/
TEST(Listener, ListenToNothing) {
Listener l;
if (l.connect_to_target()) {
printf(
"~~~~~~ Test connected to a runtime when there shouldn't be anything running! Check that "
"you don't have gk running in the background!\n");
}
EXPECT_FALSE(l.connect_to_target());
l.disconnect();
}
@ -89,7 +94,7 @@ TEST(Listener, ListenerThenDeci) {
EXPECT_FALSE(s.check_for_listener());
EXPECT_TRUE(l.connect_to_target());
while (!s.check_for_listener()) {
printf("...\n");
// printf("...\n");
}
}
}

View file

@ -3,22 +3,316 @@
#include "third-party/fmt/core.h"
TEST(TypeSystem, Construction) {
// test that we can add all builtin types without any type errors
TypeSystem ts;
ts.add_builtin_types();
fmt::print("{}", ts.print_all_type_information());
// useful for debugging.
// fmt::print("{}", ts.print_all_type_information());
}
TEST(TypeSystem, DefaultMethods) {
TypeSystem ts;
ts.add_builtin_types();
ts.assert_method_id("object", "new", 0);
ts.assert_method_id("object", "delete", 1);
ts.assert_method_id("object", "print", 2);
ts.assert_method_id("object", "inspect", 3);
ts.assert_method_id("object", "length", 4);
ts.assert_method_id("object", "asize-of", 5);
ts.assert_method_id("object", "copy", 6);
ts.assert_method_id("object", "relocate", 7);
ts.assert_method_id("object", "mem-usage", 8);
}
// check that default methods have the right ID's used by the kernel
ts.assert_method_id("object", "new", GOAL_NEW_METHOD);
ts.assert_method_id("object", "delete", GOAL_DEL_METHOD);
ts.assert_method_id("object", "print", GOAL_PRINT_METHOD);
ts.assert_method_id("object", "inspect", GOAL_INSPECT_METHOD);
ts.assert_method_id("object", "length", GOAL_LENGTH_METHOD);
ts.assert_method_id("object", "asize-of", GOAL_ASIZE_METHOD);
ts.assert_method_id("object", "copy", GOAL_COPY_METHOD);
ts.assert_method_id("object", "relocate", GOAL_RELOC_METHOD);
ts.assert_method_id("object", "mem-usage", GOAL_MEMUSAGE_METHOD);
// check that they are inherited.
ts.assert_method_id("function", "new", GOAL_NEW_METHOD);
ts.assert_method_id("function", "delete", GOAL_DEL_METHOD);
ts.assert_method_id("function", "print", GOAL_PRINT_METHOD);
ts.assert_method_id("function", "inspect", GOAL_INSPECT_METHOD);
ts.assert_method_id("function", "length", GOAL_LENGTH_METHOD);
ts.assert_method_id("function", "asize-of", GOAL_ASIZE_METHOD);
ts.assert_method_id("function", "copy", GOAL_COPY_METHOD);
ts.assert_method_id("function", "relocate", GOAL_RELOC_METHOD);
ts.assert_method_id("function", "mem-usage", GOAL_MEMUSAGE_METHOD);
}
TEST(TypeSystem, TypeSpec) {
TypeSystem ts;
ts.add_builtin_types();
// try some simple typespecs
auto string_typespec = ts.make_typespec("string");
auto function_typespec = ts.make_typespec("function");
EXPECT_EQ(string_typespec.print(), "string");
EXPECT_EQ(string_typespec.base_type(), "string");
EXPECT_TRUE(function_typespec == function_typespec);
EXPECT_FALSE(function_typespec == string_typespec);
// try some pointer typespecs
auto pointer_function_typespec = ts.make_pointer_typespec("function");
EXPECT_EQ(pointer_function_typespec.print(), "(pointer function)");
EXPECT_EQ(pointer_function_typespec.get_single_arg(), ts.make_typespec("function"));
EXPECT_EQ(pointer_function_typespec.base_type(), "pointer");
// try some function typespecs
auto test_function_typespec = ts.make_function_typespec({"string", "symbol"}, "integer");
EXPECT_EQ(test_function_typespec.base_type(), "function");
EXPECT_EQ(test_function_typespec.print(), "(function string symbol integer)");
// try the none typespec
EXPECT_EQ(ts.make_typespec("none").base_type(), "none");
}
TEST(TypeSystem, TypeSpecEquality) {
TypeSystem ts;
ts.add_builtin_types();
auto pointer_to_function = ts.make_pointer_typespec("function");
auto ia_to_function = ts.make_inline_array_typespec("function");
auto pointer_to_string = ts.make_pointer_typespec("string");
EXPECT_TRUE(pointer_to_function == pointer_to_function);
EXPECT_FALSE(pointer_to_function == ia_to_function);
EXPECT_FALSE(pointer_to_string == pointer_to_function);
}
TEST(TypeSystem, RuntimeTypes) {
TypeSystem ts;
ts.add_builtin_types();
// pointers and inline arrays should all become simple pointers
EXPECT_EQ(ts.get_runtime_type(ts.make_typespec("pointer")), "pointer");
EXPECT_EQ(ts.get_runtime_type(ts.make_typespec("inline-array")), "pointer");
EXPECT_EQ(ts.get_runtime_type(ts.make_pointer_typespec("function")), "pointer");
EXPECT_EQ(ts.get_runtime_type(ts.make_inline_array_typespec("function")), "pointer");
// functions of any kind should become function
EXPECT_EQ(ts.get_runtime_type(ts.make_function_typespec({"integer", "string"}, "symbol")),
"function");
}
TEST(TypeSystem, ForwardDeclaration) {
TypeSystem ts;
ts.add_builtin_types();
// before forward declaring, lookup and creating a typespec should fail
EXPECT_ANY_THROW(ts.lookup_type("test-type"));
EXPECT_ANY_THROW(ts.make_typespec("test-type"));
// after forward declaring, we should be able to create typespec, but not do a full lookup
ts.forward_declare_type("test-type");
EXPECT_TRUE(ts.make_typespec("test-type").print() == "test-type");
EXPECT_ANY_THROW(ts.lookup_type("test-type"));
}
TEST(TypeSystem, DerefInfoNoLoadInfoOrStride) {
// test the parts of deref info, other than the part where it tells you how to load or the stride.
TypeSystem ts;
ts.add_builtin_types();
// can't dereference a non-pointer
EXPECT_FALSE(ts.get_deref_info(ts.make_typespec("string")).can_deref);
// can't deref a pointer with no type
EXPECT_FALSE(ts.get_deref_info(ts.make_typespec("pointer")).can_deref);
EXPECT_FALSE(ts.get_deref_info(ts.make_typespec("inline-array")).can_deref);
// test pointer to reference type
auto type_spec =
ts.make_pointer_typespec(ts.make_function_typespec({"string", "symbol"}, "int32"));
auto info = ts.get_deref_info(type_spec);
EXPECT_TRUE(info.can_deref);
EXPECT_TRUE(info.mem_deref);
EXPECT_FALSE(info.sign_extend); // it's a memory address being loaded
EXPECT_EQ(info.reg, RegKind::GPR_64);
EXPECT_EQ(info.stride, 4);
EXPECT_EQ(info.result_type.print(), "(function string symbol int32)");
EXPECT_EQ(info.load_size, 4);
// test pointer to value type
type_spec = ts.make_pointer_typespec("int64");
info = ts.get_deref_info(type_spec);
EXPECT_TRUE(info.can_deref);
EXPECT_TRUE(info.mem_deref);
EXPECT_EQ(info.load_size, 8);
EXPECT_EQ(info.stride, 8);
EXPECT_EQ(info.sign_extend, true);
EXPECT_EQ(info.reg, RegKind::GPR_64);
EXPECT_EQ(info.result_type.print(), "int64");
// test inline-array (won't work because type is dynamically sized)
type_spec = ts.make_inline_array_typespec("type");
info = ts.get_deref_info(type_spec);
EXPECT_FALSE(info.can_deref);
// TODO - replace with a better type.
// TODO - maybe block basic or structure from being inline-array-able?
type_spec = ts.make_inline_array_typespec("basic");
auto type = ts.lookup_type("basic");
info = ts.get_deref_info(type_spec);
EXPECT_TRUE(info.can_deref);
EXPECT_FALSE(info.mem_deref);
EXPECT_EQ(info.stride, (type->get_size_in_memory() + 15) & (~15));
EXPECT_EQ(info.result_type.print(), "basic");
EXPECT_EQ(info.load_size, -1);
}
TEST(TypeSystem, AddMethodAndLookupMethod) {
TypeSystem ts;
ts.add_builtin_types();
auto parent_info = ts.add_method(ts.lookup_type("structure"), "test-method-1",
ts.make_function_typespec({"integer"}, "string"));
// when trying to add the same method to a child, should return the parent's method
auto child_info_same = ts.add_method(ts.lookup_type("basic"), "test-method-1",
ts.make_function_typespec({"integer"}, "string"));
EXPECT_EQ(parent_info.id, child_info_same.id);
EXPECT_EQ(parent_info.id, GOAL_MEMUSAGE_METHOD + 1);
// any amount of fiddling with method types should cause an error
EXPECT_ANY_THROW(ts.add_method(ts.lookup_type("basic"), "test-method-1",
ts.make_function_typespec({"integer"}, "integer")));
EXPECT_ANY_THROW(ts.add_method(ts.lookup_type("basic"), "test-method-1",
ts.make_function_typespec({}, "string")));
EXPECT_ANY_THROW(ts.add_method(ts.lookup_type("basic"), "test-method-1",
ts.make_function_typespec({"integer", "string"}, "string")));
EXPECT_ANY_THROW(ts.add_method(ts.lookup_type("basic"), "test-method-1",
ts.make_function_typespec({"string"}, "string")));
ts.add_method(ts.lookup_type("basic"), "test-method-2",
ts.make_function_typespec({"integer"}, "string"));
EXPECT_EQ(parent_info.id, ts.lookup_method("basic", "test-method-1").id);
EXPECT_EQ(parent_info.id, ts.lookup_method("structure", "test-method-1").id);
EXPECT_EQ(parent_info.id + 1, ts.lookup_method("basic", "test-method-2").id);
EXPECT_ANY_THROW(ts.lookup_method("structure", "test-method-2"));
EXPECT_EQ(ts.lookup_method("basic", "test-method-1").defined_in_type, "structure");
EXPECT_EQ(ts.lookup_method("basic", "test-method-1").type.print(), "(function integer string)");
EXPECT_EQ(ts.lookup_method("basic", "test-method-1").name, "test-method-1");
}
TEST(TypeSystem, NewMethod) {
TypeSystem ts;
ts.add_builtin_types();
ts.add_type("test-1", std::make_unique<BasicType>("basic", "test-1"));
ts.add_method(ts.lookup_type("test-1"), "new",
ts.make_function_typespec({"symbol", "string"}, "test-1"));
ts.add_type("test-2", std::make_unique<BasicType>("test-1", "test-2"));
ts.add_method(ts.lookup_type("test-2"), "new",
ts.make_function_typespec({"symbol", "string", "symbol"}, "test-2"));
EXPECT_EQ(ts.lookup_method("test-1", "new").type.print(), "(function symbol string test-1)");
EXPECT_EQ(ts.lookup_method("test-2", "new").type.print(),
"(function symbol string symbol test-2)");
ts.add_type("test-3", std::make_unique<BasicType>("test-1", "test-3"));
EXPECT_EQ(ts.lookup_method("test-3", "new").type.print(), "(function symbol string test-1)");
ts.add_type("test-4", std::make_unique<BasicType>("test-2", "test-4"));
EXPECT_EQ(ts.lookup_method("test-4", "new").type.print(),
"(function symbol string symbol test-2)");
}
TEST(TypeSystem, MethodSubstitute) {
TypeSystem ts;
ts.add_builtin_types();
ts.add_type("test-1", std::make_unique<BasicType>("basic", "test-1"));
ts.add_method(ts.lookup_type("test-1"), "new",
ts.make_function_typespec({"symbol", "string", "_type_"}, "_type_"));
auto final_type = ts.lookup_method("test-1", "new").type.substitute_for_method_call("test-1");
EXPECT_EQ(final_type.print(), "(function symbol string test-1 test-1)");
}
namespace {
bool ts_name_name(TypeSystem& ts, const std::string& ex, const std::string& act) {
return ts.typecheck(ts.make_typespec(ex), ts.make_typespec(act), "", false, false);
}
} // namespace
TEST(TypeSystem, TypeCheck) {
TypeSystem ts;
ts.add_builtin_types();
EXPECT_TRUE(ts_name_name(ts, "none",
"none")); // none - none _shouldn't_ fail (for function return types!)
EXPECT_TRUE(ts_name_name(ts, "object", "object"));
EXPECT_TRUE(ts_name_name(ts, "object", "type"));
EXPECT_TRUE(ts_name_name(ts, "basic", "type"));
EXPECT_FALSE(ts_name_name(ts, "type", "basic"));
EXPECT_TRUE(ts_name_name(ts, "type", "type"));
auto f = ts.make_typespec("function");
auto f_s_n = ts.make_function_typespec({"string"}, "none");
auto f_b_n = ts.make_function_typespec({"basic"}, "none");
// complex
EXPECT_TRUE(ts.typecheck(f, f_s_n));
EXPECT_TRUE(ts.typecheck(f_s_n, f_s_n));
EXPECT_TRUE(ts.typecheck(f_b_n, f_s_n));
EXPECT_FALSE(ts.typecheck(f_s_n, f, "", false, false));
EXPECT_FALSE(ts.typecheck(f_s_n, f_b_n, "", false, false));
// number of parameter mismatch
auto f_s_s_n = ts.make_function_typespec({"string", "string"}, "none");
EXPECT_FALSE(ts.typecheck(f_s_n, f_s_s_n, "", false, false));
EXPECT_FALSE(ts.typecheck(f_s_s_n, f_s_n, "", false, false));
}
TEST(TypeSystem, FieldLookup) {
// note - this test isn't testing the specific needs_deref, type of the returned info. Until more
// stuff is set up that test is kinda useless - it would just be testing against the exact
// implementation of lookup_field_info
TypeSystem ts;
ts.add_builtin_types();
EXPECT_EQ(ts.lookup_field_info("type", "parent").field.offset(), 8);
EXPECT_EQ(ts.lookup_field_info("string", "data").type.print(), "(pointer uint8)");
EXPECT_ANY_THROW(ts.lookup_field_info("type", "not-a-real-field"));
}
TEST(TypeSystem, get_path_up_tree) {
TypeSystem ts;
ts.add_builtin_types();
EXPECT_EQ(ts.get_path_up_tree("type"),
std::vector<std::string>({"type", "basic", "structure", "object"}));
}
TEST(TypeSystem, lca) {
TypeSystem ts;
ts.add_builtin_types();
EXPECT_EQ(
ts.lowest_common_ancestor(ts.make_typespec("string"), ts.make_typespec("basic")).print(),
"basic");
EXPECT_EQ(
ts.lowest_common_ancestor(ts.make_typespec("basic"), ts.make_typespec("string")).print(),
"basic");
EXPECT_EQ(
ts.lowest_common_ancestor(ts.make_typespec("string"), ts.make_typespec("int32")).print(),
"object");
EXPECT_EQ(
ts.lowest_common_ancestor(ts.make_typespec("int32"), ts.make_typespec("string")).print(),
"object");
EXPECT_EQ(
ts.lowest_common_ancestor({ts.make_typespec("int32"), ts.make_typespec("string")}).print(),
"object");
EXPECT_EQ(ts.lowest_common_ancestor({ts.make_typespec("int32")}).print(), "int32");
EXPECT_EQ(ts.lowest_common_ancestor({ts.make_typespec("string"), ts.make_typespec("kheap"),
ts.make_typespec("pointer")})
.print(),
"object");
EXPECT_EQ(ts.lowest_common_ancestor(ts.make_pointer_typespec("int32"),
ts.make_pointer_typespec("string"))
.print(),
"(pointer object)");
}
// TODO - a big test to make sure all the builtin types are what we expect.