diff --git a/common/type_system/TypeSystem.cpp b/common/type_system/TypeSystem.cpp index fd1675ca3..9daa0eea5 100644 --- a/common/type_system/TypeSystem.cpp +++ b/common/type_system/TypeSystem.cpp @@ -9,6 +9,7 @@ TypeSystem::TypeSystem() { // the "none" and "_type_" types are included by default. add_type("none", std::make_unique("none")); add_type("_type_", std::make_unique("_type_")); + add_type("_varargs_", std::make_unique("_varargs_")); } /*! @@ -41,7 +42,7 @@ Type* TypeSystem::add_type(const std::string& name, std::unique_ptr type) // newly defined! // none/object get to skip these checks because they are roots. - if (name != "object" && name != "none" && name != "_type_") { + if (name != "object" && name != "none" && name != "_type_" && name != "_varargs_") { 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()); @@ -234,8 +235,9 @@ Type* TypeSystem::lookup_type(const TypeSpec& ts) const { MethodInfo TypeSystem::add_method(const std::string& type_name, const std::string& method_name, - const TypeSpec& ts) { - return add_method(lookup_type(make_typespec(type_name)), method_name, ts); + const TypeSpec& ts, + bool allow_new_method) { + return add_method(lookup_type(make_typespec(type_name)), method_name, ts, allow_new_method); } /*! @@ -247,7 +249,10 @@ MethodInfo TypeSystem::add_method(const std::string& type_name, * is overriding the "new" method - the TypeSystem will track that because overridden new methods * may have different arguments. */ -MethodInfo TypeSystem::add_method(Type* type, const std::string& method_name, const TypeSpec& ts) { +MethodInfo TypeSystem::add_method(Type* type, + const std::string& method_name, + const TypeSpec& ts, + bool allow_new_method) { if (method_name == "new") { return add_new_method(type, ts); } @@ -285,6 +290,11 @@ MethodInfo TypeSystem::add_method(Type* type, const std::string& method_name, co return existing_info; } else { + if (!allow_new_method) { + fmt::print("[TypeSystem] Attempted to add method {} to type {} but it was not declared.\n", + method_name, type->get_name()); + throw std::runtime_error("illegal method definition"); + } // add a new method! return type->add_method({get_next_method_id(type), method_name, ts, type->get_name()}); } @@ -299,7 +309,7 @@ MethodInfo TypeSystem::add_new_method(Type* type, const TypeSpec& ts) { MethodInfo existing; if (type->get_my_new_method(&existing)) { // it exists! - if (existing.type != ts) { + if (!existing.type.is_compatible_child_method(ts, type->get_name())) { fmt::print( "[TypeSystem] The new method of {} was originally defined as {}, but has been redefined " "as {}\n", @@ -487,7 +497,7 @@ int TypeSystem::add_field_to_type(StructureType* type, // we need to compute the offset ourself! offset = align(type->get_size_in_memory(), field_alignment); } else { - int aligned_offset = align(type->get_size_in_memory(), field_alignment); + int aligned_offset = align(offset, field_alignment); if (offset != aligned_offset) { fmt::print( "[TypeSystem] Tried to overwrite offset of field to be {}, but it is not aligned " @@ -584,7 +594,7 @@ void TypeSystem::add_builtin_types() { // the type. Dynamic structures use new-dynamic-structure, which is used exactly once ever. add_method(structure_type, "new", make_function_typespec({"symbol", "type"}, "structure")); // structure_type is a field-less StructureType, so we have to do this to match the runtime. - structure_type->override_size_in_memory(4); + // structure_type->override_size_in_memory(4); // BASIC // we intentionally don't inherit from structure because structure's size is weird. diff --git a/common/type_system/TypeSystem.h b/common/type_system/TypeSystem.h index 2f154a540..c62074cb5 100644 --- a/common/type_system/TypeSystem.h +++ b/common/type_system/TypeSystem.h @@ -54,8 +54,12 @@ class TypeSystem { MethodInfo add_method(const std::string& type_name, const std::string& method_name, - const TypeSpec& ts); - MethodInfo add_method(Type* type, const std::string& method_name, const TypeSpec& ts); + const TypeSpec& ts, + bool allow_new_method = true); + MethodInfo add_method(Type* type, + const std::string& method_name, + const TypeSpec& ts, + bool allow_new_method = true); MethodInfo add_new_method(Type* type, const TypeSpec& ts); MethodInfo lookup_method(const std::string& type_name, const std::string& method_name); MethodInfo lookup_new_method(const std::string& type_name); diff --git a/doc/decompilation_notes.md b/doc/decompilation_notes.md new file mode 100644 index 000000000..e4879e691 --- /dev/null +++ b/doc/decompilation_notes.md @@ -0,0 +1,277 @@ +# GOAL Operations + +## `div.s` +Suspected source +``` +(/ 1.0 x) +``` +where `x` is in a GPR: +``` + lwc1 f0, L345(fp) ;; first argument prepared first? + mtc1 f1, a0 ;; second argument prepared second? + div.s f0, f0, f1 +``` +Sequence +- Compile first +- First to FPR +- Compile second +- Second to FPR + +## `daddu` +Used for `int` and `uint` addition. + +Two element form: +``` +daddu v0, a0, a1 +``` +is `(+ a0 a1)` - the order in the opcode matches the order in the expression. + +## `daddiu` to get a symbol +``` +daddiu v0, s7, #t +``` +Note for `#t`: `#t` is linked when the code literally has a `#t` in it. Other cases are currently unknown. + +## `dsubu` +Used for `int` and `uint` subtraction. + +## `mult3` (EE `mult`) +Used for `int` multiplication. + +Like `daddu` for opcode ordering: +``` +mult3 v0, a0, a1 +``` +is `(* a0 a1)`. + +## `div` +Used for `int` division. + +``` + div a0, a1 + mflo v0 +``` +is `(/ a0 a1)`. + +and also for `int` mod +``` + div a0, a1 + mfhi v0 +``` +## `or` used to get the value of false +``` +or v0, s7, r0 +``` + +## `or` used as a bitwise or +``` +or v0, a0, a1 +``` +is `(logior a0 a1)` + +## `and` used as a bitwise and +``` +and v0, a0, a1 +``` +is `(logand a0 a1)`. + +``` +(logand #xfffffff0 (+ (ash (-> thing field) 2) 43)) +``` +is +``` + ld v1, L346(fp) ;; first arg to the and + lhu a0, 14(a0) ;; second arg evaluation... + dsll a0, a0, 2 + daddiu a0, a0, 43 + and v0, v1, a0 ;; and result, first, second +``` + +## `nor` used as a bitwise nor +``` +nor v0, a0, a1 +``` +is `(lognor a0 a1)` + +## `xor` used as a bitwise xor +``` +xor v0, a0, a1 +``` +is `(logxor a0 a1)` + +## `nor` used as a logical not +``` +nor v0, a0, r0 +``` +is `(lognot a0)` + + + +# Common "Idioms" + +## `ash` +Variable shift (`ash`) is an inline function +``` + or v1, a0, r0 + bgezl a1, L306 + dsllv v0, v1, a1 + + dsubu a0, r0, a1 + dsrav v0, v1, a0 +L306: +``` + +## `abs` of integer +``` + or v0, a0, r0 + bltzl v0, L302 + dsubu v0, r0, v0 +L302: +``` + +## `min` of integers +``` + or v0, a0, r0 + or v1, a1, r0 + slt a0, v0, v1 + movz v0, v1, a0 +``` + +## `max` of integers +``` + or v0, a0, r0 + or v1, a1, r0 + slt a0, v0, v1 + movn v0, v1, a0 +``` + +# Others + +## Integer constants that are large +A constant of `0xfffffff0` is loaded with `ld` + +## Access value of symbol +Seems to always use `lw`? + +# Control Flow Info + + +## Begin-like forms flush everything always, immediately after compiling +Example in `vector` with flushing: +``` +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +; .function vector3s+! +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +L8: + daddiu sp, sp, -16 + sd fp, 8(sp) + or fp, t9, r0 + daddiu v1, fp, L109 ;; string: "Add 2 vectors3." + lwc1 f0, 0(a1) + lwc1 f1, 0(a2) + add.s f0, f0, f1 + swc1 f0, 0(a0) + lwc1 f0, 4(a1) + lwc1 f1, 4(a2) + add.s f0, f0, f1 + swc1 f0, 4(a0) + lwc1 f0, 8(a1) + lwc1 f1, 8(a2) + add.s f0, f0, f1 + swc1 f0, 8(a0) + or v0, a0, r0 + ld fp, 8(sp) + jr ra + daddiu sp, sp, 16 +``` +The `daddiu v1, fp, L109` loads a `string` into the `v1` register which is never used, immediately after the prologue. This will only happen if the value is flushed. This is very likely a documentation comment that accidentally got included as a string constant. It's unused, so there was likely no consumer of the string that did the `flush` - it was done by the top level evaluation. +``` +(defun vector3s+! (stuff) + "Add 2 vectors3." ;; oops, a string constant instead of a comment. + ... ; rest of the function +) +``` +## Return-From evaluates to 0 bug +We would expect the value of `(return-from #f x)` to be nothing, as there's no possible way to use it. However, GOAL seems to have a small bug where `(return-from #f x)` always attempts to evaluate to 0. This would be like implementing it as: +```lisp +(set! retrun-reg return-value) +(goto end-of-function) +0 ;; oops +``` +by accident. + +Example in GOAL: + +``` +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +; .function basic-type? (in gcommon) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +L285: + lwu v1, -4(a0) + lw a0, object(s7) +L286: + bne v1, a1, L287 + or a2, s7, r0 + ; (return-from #f #t) starts here + daddiu v1, s7, #t ; compile/flush the #t + or v0, v1, r0 ; move to return register + beq r0, r0, L288 ; branch to end + sll r0, r0, 0 ; branch delay slot (usual filler) + + or v1, r0, r0 ; unreachable loading of 0 into a register. +L287: + lwu v1, 4(v1) + bne v1, a0, L286 + sll r0, r0, 0 + + or v0, s7, r0 +L288: + jr ra + daddu sp, sp, r0 + + sll r0, r0, 0 + sll r0, r0, 0 +``` + +## Unused else case returning false in cond +From `delete!` in gcommon. +``` + beq a2, a0, L222 ; (if (= a2 a0) only-one-case) + or a0, s7, r0 ; a0 is unused return value of if + + lw a0, 2(a2) ; (set! (cdr v1) (cdr a2)), will evaluate to (cdr a2) which is stored in a0 + sw a0, 2(v1) +L222: ; a0 = #f or a0 = (cdr a2) depending on branch taken, but it's totally unused! + or v0, a1, r0 ; return a1 + jr ra + daddu sp, sp, r0 +``` +Also note that all cases stored their result in `a0`, even though nothing uses the result. + +## Function Calls evaluate arguments in order: +``` + lw t9, format(s7) ;; head of function + daddiu a0, s7, #t ;; first arg + daddiu a1, fp, L344 ;; second arg + sllv a2, gp, r0 ;; third arg + dsra32 a3, gp, 0 ;; fourth arg + pcpyud v1, gp, r0 + sllv t0, v1, r0 ;; fifth arg + pcpyud v1, gp, r0 + dsra32 t1, v1, 0 ;; sixth arg + por t2, gp, r0 ;; seventh arg + jalr ra, t9 + sll v0, ra, 0 +``` +also an example of lack of common subexpression elimination on the `pcpyud v1, gp, r0`s. + +### A second example with register type conversions: +``` + lw t9, format(s7) ;; function + daddiu a0, s7, #t ;; first arg + daddiu a1, fp, L343 ;; second arg + lwc1 f0, 0(gp) ;; compile and flush third arg + mfc1 a2, f0 ;; move to correct reg type + jalr ra, t9 + sll v0, ra, 0 +``` \ No newline at end of file diff --git a/doc/goal_doc.md b/doc/goal_doc.md new file mode 100644 index 000000000..8518c55a1 --- /dev/null +++ b/doc/goal_doc.md @@ -0,0 +1,1001 @@ +# OpenGOAL Document +This is the main documentation for the OpenGOAL language. It explains the syntax of OpenGOAL programs and also how OpenGOAL can be decompiled from original GOAL. It's broken up into three sections: + +1. Compiler forms, which are things built-in to the compiler and have names. Like `+` or `deftype`. +2. Important Syntax Macros, which are really important features of the languages that are implemented as macros, like `if` and `defun`. +3. Compiler features, which are things that are built-in to the compiler but don't have explicit names. Like calling functions or the rules for how names are scoped. +4. Built-in types + +Each feature will have a description, explanation of the syntax, a guess at how often it's used, an example or two in OpenGOAL, and an example or two in MIPS GOAL. There will also be details on the order of evaluation that is useful for the decompiler but can mostly be ignored for normal programming in OpenGAL. + +The syntax description uses these rules: +- Something `[in-brackets]` is optional and can be left out. +- Something like `[:type type-name]` means there is an optional named argument. It can be used like `:type type-name`, replacing `type-name` with what you want, or left out entirely. +- When there are multiple choices, they are separated by `|`. Example: `#t|#f` is either `#t` or `#f`. +- A `...` means more of the thing before can be included. Example `(f arg...)` can have multiple arguments. + +When talking about ordering things, GOAL code fragments can be `compile`d and `flush`ed as two separate things. For the most part, everything is done during compilation, like calling functions. But consider `(set! (-> my-object value) x)`. We don't actually want the value of `(-> my-object value)`, we want to set its value. The `compile` stage gets us description of how to read/write a thing (if possible), and the `flush` stage gets us an actual value in a register. This can basically be ignored outside of the decompiler. + + +# Compiler Forms - Block Related + +## `begin` +Execute forms in order. +```lisp +(begin form...) +``` +A `begin` form is just a list of other forms which are executed in order. A `begin` form evaluates to the value of the last form. + +Example: +```lisp +(begin + (print "hello ") + (print "world!") + 7 + ) +``` +will print `hello world!` and the value of the entire form is `7`. + +In `begin` and similar "do everything in the list" forms, each form is `compile`d then `flush`ed. + +The `begin` form is used a lot in macros, but not that much in code. It's generally used when you want to execute multiple things, but fit it into a single form. + + +## `block` +A `block` form is pretty similar to a `begin`, except the `block` has a name. You can then "return" from the block early with `return-from`. +```lisp +(block block-name form...) +``` + +Example: +```lisp +(block my-block + (print "hello ") + (return-from my-block 7) + (print "world") + "test" + ) +``` +will print `hello ` only and the value of the entire `block` form is `7`. The type of the `block` is the most specific type that describes all of the possible return values from any `return-from` or from reaching the end (even if its technically not possible to reach the end). In the case above, the possible return types are `int` and `string`, so the return type of the whole block is `object`, the lowest common ancestor type of `int` and `string`. + +Block is used rarely, and possibly almost never? + +## `return-from` +Exit a `block` or function early. +```lisp +(return-from block-name value) +``` + +Looks up the block and exits from it with the value. You can exit out nested blocks. If you are enclosed in multiple blocks with the same name, exits from the inner-most one with a matching name. Everything in a function is wrapped in a block named `#f`, so you can use `(return-from #f x)` to return early from a function with `x`. Unlike returning from a block, using `return-from` to exit a function currently does _not_ modify the return type of your function and does _not_ check the type of what you return. + +Example +```lisp +(if (is-a-match? x) + (return-from #f x) + ) +``` +if `x` is a match, returns `x` from the function (not shown) immediately. + +The `return-from` form is very rarely used to return from a block, but sometimes used to return from a function. + +## `label` +Create a named label for `goto` or `goto-when`. +```lisp +(label label-name) +``` +The label spaces are per-function and not nested. You can't jump from function to function. You can't jump in or out of functions which end up getting inlined. You can't jump in or out of an anonymous lambda function. You can jump in and out of `let`s. + +See `goto` for an example. + +Labels are used extremely rarely. Usually only in inline assembly and part of macros for `while` loops and similar. + +## `goto` +Jump to a label. +```lisp +(goto label-name) +``` +The label must be in the current label space. You can jump forward or backward. + +Example: +```lisp +(if skip-code? + (goto end) + ) + +;; code here runs only if skip-code is false. + +(label end) +``` + +The `goto` form used very rarely outside of macros and inline assembly. Try to avoid using `goto`. + +# Compiler Forms - Compiler Control +These forms are used to control the GOAL compiler, and are usually entered at the GOAL REPL, or as part of a macro that's executed at the GOAL REPL. These shouldn't really be used in GOAL source code. + +## `:exit` +This causes the compiler to exit after the current REPL command is over. +```lisp +(:exit) +``` +If the listener is connected, it sends a reset command to reboot the target so it is ready for the next compiler connection. + +There's a useful macro `(e)` to exit with less typing. Obviously this shouldn't be used in game code. + +## `seval` +Execute GOOS code. +```lisp +(seval form...) +``` +Evaluates the forms in the GOOS macro language. The result is not returned in any way, so it's only useful for getting side effects. It's not really used other than to bootstrap some GOAL macros for creating macros. + +## `asm-file` +Compile a file. +```lisp +(asm-file "file-name" [:color] [:write] [:load] [:no-code]) +``` +This runs the compiler on a given file. The file path is relative to the `jak-project` folder. These are the options: +- `:color`: run register allocation and code generation. Can be omitted if you don't want actually generate code. Usually you want this option. +- `:write`: write the object file to the `data` folder. You must also have `:color` on. You must do this to include this file in a DGO. +- `:load`: send the object file to the target with the listener. Requires `:color` but not `:write`. There may be issues with `:load`ing very large object files. + +To reduce typing, there are some useful macros: +- `(m "filename")` is "make" and does a `:color` and `:write`. +- `(ml "filename")` is "make and load" and does a `:color` and `:write` and `:load`. This effectively replaces the previous version of file in the currently running game with the one you just compiled, and is a super useful tool for quick debugging/iterating. +- `(build-game)` does `m` on all game files and rebuilds DGOs +- `(blg)` (build and load game) does `build-game` then sends commands to load KERNEL and GAME CGOs. The load is done through DGO loading, not `:load`ing individual object files. + +## `lt` +Listen to target. +```lisp +(lt ["ip address"] [port-number]) +``` +This causes the compiler to connect to the target/runtime. Usually it's just run as `(lt)`, as the default IP is `127.0.0.1` and the default port is the right one. If it works, you should see something like this: +``` +[Listener] Socket connected established! (took 0 tries). Waiting for version... +Got version 2.6 OK! +[OUTPUT] reset #x147d24 +``` +The `OUTPUT` message is a pending message from the runtime saying that it has reset and the location of the symbol table. + +Note: `lt` is actually a macro. Use these target control macros over the direct compiler forms (currently undocumented) whenever possible, as running the compiler forms in the wrong order can leave the target/compiler in a strange state. + +## `r` +Reset the target. +```lisp +(r) +``` +Regardless of the current state, attempt to reset the target and reconnect. + +Note: `r` is actually a macro. Use it over the (currently undocumented) compiler forms. + +## `shutdown-target` +If the target is connected, shut it down. +```lisp +(shutdown-target) +``` +The target will print +``` +GOAL Runtime Shutdown (code 2) +``` +when it shuts down. + +## `:status` +Ping the target. +```lisp +(:status) +``` +Send a ping-like message to the target. Requires the target to be connected. If successful, prints nothing. Will time-out if the GOAL kernel or code dispatched by the kernel is stuck in an infinite loop. Unlikely to be used often. + +## `gs` +Enter a GOOS REPL. +```lisp +(gs) +``` +Example: +``` +g> (gs) +goos> (+ 1 2 3) +6 +goos> (exit) +() +``` +mainly useful for debugging/trying things out in GOOS. The GOOS REPL shares its environment with the GOOS interpreter used by the compiler, so you can inspect/modify things for debugging with this. Likely not used much outside of initial debugging. + +## `set-config!` +```lisp +(set-config! config-name config-value) +``` +Used to set compiler configuration. This is mainly for debugging the compiler and enabling print statements. There is a `(db)` macro which sets all the configuration options for the compiler to print as much debugging info as possible. Not used often. + +## `in-package` +```lisp +(in-package stuff...) +``` +The compiler ignores this. GOAL files evidently start with this for some reason related to emacs. + +## `build-dgos` +```lisp +(build-dgos "path to dgos description file") +``` +Builds all the DGO files described in the DGO description file. See `goal_src/builds/dgos.txt` for an example. This just packs existing things into DGOs - you must have already built all the dependencies. + +# Compiler Forms - Control Flow + +## GOAL "Two Element" Conditions +These are `!=`, `eq?`, `neq?`, `=`, `>`, `<`, `>=`, `<=`. The default is to compare the two objects as unsigned 64-bit integers, unless a special case is hit. The special case is determined by the type of the __first__ argument: +- Floating point: if second argument is a number, convert to floating point. Use floating point comparisons +- Binteger: convert first argument to integer. If second argument is a number, convert to integer. Use signed integer comparisons +- Integer: If second argument is a number, convert to integer. Use signed integer comparison +- Unsigned Integer: If second argument is a number, convert to integer. Use unsigned integer comparison + +Note that comparing structures just checks if they refer to the same memory, not if the contents are the same. + +Currently there is absolutely no type checking done on comparisons, which makes it quite easy to get things wrong. + +When a two-element condition is used as a condition for an `if`, `cond`, `while`, `goto-when`, or any other branching condition, there is an optimization that avoids computing a GOAL boolean, then checking that GOAL boolean as a branch condition. Instead the comparison operation directly sets the x86-64 flags for a jump. Original GOAL has a similar optimization. + +There are a lot of unknown details +- What order are things evaluated / converted? +- How does the order work when there's a `not`? +- Type Checking is probably needed. +- Currently `=` and `eq?` are exactly the same. Likely `=` should only work on numbers and `eq?` should work on references? + +## `not` +```lisp +(not value) +``` +If value is `#f`, return `#t`. Otherwise return `#f`. + +Using `not` on a "two element condition" will be optimized to modifying that condition. + +## `when-goto` +```lisp +(when-goto value-or-condition destination) +``` +Examples: +```lisp +(when-goto (> x y) some-label) +(when-goto (is-player-dead?) some-label) +``` +Jump to `destination` if the condition is truthy (not `#f`). This ends up generating much better code than `(if condition (goto x))`. + +Like normal `goto`, this isn't used much outside of macros. + +## `cond` +A normal Lisp/Scheme `cond`. +```lisp +(cond (test clause...)...) +``` +Evaluates `test`s until one is truthy. Then evaluates all `clause`s in that case and returns the value of the last one. Optionally there can be a case with `else` as the test which runs if no other cases match. This must be the last case. + +If there is no `else`, and no cases match, return `#f`. (is it always `#f`?) + +The return type is the lowest common ancestor type of all cases. Note that for a `cond` without `else`, the possible return value of `#f` is __not__ considered when determining the return type. So: +```lisp +(print-type + (cond (test1 "hi") (test2 "bye")) + ) +``` +will print `[TYPE] string`, even though `cond` may return `#f`. + +This behavior seems correct if the return value is a `basic`, but less clear if the return value is supposed to be a number. + +Note that GOAL will load `#f` into a register for an else case even if the value of the `cond` is never used. Also, even if the value of the `cond` is never used, all cases will store their result into a common `cond` return-register. This is incredibly helpful for the decompiler! + +## `if` +A normal Lisp/Scheme `if`. +```lisp +(if test true-case [false-case]) +``` +The value of the entire statement is the value of the taken case. The false case can be left out. If the condition evaluates to false and the false case is left out, `#f` is returned. The return type follows the same logic as `cond`, as `if` is a macro using `cond`. + +## `when` +A normal Lisp/Scheme `when`. +```lisp +(when test forms...) +``` +If `test` is truthy, evaluate all forms and return the value of the last one. If `test` isn't true, return `#f`. + +## `unless` +A normal Lisp/Scheme `unless`. +```lisp +(unless test forms...) +``` +If `test` is false, evaluate all forms and return the value of the last one. If `test` isn't false, return `#f`. + +# Compiler Forms - Defintion + + + +## `set!` +Set a value! +```lisp +(set! place value) +``` + +Sets `place` to `value`. The `place` can be once of: +- A lexical variable defined by `let`, or an argument of a function +- A global symbol +- A `car` or `cdr` of a pair +- A field of an object +- A dereferenced pointer +- An element in an array +- A bitfield within any of the above (not yet implemented) + +## `define` +Define a global symbol +```lisp +(define symbol-name value) +``` +Kind of like `set!`, but works exclusively on global symbols. Also sets the type of the global symbol. If the global symbol already has a type, and the `define` tries to change it, there will be an error or warning. (to be determined) + +## `define-extern` +Inform the compiler about the type of a global symbol +```lisp +(define-extern symbol-name type) +``` +Useful to forward declare functions or to let the compiler know about things in the C Kernel. See `goal-externs.gc` for where all the C Kernel stuff is declared. + +# Compiler Forms - Functions + +## `defun` +Define a new named global function! +```lisp +(defun name arg-list ["doc-string"] body...) +arg-list = (arg...) +arg = (arg-name arg-type)|arg-name +``` +Example: +``` +(defun 1/ ((x float)) + "Compute 1.0 / x" + (/ 1.0 x) + ) +``` +Define a new function with the given name. Note that until the `defun` itself executes, the function __cannot be used!__. So don't call functions before the `defun`. This is unlike C, where you can forward declare a function and use it before the actual definition. + +There is an optional docstring. Currently the docstring is just thrown away but in the future we could save them and and generate documentation or something. + +## `defmethod` +Define a method! +```lisp +(defmethod method-name type-name arg-list ["doc-string"] body...) +``` +In many ways is similar to `defun`. See section on methods for more details. + +You can only `defmethod` if one of these two is true: +- You are overriding a parent method +- You have declared this method in the type's `deftype` + +Like `defun`, the method only exists after the `defmethod` executes. Before that the method may be undefined or the parent's method. + +## `inline` +Make this function call inlined. +``` +(inline function-name) +``` +Example: inline call `my-function` +``` +((inline my-funciton) my-arg) +``` +Attempts to make the function call inline. If it is not possible, it will throw an error. You can't save the value of `(inline my-function)`, only use it immediately in a function call. You must provide a name of a global function as the function, not a function pointer. + +Methods cannot be inlined. (Maybe new methods can be in original GOAL?) + +## `lambda` +Create a GOAL Lambda. +```lisp +(lambda [:name name] [:inline-only #f|#t] [:segment main|debug] arg-list body-form..) +arg-list = (arg...) +arg = (arg-name arg-type)|arg-name +``` + +The `:name` option is only used for debugging the compiler - it cannot be accessed or referenced outside of internal compiler debugging features. It should be ignored unless you are debugging the compiler. + +The `:inline-only` flag defaults to `#f`, but can be set to `#t` to force the compiler to not generate x86-64 code for the function. In this case, the lambda cannot be used as a function pointer/GOAL `function` object. This is used in the `let` macro and not really useful for general programming. If the flag is `#f` (default value), it will generate and store an x86-64 function, regardless of if the function is ever called. + +The `:segment` option defaults to `main`, unless the `lambda` is defined in a function in the `debug` segment, in which case it defaults to `debug`. It can be overridden by this flag. This picks the segment of the object file where the function will be stored. + +The arguments default to type of `object` if no type is provided. In the case where a lambda is used immediately (a `let`) the compiler will propagate more specific types. This is why you don't have to give types when using `let`. + +A GOAL lambda has three purposes: +1. Generate a function as a "real" x86-64 function. +2. Save the code so the function can later be inlined +3. Immediately use the lambda in the head of a list as part of a `let` or `let*` macro + +__Example of 1:__ +``` +(sort my-list (lambda ((x int) (y int) (< x y)))) +``` +We create a real x86-64 function to pass to the `sort` function. In this case you should specify types - the default of `object` is not suitable. + +The other two cases are handled by `let` and `defun` macros, and shouldn't show up in other places, so they are left out. + +## `declare` +Set options for a function or method +```lisp +(declare [(inline)] [(allow-inline)] [(disallow-inline)] [(asm-func)]) +``` +If used, this should be the first thing inside of a `defun`/`defmethod`. Don't use it anywhere else. +Example: +```lisp +(defun my-function () + (declare (inline)) + ... + ) +``` + +- `inline` means "inline whenever possible". See function inlining section for why inlining may be impossible in some cases. +- `allow-inline` or `disallow-inline`. You can control if inlining is allowed, though it is not clear why I thought this would be useful. Currently the default is to allow always. +- `asm-func` currently does nothing. Eventually should disable generating prologues/epilogues. Use if you want an entirely asm function. Used very rarely and probably only in the GOAL kernel. + +This form will probably get more options in the future. + +# Compiler Forms - Macro Forms + +## `#cond` +This is like an `#ifdef`, but the language for the conditions is GOOS and it's like a `cond`. +```lisp +(#cond (test clause...)...) +``` +The `#cond` is executed at compile time. The cases which don't match aren't compiled. There are `#when` and `#unless` macros that work like you would expect. + +Example: +```lisp +(#when PRINT_DEBUG_INFO + (format #t "debug info~%") + ) +``` + +## `quote` / `'` +The reader will expand `'thing` to `(quote thing)` + +```lisp +(quote thing) +``` +Currently `quote` supports: +- `'a-symbol` - will return the symbol `a-symbol` +- `'()` - will return the empty list. + +In the future, it should support quoted lists of static data. + +## `defglobalconstant` +Define a GOOS and GOAL constant. +```lisp +(defconstant name value) +``` + +The constant is available in both GOOS and GOAL. You can use it in `#cond` expressions (GOOS) and in GOAL code. The constant inserts the `value` as an s-expression in the code. This means it's a "code" constant, and doesn't have a type. + +There are a few restrictions: +- Currently constants do not work in `deftype`. (this should be fixed eventually) +- Constants may not work in places where the compiler is expecting a symbol which isn't actually compiled. So `(+ MY_CONSTANT 2)` is fine, but `(defun MY_CONSANT () ...)` isn't. Don't use constants for type names, function names, or symbol names. It is fine to have a constant with a value of a symbol, but don't `(define MY_CONSTANT)` or `(set! MY_CONSTANT)` or `(new MY_CONSTANT)` or things like that. +- Don't use constants with `method` form. +- You can modify constants created with `defglobalconstant` in GOOS (effect won't be seen in GOAL) with `(set!)` +- There is no warning if one `defglobalconstant` changes the value of an exisiting `defglobalconstant` + +In general constants should have `UPPERCASE` names otherwise things get very confusing when there are name conflicts. + +The recommendation is to use constants for things like numbers or expressions like `(* NUM_THINGS SIZE_OF_EACH_THING)`. + +## `mlet` +Scoped constants in GOAL. Syntax is like a `let`. This feature has all the restrictions of `defglobalconstant`. +Avoid using `mlet` becuase it's confusing and not useful. +```lisp +(mlet ((constant-name constant-value)...) + body... + ) +``` + +Example: +```lisp +(mlet ((NUM-THINGS 12) + (THING-NAME "test")) + ; in here, NUM-THING and THING-NAME are constants + ) +``` + +# Compiler Forms - Math Forms + +Math forms will look at the type of the first argument to determine the "mode". So if you have `(+ 1 1.2)`, it will convert the `1.2` to an integer, do the add, and return an integer. + +## `+` +Addition. Can take 1 or more arguments. `(+ 1)` will give you `1`, like you'd expect. +```lisp +(+ form...) +``` +Works on integers and floats. Integer add is 64-bit and wrapping. + +## `-` +Subtraction or negative. Can take 1 or more arguments. +```lisp +(- form...) +``` +`(- 1)` gives you `-1`, but `(- 1 3)` gives you `-2`, which may be slightly confusing at first. Works on integers and floats. Integer subtract is 64-bit and wrapping. + +## `*` +Multiplication. Can take 1 or more arguments. +```lisp +(* form...) +``` +`(* 7)` will become `7`, as you'd expect. Works on integers and floats. The exact behavior for integers needs to be explored more because the EE is weird. GOAL generates different code for multiplication of signed vs. unsigned integers. + +## `/` +Division. Takes exactly two arguments +```lisp +(/ dividend divisor) +``` +Works on floats and integers. The exact behavior for integers needs to be explored more because the EE is weird. GOAL generates different code for division of signed vs. unsigned integers. + +## `mod` +Modulo operation. Takes exactly two arguments. +```lisp +(/ dividend divisor) +``` +Works on integers only. The exact behavior needs to be explored because the EE is weird. It is unknown if the same code is generate for signed/unsigned mod. + +## `slhv`, `sarv`, `shrv` +```lisp +(shlv value shift-amount) +``` +The exact behavior of GOAL shifts isn't fully understood, so these are temporary wrappers around x86-64 variable shift instructions. +- `shlv` shift left variable +- `sarv` shift arithmetic right variable +- `shrv` shift logical right variable +64-bit operation. + +## `logand` +Bitwise And +```lisp +(logand a b) +``` +64-bit operation. + +## `logior` +Bitwise Inclusive Or (normal Or) +```lisp +(logior a b) +``` +64-bit operation. + +## `logxor` +Bitwise Exclusive Or (Xor +```lisp +(logxor a b) +``` +64-bit operation. + +## `lognot` +Bitwise Not +```lisp +(lognot a) +``` +64-bit operation. + +# Compiler Forms - Type Forms + +## `defmethod` + +## `deftype` + + +## `method` +Get a method of a type or an object. +__Warning - I will probably change this in the future.__ +``` +(method type method-name) +(method object method-name) +``` + +The first form of this takes a type name and method name and returns a GOAL `function` for this method. For example: +``` +(method string inspect) +``` +will return the `inspect` method of `string`. + +The second form of this takes an object and gets the method from it. If the object has runtime type information, will consult the method table to get a possibly more specific method than what is available at compile time. This uses the same lookup logic as method calling - see the section on method calls for more information. + +## `car` and `cdr` +Get element from pair +```lisp +(car some-pair) +(cdr some-pair) +``` + +The type of the result is always `object`, as pairs can hold any `object`. The type-check for `car` and `cdr` is relaxed - it allows it to be applied to any `pair` or `object`. The reason for allowing `object` is so you can write `(car (car x))` instead of `(car (the pair (car x)))`. However, if the argument to `car` is not a `pair`, you will get garbage or a crash. + +## `new` +See section on creating new GOAL objects + +## `print-type` +Print the type of some GOAL expression at compile time. +```lisp +(print-type form) +``` +This is mainly used to debug the compiler or figure out why some code is failing a type check. The thing inside is actually executed at runtime. Example: +```lisp +(print-type "apples") ;; [TYPE] string +(print-type (+ 12 1.2)) ;; [TYPE] int +(print-type (the float 12)) ;; [TYPE] float +``` + +## `the` +Convert between types, doing the expected thing for number conversions. +```lisp +(the type thing) +``` + +If the `type` and the `thing` are both numbers, it will automatically convert between the different numeric types as needed. In all other cases, it does a dangerous `reinterpret_cast`. cppreference.com explains this idea clearly with "Converts between types by reinterpreting the underlying bit pattern." + +If the `thing` is a number: +- If `type` is `binteger`, convert the number to a `binteger` +- If `type` is `int` or `uint` (or some user-defined child type of these), convert the number to an integer. +- If `type` is `float`, conver the number to a `float` + +In all other cases, directly use the 64-bit value in the reigster as the value of the desired `type`. + + +Example of number conversions: +``` +(the binteger 12) ;; boxed integer for 12 +(the float 1) ;; floating point 1.0 +(the int 1.234) ;; signed integer 1 +``` + +Examples of common casts: +``` +(the uint 1) ;; unsigned integer, doesn't do any modification if input is already an int. +(the (pointer uint8) x) ;; like C (uint8_t*)x +(the string (car x)) ;; if you know (car x) is a string, do this. +``` + +Examples of dangerous things you can do but probably shouldn't: +``` +(the string 1.234) ;; take the binary representation of 1.234 and treat it as a GOAL memory address to string data. +(the float 'bean) ;; take the GOAL memory address of the "bean" symbol and treat it as the binary representation of a float. +``` + +There are some weird edge cases with `the` that are worth mentioning, if you try to set the value of something you've used `the` on. In the future the compiler should block you from doing something bad, but this is not yet implemented. + +``` +(set! (-> (the (pointer uint8) x)) 1) ;; OK, not casting the actual value +(set! (the int (-> obj x)) 1) ;; NOT OK. Int is a value type +;; Will actually work if x is already an int/uint, and with non-numeric values types like pointer. Avoid just to be safe. +(set! (the string (-> obj x)) "test") ;; OK, string is a basic, which is a reference type. +``` + +This becomes more clear if we look at the C equivalent: +``` +*(uint8_t*)(a->x) = 1; // makes sense + +(int)(a->x) = 1; // doesn't make sense + +*(thing*)(a->x) = thing(); // makes sense +``` + + +## `the-as` +Convert between types always using `reinterpret_cast` +```lisp +(the-as type thing) +``` +cppreference.com explains this idea clearly with "Converts between types by reinterpreting the underlying bit pattern." + +The recommendation is to prefer `the` in almost all cases, unless you want to set an exact bit pattern of a `float`, or want to examine the bits in a `float`. + +Example: +``` +(the-as int 1.234) ;; result happens to be 1067316150 +``` + +None of the edge cases of `the` apply to `the-as`. + + +## Pointer Math +Not implemented well yet. + +# Compiler Forms - Unsorted + +## `let` +```lisp +(let ((var-name value)...) body...) +``` +Define local variables. The variables are automatically assigned a type, like C++ `auto`. + +Example: +```lisp +(let ((count-1 (+ a b)) ;; count_1 = a + b + (count-2 (+ c d))) ;; count_2 = c + d + (if (count > 10) + ;; print a message if count > 10. + (format #t "count = ~D, ~D~%" count-1 count-2) + ) + ) +``` + +Note, if you define multiple variables in a `let`, you cannot refer to variables defined previously in the same `let`. In the example above, `count-2`'s value couldn't be defined in terms of `count-1` for example. See `let*`. + +## `let*` +```lisp +(let ((var-name value)...) body...) +``` + +Define local variables. If you define multiple in a single `let*`, you can refer to previous variables. + +Example: +```lisp +(let* ((count-1 (+ a b)) + (count-2 (+ count-1 d))) ;; okay because we used let* + (if (count > 10) + ;; print a message if count > 10. + (format #t "count = ~D, ~D~%" count-1 count-2) + ) + ) +``` + +## Things related to enums +Not yet implemented + +## `defmacro` + +## Loop forms + +## `&` + +## `->` + +## Type + +## Compile-Time Size stuff + +## `object-new` + + +# Compiler Features + +## Compiling a list +When the compiler encounters a list like `(a b c)` it attempts to parse in multiple ways in this order: +1. A compiler form +2. A GOOS macro +3. An enum (not yet implemented) +4. A function or method call + +## Compiling an integer +Integers can be specified as +- decimal: `1` or `-1234` (range of `INT64_MIN` to `INT64_MAX`) +- hex: `#x123`, `#xbeef` (range of `0` to `UINT64_MAX`) +- binary: `#b101010` (range of `0` to `UINT64_MAX`) + +All integers are converted to the signed "integer in variable" type called `int`, regardless of how they are specified. +Integer "constant"s are not stored in memory but instead are generated by code, so there's no way to modify them. + +## Compiling a string +A string constant can be specified by just putting it in quotes. Like `"this is a string constant"`. +There is an escape code `\` for string: +- `\n` newline +- `\t` tab character +- `\\` the `\` character +- `\"` the `"` character +- Any other character following a `\` is an error. + +OpenGOAL stores strings in the same segment of the function which uses the string. I believe GOAL does the same. + +In GOAL, string constants are pooled per object file (or perhaps per segment)- if the same string appears twice, it is only included once. OpenGOAL currently does not pool strings. If any code is found that modifies a string "constant", or if repeated strings take up too much memory, string pooling will be added. + +For now I will assume that string constants are never modified. + + +## Compiling a float +A floating point constant is distinguished from an integer by a decimal point. Leading/trailing zeros are optional. Examples of floats: `1.0, 1., .1, -.1, -0.2`. Floats are stored in memory, so it may be possible to modify a float constant. For now I will assume that float constants are never modified. It is unknown if they are pooled like strings. + +Trivia: Jak 2 realized that it's faster to store floats inline in the code. + +## Compiling a symbol +A `symbol` appearing in code is compiled by trying each of these in the following order +1. Is it `none`? (see section on `none`) +2. Try `mlet` symbols +3. Try "lexical" variables (defined in `let`) +4. Try global constants +5. Try global variables (includes named functions and all types) + +## The Special `none` type +Anything which doesn't return anything has a return type of `none`, indicating the return value can't be used. This is similar to C's `void`. + +## GOAL Structs vs. C Structs +There is one significant difference between C and GOAL when it comes to structs/classes - GOAL variables can only be references to structs. + +As an example, consider a GOAL type `my-type` and a C type `my_type`. In C/C++, a variable of type `my_type` represents an entire copy of a `my_type` object, and a `my_type*` is like a reference to an existing `my_type` object. In GOAL, an object of `my-type` is a reference to an existing `my-type` object, like a C `my_type*`. There is no equivalent to a C/C++ `my_type`. + +As a result you cannot pass or return a structure by value. + +Another way to explain this is that GOAL structures (including `pair`) always have reference semantics. All other GOAL types have value semantics. + +## Pointers +GOAL pointers work a lot like C/C++ pointers, but have some slight differences: +- A C `int32_t*` is a GOAL `(pointer int32)` +- A C `void*` is a GOAL `pointer` +- In C, if `x` is a `int32_t*`, `x + 1` is equivalent to `uintptr_t(x) + sizeof(int32_t)`. In GOAL, all pointer math is done in units of bytes. +- In C, you can't do pointer math on a `void*`. In GOAL you can, and all math is done in units of bytes. + +In both C and GOAL, there is a connection between arrays and pointers. A GOAL array field will have a pointer-to-element type, and a pointer can be accessed as an array. + +One confusing thing is that a `(pointer int32)` is a C `int32_t*`, but a `(pointer my-structure-type)` is a C `my_structure_type**`, becuase a GOAL `my-structure-type` is like a C `my_structure_type*`. + +## Inline Arrays +One limitation of the system above is that an array of `my_structure_type` is actually an array of references to structures (C `object*[]`). It would be more efficient if instead we had an array of structures, laid out together in memory (C `object[]`). + +GOAL has a "inline array" to represent this. A GOAL `(inline-array thing)` is like a C `thing[]`. The inline-array can only be used on structure types, as these are the only reference types. + + +## Fields in Structs +For a field with a reference type (structure/basic) +- `(data thing)` is like C `Thing* data;` +- `(data thing :inline #t)` is like C `Thing data;` +- `(data thing 12)` is like C `Thing* data[12];`. The field has `(pointer thing)` type. +- `(data thing 12 :inline #t)` is like `Thing data[12];`. The field has `(inline-array thing)` type + +For a field with a value type (integer, etc) +- `(data int32)` is like C `int32_t data;` +- `(data int32 12)` is like `int32_t data[12];`. The field has `(array int32)` type. + +Using the `:inline #t` option on a value type is not allowed. + +## Dynamic Structs +GOAL structure can be dynamically sized, which means their size isn't determined at compile time. Instead the user should implement `asize-of` to return the actual size. + +This works by having the structure end in an array of unknown size at compile time. In a dynamic structure definition, the last field of the struct should be an array with an unspecified size. To create this, add a `:dynamic #t` option to the field and do not specify an array size. This can be an array of value types, an array of reference types, or an inline-array of reference types. + +### Unknown +Is the `size` of a dynamic struct: +- size assuming the dynamic array has 0 elements (I think it's this) +- size assuming the dynamic array doesn't + +These can differ by padding for alignment. + + +## Methods + +## Method `_type_` type + +## Calling Methods + +## Built-in Methods + +## New - How To Create GOAL Objects + +## Defining a `new` Method + +## Integer Type +GOAL has some weird behavior when it comes to integers. It may seem complicated to describe, but it really makes the implementation simpler - the integer types are designed around the available MIPS instructions. + +Integers that are used as local variables (defined with `let`), function arguments, function return values, and intermediate values when combining these are called "register integers", as the values will be stored in CPU registers. + +Integers that are stored in memory as a field of a `structure`/`basic`, an element in an array, or accessed through a `pointer` are "memory integers", as the values will need to be loaded/stored from memory to access them. + +The "register integer" types are `int` and `uint`. They are 64-bit and mostly work exactly like you'd expect. Multiplication, division, and mod, are a little weird and are documented separately. + +The "memory integer" types are `int8`, `int16`, `int32`, `int64`, `uint8`, `uint16`, `uint32`, and `uint64`. + +Conversions between these types are completely automatic - as soon as you access a "memory integer", it will be converted to a "register integer", and trying to store a "register integer" will automatically convert it to the appropriate "memory integer". It (should be) impossible to accidentally get this wrong. + +### Side Note +- It's not clear what types `(new 'static 'integer)` or `(new 'stack 'integer)` are, though I would assume both are memory. + +- If there aren't enough hardware registers, "register integers" can be spilled to stack, but keep their "register integer" types. This process should be impossible to notice, so you don't have to worry about it. + +## Array Spacing +In general, all GOAL objects are 16-byte aligned and the boxing system requires this. All heap memory allocations are 16-byte aligned too, so this is usually not an issue. + +## Truth +Everything is true except for `#f`. This means `0` is true, and `'()` is true. +The value of `#f` can be used like `nullptr`, at least for any `basic` object. It's unclear if `#f` can/should be used as a null for other types, including `structure`s or numbers or pointers. + +Technical note: the hex number `0x147d24` is considered false in Jak 1 NTSC due to where the symbol table happened to be allocated. However, checking numbers for true/false shouldn't be done, you should use `(zero? x)` instead. + +## Empty Pair + +# Built-in Types + +## `structure` +A structure is the parent type of all types with fields. + +## `basic` +A basic is a structure with runtime type information. The first field is named `type` and contains the `type` of the basic. + +# Implemented in Runtime Language Functions + +## `string->symbol` + +## `symbol->string` + +## `format` +GOAL's `printf`. +```lisp +(format destination format-string args...) +``` + +The following destinations are currently supported: +- `#t` - print to the listener. After the current game frame is over, this will be sent to the compiler and you can see it at the GOAL prompt. +- `0` - print to `stdout` in the runtime immediately. This won't be visible from within the compiler - you must look at the runtime to see it. This is useful for debugging if the runtime crashes before flushing the normal print buffer. +- `#f` - print to a new string allocated on the global heap. +- a GOAL `string` object - append to an existing string +- a GOAL `file-stream` object - currently unsupported but eventually may allow printing into a file somewhere + +The global variable `*print-column*` can be used to automatically print at a certain indentation. The very first thing printed during a frame will not have the indentation applied. + +The format string excape sequences all start with `~`, then have arguments (possibly none), then have a single character code. The arguments look like: +- `~A`, the `A` code with no arguments +- `~12A`, the `A` code with an integer argument of `12` +- `~'zA`, the `A` code with a character argument of `z` +- ``` ~`hello`A```, the `A` code with a string argument of `"hello"` +- `~12,'bA`, the `A` code with two arguments, integer `12` and character `b` + + +The escape codes are + +### `~%` +Replace with a newline character. + +### `~~` +Replace with a `~` character + +### `~G` or `~g` +Replace with a C-style string given in `args`. Note that this will not work on a GOAL string. + +### `~A` or `~a` +Print a boxed object given in `args`. Uses the `print` method. Can take optional arguments for length and padding character. If the `print` method gives something shorter than the length argument, it will be padded on the left with the padding character (default is space). If the `print` method gives something too long, it will be truncated on the right. The last character printed will be changed to `~` to indicate it was truncated here. + +There is an option to left justify instead but I don't think there's a way to turn it on. + +### `~S` or `~s` +Very similar to `~A`, but a GOAL string will be printed without quotes around it. + +### `~C` or `~c` +Print a single character (GOAL `uint8` or `int8` type) + +### `~P` or `~p` +Print an object, similar to `~A`, but does not have optional arguments for length or padding. Instead, there is an optional argument to give a type name, then use that types `print` method. This is useful for printing non-boxed objects. If no type name is given, behaves like `~A`. + +### `~I` or `~i` +Like `~P`, but uses the inspect method instead. + +### `~Q` or `~q` +Likely was supposed to be for printing 128-bit integers stored in registers, but will always prints `0`. The `format` function is written in C using varargs, which doesn't support 128-bit register values. + +### `~B` or `~b` +Print integer as binary. Optional arguments for pad-length and pad-character (default is 0). Won't truncate. + +### `~D` or `~d` +Print integer as decimal (signed). Optional arguments for pad-length and pad-character (default is space). Won't truncate. + +### `~X` or `~x` +Print integer as hex. Optional arguments for pad-length and pad-character (default is 0). Won't truncate. + +### `~F` +Print floating point. Will print 4 digits after the decimal and pad with space to a width of 12 and does not accept any options. + +### `~f` +Print floating point. Takes optional arguments for pad-length (default don't pad), pad-character (default space), and precision (default 4). + +## `~R` or `~r` +Like `~f` but scales in-game rotation units to degrees. The float `65536.0` is 360 degrees. + +### `~M` or `~m` +Like `~f` but scales in-game distance units to meters. The float `4096.0` is 1 meter. + +### `~E` or `~e` +Like `~f` for argument, but takes an integer time-code as an input and scales time-code units to seconds. There are 300 ticks / second, which is the smallest number which has an integer number of ticks per NTSC and PAL frame. Something very weird happens when the input is negative? + +### `~T` or `~t` +Insert a tab character. + +### Pass Through Codes +Some codes will be passed through automatically, along with any arguments. This will also pass through arguments of `+` or `-`. I believe that these options could later be interpreted by the code for printing on-screen characters. + +The pass through codes are `H,J,K,L,N,V,W,Y,Z,h,j,k,l,n,v,w,y,z`. + +Any other codes will result in an error message being printed. + + + +## Load Stuff \ No newline at end of file diff --git a/game/CMakeLists.txt b/game/CMakeLists.txt index 6ded6b650..d8aeb06f3 100644 --- a/game/CMakeLists.txt +++ b/game/CMakeLists.txt @@ -70,16 +70,24 @@ set(RUNTIME_SOURCE overlord/stream.cpp) # the runtime should be built without any static/dynamic libraries. -add_executable(gk ${RUNTIME_SOURCE} main.cpp) +#add_executable(gk ${RUNTIME_SOURCE} main.cpp) + # we also build a runtime library for testing. This version is likely unable to call GOAL code correctly, but # can be used to test other things. add_library(runtime ${RUNTIME_SOURCE}) +add_executable(gk main.cpp) + IF (WIN32) # set stuff for windows - target_link_libraries(gk cross_sockets mman common_util) + target_link_libraries(runtime mman cross_sockets common_util) + target_link_libraries(gk cross_sockets mman common_util runtime) ELSE() # set stuff for other systems - target_link_libraries(gk cross_sockets pthread common_util) + target_link_libraries(runtime pthread cross_sockets common_util) + target_link_libraries(gk cross_sockets pthread common_util runtime) + ENDIF() + + diff --git a/game/kernel/kboot.cpp b/game/kernel/kboot.cpp index d33fe4738..da41b0128 100644 --- a/game/kernel/kboot.cpp +++ b/game/kernel/kboot.cpp @@ -143,15 +143,15 @@ void KernelCheckAndDispatch() { call_goal(Ptr(kernel_dispatcher->value), 0, 0, 0, s7.offset, g_ee_main_mem); } else { if (ListenerFunction->value != s7.offset) { - fprintf(stderr, "Running Listener Function:\n"); - auto cptr = Ptr(ListenerFunction->value).c(); - for (int i = 0; i < 40; i++) { - fprintf(stderr, "%x ", cptr[i]); - } - fprintf(stderr, "\n"); + // fprintf(stderr, "Running Listener Function:\n"); + // auto cptr = Ptr(ListenerFunction->value).c(); + // for (int i = 0; i < 40; i++) { + // fprintf(stderr, "%x ", cptr[i]); + // } + // fprintf(stderr, "\n"); auto result = call_goal(Ptr(ListenerFunction->value), 0, 0, 0, s7.offset, g_ee_main_mem); - fprintf(stderr, "result of listener function: %lld\n", result); +// fprintf(stderr, "result of listener function: %lld\n", result); #ifdef __linux__ cprintf("%ld\n", result); #else diff --git a/game/kernel/klisten.cpp b/game/kernel/klisten.cpp index 22f9aad10..322d049e3 100644 --- a/game/kernel/klisten.cpp +++ b/game/kernel/klisten.cpp @@ -149,7 +149,6 @@ void ProcessListenerMessage(Ptr msg) { // this setup allows listener function execution to clean up after itself. ListenerFunction->value = link_and_exec(buffer, "*listener*", 0, kdebugheap, LINK_FLAG_FORCE_DEBUG).offset; - fprintf(stderr, "ListenerFunction is now 0x%x\n", ListenerFunction->value); return; // don't ack yet, this will happen after the function runs. } break; default: diff --git a/game/kernel/kscheme.cpp b/game/kernel/kscheme.cpp index 09cc2ea00..e5b7fda32 100644 --- a/game/kernel/kscheme.cpp +++ b/game/kernel/kscheme.cpp @@ -887,8 +887,6 @@ u64 method_set(u32 type_, u32 method_id, u32 method) { if (method_id > 127) printf("[METHOD SET ERROR] tried to set method %d\n", method_id); - // printf("METHOD SET id %d to 0x%x type 0x%x!\n", method_id, method, type_); - auto existing_method = type->get_method(method_id).offset; if (method == 1) { diff --git a/game/system/Deci2Server.cpp b/game/system/Deci2Server.cpp index f3e2f09b6..665beb467 100644 --- a/game/system/Deci2Server.cpp +++ b/game/system/Deci2Server.cpp @@ -194,8 +194,8 @@ void Deci2Server::run() { } auto* hdr = (Deci2Header*)(buffer); - fprintf(stderr, "[DECI2] Got message:\n"); - fprintf(stderr, " %d %d 0x%x %c -> %c\n", hdr->len, hdr->rsvd, hdr->proto, hdr->src, hdr->dst); + fprintf(stderr, "[DECI2] Got message: %d %d 0x%x %c -> %c\n", hdr->len, hdr->rsvd, hdr->proto, + hdr->src, hdr->dst); hdr->rsvd = got; diff --git a/goal_src/goal-lib.gc b/goal_src/goal-lib.gc index ed89c1c71..db170bc6b 100644 --- a/goal_src/goal-lib.gc +++ b/goal_src/goal-lib.gc @@ -92,6 +92,12 @@ ) ) +(defmacro shutdown-target () + `(begin + (reset-target :shutdown) + ) + ) + ;;;;;;;;;;;;;;;;;;; ;; GOAL Syntax @@ -366,3 +372,34 @@ `(the ,(current-method-type) ((-> object method-table 0) allocation type-to-make ,@sz)) ) ) + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; TEST STUFF +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defmacro expect-eq (a b &key (name "unknown")) + `(if (!= ,a ,b) + (format #t "Test Failed On Test ~D: ~A~%" *test-count* ,name) + (+! *test-count* 1) + ) + ) + +(defmacro expect-true (a) + `(expect-eq ,a #t) + ) + +(defmacro expect-false (a) + `(expect-eq ,a #f) + ) + +(defmacro start-test (test-name) + `(begin + (define *test-name* ,test-name) + (define *test-count* 0) + ) + ) + +(defmacro finish-test () + `(format #t "Test ~A: ~D Passes~%" *test-name* *test-count*) + ) \ No newline at end of file diff --git a/goal_src/kernel-defs.gc b/goal_src/kernel-defs.gc index 862d2c7ff..bcf2b123f 100644 --- a/goal_src/kernel-defs.gc +++ b/goal_src/kernel-defs.gc @@ -71,7 +71,7 @@ (define-extern load (function string kheap object)) (define-extern loado (function string kheap object)) (define-extern unload (function string none)) -(define-extern _format function) +(define-extern _format (function _varargs_ object)) (define-extern malloc (function kheap int pointer)) (define-extern kmalloc (function kheap int int string)) (define-extern new-dynamic-structure (function kheap type int structure)) diff --git a/goal_src/kernel/gcommon.gc b/goal_src/kernel/gcommon.gc index b1dbdc9cb..8a8fdf622 100644 --- a/goal_src/kernel/gcommon.gc +++ b/goal_src/kernel/gcommon.gc @@ -301,9 +301,9 @@ (object-type object)) (until (eq? (set! basics-type (-> basics-type parent)) object-type) (if (eq? basics-type input-type) - ;; return-from #f will return from the function with the value of #t - (return-from #f #t) - ) + ;; return-from #f will return from the function with the value of #t + (return-from #f #t) + ) ) ) #f ;; didn't find it, return false @@ -315,8 +315,8 @@ ;; it's not clear why a might be zero? ;; perhaps if the type system is not yet initialized fully for the type? (if (or (eq? a b) (zero? a)) - (return-from #f #t) - ) + (return-from #f #t) + ) (set! a (-> a parent)) ) #f @@ -330,19 +330,19 @@ (let* ((child-method (-> the-type method-table method-id)) (parent-method child-method) ) - + ;; keep looking until we find a different parent method (until (not (eq? parent-method child-method)) ;; at the top of the type tree. (if (eq? the-type object) - (return-from #f nothing) - ) - + (return-from #f nothing) + ) + (set! the-type (-> the-type parent)) (set! parent-method (-> the-type method-table method-id)) (if (eq? 0 (the int parent-method)) - (return-from #f nothing) - ) + (return-from #f nothing) + ) ) parent-method ) @@ -362,17 +362,17 @@ (defmethod length pair ((obj pair)) "Get the number of elements in a proper list" (if (eq? obj '()) - (return-from #f 0) - ) - + (return-from #f 0) + ) + (let ((lst (cdr obj)) (len 1)) (while (and (not (eq? lst '())) (pair? lst) ) - (+1! len) - (set! lst (cdr lst)) - ) + (+1! len) + (set! lst (cdr lst)) + ) len) ) @@ -386,8 +386,8 @@ (defun last ((obj object)) "Get the last pair in a list." (while (not (eq? (cdr obj) '())) - (set! obj (cdr obj)) - ) + (set! obj (cdr obj)) + ) obj ) @@ -396,42 +396,42 @@ if not, return #f." (while (and (not (eq? lst '())) (not (eq? (car lst) obj))) - (set! lst (cdr lst)) - ) - - (if (eq? lst '()) - #f - lst + (set! lst (cdr lst)) ) + + (if (eq? lst '()) + #f + lst + ) ) (define-extern name= (function basic basic symbol)) (defun nmember ((obj basic) (lst object)) "If obj is a member of the list, return the pair containing obj as its car. - If not, return #f. Use name= (see gstring.gc) to check equality." - (while (and (not (eq? lst '())) - (not (name= (the basic (car lst)) obj)) - ) - (set! lst (cdr lst)) - ) - - (if (eq? lst '()) - #f - lst - ) + If not, return #f. Use name= (see gstring.gc) to check equality." + (while (and (not (eq? lst '())) + (not (name= (the basic (car lst)) obj)) + ) + (set! lst (cdr lst)) + ) + + (if (eq? lst '()) + #f + lst + ) ) (defun assoc ((item object) (alst object)) "Get a pair with car of item from the association list (list of pairs) alst." (while (and (not (null? alst)) (not (eq? (caar alst) item))) - (set! alst (cdr alst)) - ) - (if (not (null? alst)) - (car alst) - #f + (set! alst (cdr alst)) ) + (if (not (null? alst)) + (car alst) + #f + ) ) (defun assoce ((item object) (alst object)) @@ -440,12 +440,12 @@ (not (eq? (caar alst) item)) (not (eq? (caar alst) 'else)) ) - (set! alst (cdr alst)) - ) - (if (not (null? alst)) - (car alst) - #f + (set! alst (cdr alst)) ) + (if (not (null? alst)) + (car alst) + #f + ) ) ;; todo @@ -455,20 +455,20 @@ (defun append! ((front object) (back object)) "Append back to front." (if (null? front) - (return-from #f back) - ) - + (return-from #f back) + ) + (let ((lst front)) ;; seek to the end of front (while (not (null? (cdr lst))) - (set! lst (cdr lst)) - ) - + (set! lst (cdr lst)) + ) + ;; this check seems not needed (if (not (null? lst)) - (set! (cdr lst) back) - ) - + (set! (cdr lst) back) + ) + front ) ) @@ -477,21 +477,21 @@ "Delete the first occurance of item from a list and return the list. Does nothing if the item isn't in the list." (if (eq? (car lst) item) - (return-from #f (cdr lst)) - ) - + (return-from #f (cdr lst)) + ) + (let ((iter (cdr lst)) (rep lst)) - + (while (and (not (null? iter)) (not (eq? (car iter) item))) - (set! rep iter) - (set! iter (cdr iter)) - ) - - (if (not (null? iter)) - (set! (cdr rep) (cdr iter)) + (set! rep iter) + (set! iter (cdr iter)) ) + + (if (not (null? iter)) + (set! (cdr rep) (cdr iter)) + ) ) (the pair lst) ) @@ -500,20 +500,20 @@ "Like delete, but will delete if (car item-from-list) is equal to item. Useful for deleting from association list by key." ;(format #t "call to delete car: ~A ~A~%" item lst) (if (eq? (caar lst) item) - (return-from #f (cdr lst)) - ) - + (return-from #f (cdr lst)) + ) + (let ((rep lst) (iter (cdr lst))) (while (and (not (null? iter)) (not (eq? (caar iter) item))) - (set! rep iter) - (set! iter (cdr iter)) - ) - - (if (not (null? iter)) - (set! (cdr rep) (cdr iter)) + (set! rep iter) + (set! iter (cdr iter)) ) + + (if (not (null? iter)) + (set! (cdr rep) (cdr iter)) + ) ) lst ) @@ -523,7 +523,7 @@ (cons kv (delete-car! (car kv) alst)) ) -(defun sort ((lst object) (compare function)) +(defun sort ((lst object) (compare (function object object object))) "Sort the given list in place. Uses the given comparison function. The comparison function can either return #t/#f or an integer, in which case the sign of the integer determines lt/gt." ;; in each iteration, we count how many changes we make. Once we make no changes, the list is sorted. @@ -573,11 +573,12 @@ ((length int32 :offset-assert 4) (allocated-length int32 :offset-assert 8) (data uint8 :dynamic) - ;; ?? ) + (:methods (new (symbol type int) _type_ 0) ;; we will override print later on. This is optional to include + ) ) -(defmethod new inline-array-class ((allocation symbol) (type-to-make type) (cnt integer)) +(defmethod new inline-array-class ((allocation symbol) (type-to-make type) (cnt int)) "Create a new inline-array. Sets the length, allocated-length to cnt. Uses the mysterious heap-base field of the type-to-make to determine the element size" (let* ((sz (+ (-> type-to-make size) (* (-> type-to-make heap-base) cnt))) @@ -620,11 +621,24 @@ ) (while (< i size) - (set! (-> (the (pointer uint8) d) 0) (-> (the (pointer uint8) s) 0)) - (&+! d 1) - (&+! s 1) - (+1! i) - ) + (set! (-> (the (pointer uint8) d) 0) (-> (the (pointer uint8) s) 0)) + (&+! d 1) + (&+! s 1) + (+1! i) + ) + ) + dst + ) + +(defun mem-set32! ((dst pointer) (value int) (n int)) + "Memset a 32-bit value n times. Total memory filled is 4 * n bytes." + (let ((p (the pointer dst)) + (i 0)) + (while (< i n) + (set! (-> (the (pointer int32) p) 0) value) + (&+! p 4) + (+1! i) + ) ) dst ) \ No newline at end of file diff --git a/goal_src/test/test-approx-pi.gc b/goal_src/test/test-approx-pi.gc new file mode 100644 index 000000000..61af79174 --- /dev/null +++ b/goal_src/test/test-approx-pi.gc @@ -0,0 +1,49 @@ +(start-test "approx-pi") + +(defun test-approx-pi ((res integer)) + (let ((rad (* res res)) + (count 0)) + (dotimes (x res) + (dotimes (y res) + (if (> rad (+ (* x x) (* y y))) + (+! count 1) + ) + ) + ) + (* 4.0 (/ (the float count) (the float rad))) + ) + ) + +(let ((approx-pi (test-approx-pi 1000))) + (expect-true (> approx-pi 3.14)) + (expect-true (< approx-pi 3.15)) + ) + + +(defun test-approx-pi-float ((res float)) + (let* ((rad (the float (* res res))) + (count (the float 0)) + (x (the float 0)) + (y (the float 0)) + (scale (/ 1.0 rad))) + (while (< x res) + (set! y 0.0) + (while (< y res) + ; (format #t "tapf ~f ~f ~f~%" x y res) + (if (> rad (+ (* x x) (* y y))) + (+! count scale) + ) + (+! y 1.0) + ) + (+! x 1.0) + ) + (* 4.0 count) + ) + ) + +(let ((approx-pi (test-approx-pi-float 500.0))) + (expect-true (> approx-pi 3.14)) + (expect-true (< approx-pi 3.15)) + ) + +(finish-test) \ No newline at end of file diff --git a/goal_src/test/test-binteger-print.gc b/goal_src/test/test-binteger-print.gc new file mode 100644 index 000000000..7f5140d28 --- /dev/null +++ b/goal_src/test/test-binteger-print.gc @@ -0,0 +1,2 @@ +(format #t "~A~%" (the binteger -17)) +0 \ No newline at end of file diff --git a/goal_src/test/test-dynamic-type.gc b/goal_src/test/test-dynamic-type.gc new file mode 100644 index 000000000..1a12d5243 --- /dev/null +++ b/goal_src/test/test-dynamic-type.gc @@ -0,0 +1,64 @@ +(start-test "dynamic-type") + +(deftype test-dynamic-type (basic) + ( + (pad0 int16 :offset 4) + (allocated-length int32 :offset 8) + (data int32 :dynamic :offset 12) + (over1 int32 :offset 12) + (over2 int32 :offset 16) + ) + ) + + +(defmethod new test-dynamic-type ((allocation symbol) (type-to-make type) (cnt integer)) + ;"Create a new inline-array. Sets the length, allocated-length to cnt. Uses the mysterious heap-base field + ;of the type-to-make to determine the element size" + (let* ((sz (+ (-> type-to-make size) (* 4 cnt))) + (new-object (object-new sz))) + ;;(format 0 "create sz ~d at #x~X~%" sz new-object) + (unless (zero? new-object) + (set! (-> new-object allocated-length) cnt) + ) + + new-object + ) + ) + +(defmethod length test-dynamic-type ((obj test-dynamic-type)) + ;"Get the length of it" + (-> obj allocated-length) + ) + +(defmethod asize-of test-dynamic-type ((obj test-dynamic-type)) + + ;"Get the size in memory of it" + (the int (+ (-> obj type size) + (* (-> obj allocated-length) 4) + )) + ) + +(define test-dynamic-obj + (the test-dynamic-type ((-> test-dynamic-type method-table 0) 'global test-dynamic-type 40))) + + +; ;(define test-dynamic-obj (new 'global 'test-dynamic-type 40)) + +; ;(inspect test-dynamic-obj) + +(set! (-> test-dynamic-obj data 0) 12) +(set! (-> test-dynamic-obj data 1) 20) +; ;(inspect test-dynamic-obj) + +; ; (format #t "should be same (~d ~d) (~d ~d)~%" (-> test-dynamic-obj data 0) (-> test-dynamic-obj over1) +; ; (-> test-dynamic-obj data 1) (-> test-dynamic-obj over2)) + +(expect-true (= (-> test-dynamic-obj data 0) (-> test-dynamic-obj over1))) +(expect-true (= (-> test-dynamic-obj data 1) (-> test-dynamic-obj over2))) + +(set! (-> test-dynamic-obj pad0) 0) +(expect-true (= (-> test-dynamic-obj type) test-dynamic-type)) + +(expect-true (= (asize-of test-dynamic-obj) 180)) + +(finish-test) \ No newline at end of file diff --git a/goal_src/test/test-format-reg-order.gc b/goal_src/test/test-format-reg-order.gc index 4339feca4..d875fbc6a 100644 --- a/goal_src/test/test-format-reg-order.gc +++ b/goal_src/test/test-format-reg-order.gc @@ -1,4 +1,3 @@ -(define-extern _format function) (define format _format) (format #t "test ~D ~D ~D ~D ~D ~D~%" 1 2 3 4 5 6) diff --git a/goal_src/test/test-memset.gc b/goal_src/test/test-memset.gc new file mode 100644 index 000000000..0a06fb842 --- /dev/null +++ b/goal_src/test/test-memset.gc @@ -0,0 +1,18 @@ +(let* ((base-addr #x6000000) + (word-cnt 23) + (base (the (pointer int32) base-addr)) + (foo (mem-set32! base #x0 (+ 1 word-cnt))) + (last-byte (the (pointer uint8) (+ base-addr 3 (* 4 (- word-cnt 1))))) + (dst (mem-set32! base #x0badbeef word-cnt)) + ) + + (if (!= dst base) + (format #t "test failed, bad base returned!~%") + ) + + (if (!= 0 (-> last-byte 1)) + (format #t "set too many bytes!~%") + ) + + (-> last-byte 0) + ) \ No newline at end of file diff --git a/goal_src/test/test-nested-float-functions.gc b/goal_src/test/test-nested-float-functions.gc index d00a5d793..dac953184 100644 --- a/goal_src/test/test-nested-float-functions.gc +++ b/goal_src/test/test-nested-float-functions.gc @@ -1,4 +1,3 @@ -(define-extern _format function) (define format _format) (defun float-testing-function-2 ((x float) (y float)) diff --git a/goal_src/test/test-number-comparison.gc b/goal_src/test/test-number-comparison.gc new file mode 100644 index 000000000..e378a649e --- /dev/null +++ b/goal_src/test/test-number-comparison.gc @@ -0,0 +1,20 @@ +(start-test "number-comparison") +(expect-true (= 1.2 1.2)) +(expect-true (= 1 1)) +(expect-false (!= 1.2 1.2)) +(expect-false (= 1 2)) +(expect-true (= 1 1.2)) +(expect-false (= 1.2 1)) + +(expect-true (> 2 1)) +(expect-false (< 2 1)) + +(expect-true (> 3.2 3)) + +(expect-true (= 1 (the integer 1.2))) +(expect-true (= (the float 1) (the integer 1.2))) + +(expect-true (= 0.6 (- 1.2 0.6))) +(expect-true (= 0.6 (- 1.0 0.2 0.2))) +(expect-true (= 0.6 (- -0.6))) +(finish-test) \ No newline at end of file diff --git a/goal_src/test/test-return-from-f-tricky-color.gc b/goal_src/test/test-return-from-f-tricky-color.gc index 3ac6c02e1..a72b624c4 100644 --- a/goal_src/test/test-return-from-f-tricky-color.gc +++ b/goal_src/test/test-return-from-f-tricky-color.gc @@ -1,4 +1,3 @@ -(define-extern _format function) (define format _format) ;(db) diff --git a/goal_src/test/test-tests.gc b/goal_src/test/test-tests.gc new file mode 100644 index 000000000..e70813154 --- /dev/null +++ b/goal_src/test/test-tests.gc @@ -0,0 +1,8 @@ +(start-test "test-of-test") + + +(expect-eq 1 2) +(expect-eq 1 3 :name "test") +(expect-eq 5 5) + +(finish-test) \ No newline at end of file diff --git a/goal_src/test/test-type-arrays.gc b/goal_src/test/test-type-arrays.gc new file mode 100644 index 000000000..51513fc1d --- /dev/null +++ b/goal_src/test/test-type-arrays.gc @@ -0,0 +1,74 @@ +(start-test "test-type-arrays") + + +(defmacro new-type-hack (type-name) + `(the ,type-name ((-> ,type-name method-table 0) 'global ,type-name)) + ) + +(deftype basic-type (basic) + ( + (flt float) + (dec int32) + ) + ) + +(deftype struct-type (structure) + ( + (flt float :offset-assert 0) + (dec int32 :offset-assert 4) + (flt-overlay float :offset 4) + (dec-overlay int32 :offset 4) + ) + ) + +(let ((obj-test (new-type-hack struct-type))) + ;(inspect obj-test) + (set! (-> obj-test dec) #x00f0f0f0) + (set! (-> obj-test flt) 123.456) + (expect-eq (-> obj-test dec-overlay) #x00f0f0f0) + + ;((-> struct-type methods 3) obj-test) + ) + + +;;;;;; TEST 000 +;; create a new typ000 (no value, no array, no inline) +(deftype type000 (basic) + ( + (pad int32) + (x basic-type) + ) + ) + + +(let ((obj000 (new-type-hack type000)) + (bo (new-type-hack basic-type)) + ) + (set! (-> obj000 x) bo) + (expect-eq (-> obj000 x type) basic-type) + ) + + +;;;;;; TEST 001 +;; create a new type001 (no value, no array, yes inline) +(deftype type001 (basic) + ((pad int32) + (x struct-type) + ) + ) + + +(let ((obj001 (new-type-hack type001)) + (sub-obj (new-type-hack struct-type))) + ;;(inspect obj001) + (set! (-> obj001 x) sub-obj) + (set! (-> obj001 x dec) 12) + (expect-eq 12 (-> sub-obj dec)) + ;(inspect obj001) + ;((-> struct-type methods 3) (-> obj001 x)) + ) + +;; todo - test the other 6 (or maybe 4?) types? + + +(finish-test) \ No newline at end of file diff --git a/goalc/compiler/Compiler.h b/goalc/compiler/Compiler.h index 33bfd08b6..8c1561ecd 100644 --- a/goalc/compiler/Compiler.h +++ b/goalc/compiler/Compiler.h @@ -82,7 +82,7 @@ class Compiler { RegVal* function, const std::vector& args, Env* env, - std::string method_type_name = ""); + const std::string& method_type_name = ""); TypeSystem m_ts; std::unique_ptr m_global_env = nullptr; diff --git a/goalc/compiler/CompilerSettings.cpp b/goalc/compiler/CompilerSettings.cpp index eec56ac01..edf55be4c 100644 --- a/goalc/compiler/CompilerSettings.cpp +++ b/goalc/compiler/CompilerSettings.cpp @@ -9,6 +9,8 @@ CompilerSettings::CompilerSettings() { m_settings["disable-math-const-prop"].kind = SettingKind::BOOL; m_settings["disable-math-const-prop"].boolp = &disable_math_const_prop; + + link(print_timing, "print-timing"); } void CompilerSettings::set(const std::string& name, const goos::Object& value) { @@ -21,4 +23,9 @@ void CompilerSettings::set(const std::string& name, const goos::Object& value) { if (kv->second.boolp) { *kv->second.boolp = !(value.is_symbol() && value.as_symbol()->name == "#f"); } +} + +void CompilerSettings::link(bool& val, const std::string& name) { + m_settings[name].kind = SettingKind::BOOL; + m_settings[name].boolp = &val; } \ No newline at end of file diff --git a/goalc/compiler/CompilerSettings.h b/goalc/compiler/CompilerSettings.h index caecf4e17..634e189f6 100644 --- a/goalc/compiler/CompilerSettings.h +++ b/goalc/compiler/CompilerSettings.h @@ -14,10 +14,12 @@ class CompilerSettings { bool debug_print_regalloc = false; bool disable_math_const_prop = false; bool emit_move_after_return = true; + bool print_timing = false; void set(const std::string& name, const goos::Object& value); private: + void link(bool& val, const std::string& name); enum class SettingKind { BOOL, INVALID }; struct SettingsEntry { diff --git a/goalc/compiler/IR.cpp b/goalc/compiler/IR.cpp index 5d90b6a9a..f5594b599 100644 --- a/goalc/compiler/IR.cpp +++ b/goalc/compiler/IR.cpp @@ -287,6 +287,7 @@ std::string IR_FunctionCall::print() { RegAllocInstr IR_FunctionCall::to_rai() { RegAllocInstr rai; rai.read.push_back(m_func->ireg()); + rai.write.push_back(m_func->ireg()); // todo, can we avoid this? rai.write.push_back(m_ret->ireg()); for (auto& arg : m_args) { rai.read.push_back(arg->ireg()); @@ -299,8 +300,6 @@ RegAllocInstr IR_FunctionCall::to_rai() { } } - // todo, clobber call reg? - return rai; } @@ -326,6 +325,7 @@ void IR_FunctionCall::do_codegen(emitter::ObjectGenerator* gen, auto freg = get_reg(m_func, allocs, irec); gen->add_instr(IGen::add_gpr64_gpr64(freg, emitter::gRegInfo.get_offset_reg()), irec); gen->add_instr(IGen::call_r64(freg), irec); + // todo, can we do a sub to undo the modification to the register? does that actually work? } ///////////////////// @@ -508,6 +508,10 @@ std::string IR_FloatMath::print() { return fmt::format("divss {}, {}", m_dest->print(), m_arg->print()); case FloatMathKind::MUL_SS: return fmt::format("mulss {}, {}", m_dest->print(), m_arg->print()); + case FloatMathKind::ADD_SS: + return fmt::format("addss {}, {}", m_dest->print(), m_arg->print()); + case FloatMathKind::SUB_SS: + return fmt::format("subss {}, {}", m_dest->print(), m_arg->print()); default: throw std::runtime_error("Unsupported FloatMathKind"); } @@ -533,6 +537,14 @@ void IR_FloatMath::do_codegen(emitter::ObjectGenerator* gen, gen->add_instr( IGen::mulss_xmm_xmm(get_reg(m_dest, allocs, irec), get_reg(m_arg, allocs, irec)), irec); break; + case FloatMathKind::ADD_SS: + gen->add_instr( + IGen::addss_xmm_xmm(get_reg(m_dest, allocs, irec), get_reg(m_arg, allocs, irec)), irec); + break; + case FloatMathKind::SUB_SS: + gen->add_instr( + IGen::subss_xmm_xmm(get_reg(m_dest, allocs, irec), get_reg(m_arg, allocs, irec)), irec); + break; default: assert(false); } @@ -664,7 +676,9 @@ void IR_ConditionalBranch::do_codegen(emitter::ObjectGenerator* gen, } if (condition.is_float) { - assert(false); // for now + gen->add_instr( + IGen::cmp_flt_flt(get_reg(condition.a, allocs, irec), get_reg(condition.b, allocs, irec)), + irec); } else { gen->add_instr(IGen::cmp_gpr64_gpr64(get_reg(condition.a, allocs, irec), get_reg(condition.b, allocs, irec)), @@ -790,3 +804,51 @@ void IR_FunctionStart::do_codegen(emitter::ObjectGenerator* gen, (void)allocs; (void)irec; } + +/////////////////////// +// FloatToInt +/////////////////////// + +IR_FloatToInt::IR_FloatToInt(const RegVal* dest, const RegVal* src) : m_dest(dest), m_src(src) {} + +std::string IR_FloatToInt::print() { + return fmt::format("f2i {}, {}", m_dest->print(), m_src->print()); +} + +RegAllocInstr IR_FloatToInt::to_rai() { + RegAllocInstr rai; + rai.read.push_back(m_src->ireg()); + rai.write.push_back(m_dest->ireg()); + return rai; +} + +void IR_FloatToInt::do_codegen(emitter::ObjectGenerator* gen, + const AllocationResult& allocs, + emitter::IR_Record irec) { + gen->add_instr(IGen::float_to_int32(get_reg(m_dest, allocs, irec), get_reg(m_src, allocs, irec)), + irec); +} + +/////////////////////// +// IntToFloat +/////////////////////// + +IR_IntToFloat::IR_IntToFloat(const RegVal* dest, const RegVal* src) : m_dest(dest), m_src(src) {} + +std::string IR_IntToFloat::print() { + return fmt::format("i2f {}, {}", m_dest->print(), m_src->print()); +} + +RegAllocInstr IR_IntToFloat::to_rai() { + RegAllocInstr rai; + rai.read.push_back(m_src->ireg()); + rai.write.push_back(m_dest->ireg()); + return rai; +} + +void IR_IntToFloat::do_codegen(emitter::ObjectGenerator* gen, + const AllocationResult& allocs, + emitter::IR_Record irec) { + gen->add_instr(IGen::int32_to_float(get_reg(m_dest, allocs, irec), get_reg(m_src, allocs, irec)), + irec); +} \ No newline at end of file diff --git a/goalc/compiler/IR.h b/goalc/compiler/IR.h index 3cb924962..5a04c683e 100644 --- a/goalc/compiler/IR.h +++ b/goalc/compiler/IR.h @@ -223,7 +223,7 @@ class IR_IntegerMath : public IR { RegVal* m_arg; }; -enum class FloatMathKind { DIV_SS, MUL_SS }; +enum class FloatMathKind { DIV_SS, MUL_SS, ADD_SS, SUB_SS }; class IR_FloatMath : public IR { public: @@ -325,4 +325,32 @@ class IR_FunctionStart : public IR { std::vector m_args; }; +class IR_FloatToInt : public IR { + public: + IR_FloatToInt(const RegVal* dest, const RegVal* src); + std::string print() override; + RegAllocInstr to_rai() override; + void do_codegen(emitter::ObjectGenerator* gen, + const AllocationResult& allocs, + emitter::IR_Record irec) override; + + private: + const RegVal* m_dest = nullptr; + const RegVal* m_src = nullptr; +}; + +class IR_IntToFloat : public IR { + public: + IR_IntToFloat(const RegVal* dest, const RegVal* src); + std::string print() override; + RegAllocInstr to_rai() override; + void do_codegen(emitter::ObjectGenerator* gen, + const AllocationResult& allocs, + emitter::IR_Record irec) override; + + private: + const RegVal* m_dest = nullptr; + const RegVal* m_src = nullptr; +}; + #endif // JAK_IR_H diff --git a/goalc/compiler/Val.cpp b/goalc/compiler/Val.cpp index 201703dc3..7160dffc3 100644 --- a/goalc/compiler/Val.cpp +++ b/goalc/compiler/Val.cpp @@ -87,6 +87,11 @@ RegVal* LambdaVal::to_reg(Env* fe) { return re; } +RegVal* InlinedLambdaVal::to_reg(Env* fe) { + throw std::runtime_error("Cannot put InlinedLambdaVal in a register."); + return lv->to_reg(fe); +} + RegVal* FloatConstantVal::to_reg(Env* fe) { auto re = fe->make_xmm(coerce_to_reg_type(m_ts)); fe->emit(std::make_unique(re, m_value)); diff --git a/goalc/compiler/Val.h b/goalc/compiler/Val.h index f4e76086d..8076084fc 100644 --- a/goalc/compiler/Val.h +++ b/goalc/compiler/Val.h @@ -44,9 +44,12 @@ class Val { const TypeSpec& type() const { return m_ts; } void set_type(TypeSpec ts) { m_ts = std::move(ts); } + bool settable() const { return m_is_settable; } + void mark_as_settable() { m_is_settable = true; } protected: TypeSpec m_ts; + bool m_is_settable = false; }; /*! @@ -117,6 +120,14 @@ class LambdaVal : public Val { RegVal* to_reg(Env* fe) override; }; +class InlinedLambdaVal : public Val { + public: + explicit InlinedLambdaVal(TypeSpec ts, LambdaVal* _lv) : Val(std::move(ts)), lv(_lv) {} + std::string print() const override { return "inline-lambda-" + lv->lambda.debug_name; } + LambdaVal* lv = nullptr; + RegVal* to_reg(Env* fe) override; +}; + class StaticVal : public Val { public: StaticVal(StaticObject* _obj, TypeSpec _ts) : Val(std::move(_ts)), obj(_obj) {} diff --git a/goalc/compiler/compilation/Atoms.cpp b/goalc/compiler/compilation/Atoms.cpp index 8d164a4f4..03688d6f9 100644 --- a/goalc/compiler/compilation/Atoms.cpp +++ b/goalc/compiler/compilation/Atoms.cpp @@ -1,6 +1,6 @@ /*! * @file Atoms.cpp - * Compiler implementation for atoms - things which aren't compound forms. + * Top-level compilation forms for each of the GOOS object types. */ #include "goalc/compiler/Compiler.h" @@ -20,41 +20,38 @@ static const std::unordered_map< // {".jmp", &Compiler::compile_asm}, // {".sub", &Compiler::compile_asm}, // {".ret-reg", &Compiler::compile_asm}, - // - // // BLOCK FORMS + + // BLOCK FORMS {"top-level", &Compiler::compile_top_level}, {"begin", &Compiler::compile_begin}, {"block", &Compiler::compile_block}, {"return-from", &Compiler::compile_return_from}, {"label", &Compiler::compile_label}, {"goto", &Compiler::compile_goto}, - // - // // COMPILER CONTROL + + // COMPILER CONTROL {"gs", &Compiler::compile_gs}, {":exit", &Compiler::compile_exit}, {"asm-file", &Compiler::compile_asm_file}, {"listen-to-target", &Compiler::compile_listen_to_target}, {"reset-target", &Compiler::compile_reset_target}, {":status", &Compiler::compile_poke}, - // {"test", &Compiler::compile_test}, {"in-package", &Compiler::compile_in_package}, - // - // // CONDITIONAL COMPILATION + + // CONDITIONAL COMPILATION {"#cond", &Compiler::compile_gscond}, {"defglobalconstant", &Compiler::compile_defglobalconstant}, {"seval", &Compiler::compile_seval}, - // - // // CONTROL FLOW + + // CONTROL FLOW {"cond", &Compiler::compile_cond}, {"when-goto", &Compiler::compile_when_goto}, - // - // // DEFINITION + + // DEFINITION {"define", &Compiler::compile_define}, {"define-extern", &Compiler::compile_define_extern}, {"set!", &Compiler::compile_set}, - // {"defun-extern", &Compiler::compile_defun_extern}, - // {"declare-method", &Compiler::compile_declare_method}, - // + // TYPE {"deftype", &Compiler::compile_deftype}, {"defmethod", &Compiler::compile_defmethod}, @@ -68,30 +65,24 @@ static const std::unordered_map< {"car", &Compiler::compile_car}, {"cdr", &Compiler::compile_cdr}, {"method", &Compiler::compile_method}, - // - // - // // LAMBDA + + // LAMBDA {"lambda", &Compiler::compile_lambda}, {"declare", &Compiler::compile_declare}, {"inline", &Compiler::compile_inline}, // {"with-inline", &Compiler::compile_with_inline}, // {"rlet", &Compiler::compile_rlet}, // {"get-ra-ptr", &Compiler::compile_get_ra_ptr}, - // - // - // - // // MACRO - // + + // MACRO {"quote", &Compiler::compile_quote}, {"mlet", &Compiler::compile_mlet}, // {"defconstant", &Compiler::compile_defconstant}, - // - // // OBJECT - // {"current-method-type", &Compiler::compile_current_method_type}, - // - // - // // IT IS MATH + // OBJECT + // {"current-method-type", &Compiler::compile_current_method_type}, + + // MATH {"+", &Compiler::compile_add}, {"-", &Compiler::compile_sub}, {"*", &Compiler::compile_mul}, @@ -115,16 +106,12 @@ static const std::unordered_map< {">=", &Compiler::compile_condition_as_bool}, {"<", &Compiler::compile_condition_as_bool}, {">", &Compiler::compile_condition_as_bool}, - // - // // BUILDER (build-dgo/build-cgo?) - {"build-dgos", &Compiler::compile_build_dgo}, - // - // // UTIL - {"set-config!", &Compiler::compile_set_config}, - // - // // temporary testing hacks... - // {"send-test", &Compiler::compile_send_test_data}, + // BUILDER (build-dgo/build-cgo?) + {"build-dgos", &Compiler::compile_build_dgo}, + + // UTIL + {"set-config!", &Compiler::compile_set_config}, }; /*! @@ -148,6 +135,12 @@ Val* Compiler::compile(const goos::Object& code, Env* env) { return get_none(); } +/*! + * Compile a pair/list. + * Can be a compiler form, function call (possibly inlined), method call, immediate application of a + * lambda, or a goos macro. + * TODO - enums. + */ Val* Compiler::compile_pair(const goos::Object& code, Env* env) { auto pair = code.as_pair(); auto head = pair->car; @@ -161,41 +154,80 @@ Val* Compiler::compile_pair(const goos::Object& code, Env* env) { return ((*this).*(kv_gfs->second))(code, rest, env); } + // next try as a macro goos::Object macro_obj; if (try_getting_macro_from_goos(head, ¯o_obj)) { return compile_goos_macro(code, macro_obj, rest, env); } - // todo enum + // try as an enum (not yet implemented) } - // todo function or method call + // if none of the above cases worked, then treat it like a function/method call. return compile_function_or_method_call(code, env); - // throw_compile_error(code, "Unrecognized symbol at head of form"); - // return nullptr; } +/*! + * Compile an integer constant. Returns an IntegerConstantVal and emits no code. + * These integer constants do not generate static data and are stored directly in the code + * which is generated with to_gpr. + * The type is always int. + */ Val* Compiler::compile_integer(const goos::Object& code, Env* env) { + assert(code.is_int()); return compile_integer(code.integer_obj.value, env); } +/*! + * Compile an integer constant. Returns an IntegerConstantVal and emits no code. + * These integer constants do not generate static data and are stored directly in the code + * which is generated with to_gpr. + * The type is always int. + */ Val* Compiler::compile_integer(s64 value, Env* env) { auto fe = get_parent_env_of_type(env); return fe->alloc_val(m_ts.make_typespec("int"), value); } +/*! + * Get a SymbolVal representing a GOAL symbol object. + */ SymbolVal* Compiler::compile_get_sym_obj(const std::string& name, Env* env) { auto fe = get_parent_env_of_type(env); return fe->alloc_val(name, m_ts.make_typespec("symbol")); } +/*! + * Get a SymbolValueVal representing the value of a GOAL symbol. + * Will throw a compilation error if the symbol wasn't previously defined. + * TODO - determine sign extension behavior when loading symbol values. + */ +Val* Compiler::compile_get_symbol_value(const std::string& name, Env* env) { + auto existing_symbol = m_symbol_types.find(name); + if (existing_symbol == m_symbol_types.end()) { + throw std::runtime_error("The symbol " + name + " was not defined"); + } + + auto ts = existing_symbol->second; + auto sext = m_ts.lookup_type(ts)->get_load_signed(); + auto fe = get_parent_env_of_type(env); + auto sym = fe->alloc_val(name, m_ts.make_typespec("symbol")); + auto re = fe->alloc_val(sym, ts, sext); + return re; +} + +/*! + * Compile a symbol. Can get mlet macro symbols, local variables, constants, or symbols. + */ Val* Compiler::compile_symbol(const goos::Object& form, Env* env) { auto name = symbol_string(form); + // special case to get "nothing", used as a return value when nothing should be returned. if (name == "none") { return get_none(); } + // see if the symbol is defined in any enclosing symbol macro envs (mlet's). auto mlet_env = get_parent_env_of_type(env); while (mlet_env) { auto mlkv = mlet_env->macros.find(form.as_symbol()); @@ -205,6 +237,7 @@ Val* Compiler::compile_symbol(const goos::Object& form, Env* env) { mlet_env = get_parent_env_of_type(mlet_env->parent()); } + // see if it's a local variable auto lexical = env->lexical_lookup(form); if (lexical) { return lexical; @@ -213,6 +246,7 @@ Val* Compiler::compile_symbol(const goos::Object& form, Env* env) { auto global_constant = m_global_constants.find(form.as_symbol()); auto existing_symbol = m_symbol_types.find(form.as_symbol()->name); + // see if it's a constant if (global_constant != m_global_constants.end()) { // check there is no symbol with the same name if (existing_symbol != m_symbol_types.end()) { @@ -225,28 +259,21 @@ Val* Compiler::compile_symbol(const goos::Object& form, Env* env) { return compile_error_guard(global_constant->second, env); } + // none of those, so get a global symbol. return compile_get_symbol_value(name, env); } -Val* Compiler::compile_get_symbol_value(const std::string& name, Env* env) { - auto existing_symbol = m_symbol_types.find(name); - if (existing_symbol == m_symbol_types.end()) { - // assert(false); - throw std::runtime_error("The symbol " + name + " was not defined"); - } - - auto ts = existing_symbol->second; - auto sext = m_ts.lookup_type(ts)->get_load_signed(); - auto fe = get_parent_env_of_type(env); - auto sym = fe->alloc_val(name, m_ts.make_typespec("symbol")); - auto re = fe->alloc_val(sym, ts, sext); - return re; -} - +/*! + * Compile a string constant. The constant is placed in the same segment as the parent function. + */ Val* Compiler::compile_string(const goos::Object& form, Env* env) { - return compile_string(form.as_string()->data, env, MAIN_SEGMENT); + return compile_string(form.as_string()->data, env, + get_parent_env_of_type(env)->segment); } +/*! + * Compile a string constant and place it in the given segment. + */ Val* Compiler::compile_string(const std::string& str, Env* env, int seg) { auto obj = std::make_unique(str, seg); auto fe = get_parent_env_of_type(env); @@ -256,11 +283,22 @@ Val* Compiler::compile_string(const std::string& str, Env* env, int seg) { return result; } +/*! + * Compile a floating point constant and place it in the same segment as the containing function. + * Unlike integers, all floating point constants are stored separately as static data outside + * of the code, at least in Jak 1. + */ Val* Compiler::compile_float(const goos::Object& code, Env* env) { assert(code.is_float()); - return compile_float(code.float_obj.value, env, MAIN_SEGMENT); + return compile_float(code.float_obj.value, env, + get_parent_env_of_type(env)->segment); } +/*! + * Compile a floating point constant and place it in given segment. + * Unlike integers, all floating point constants are stored separately as static data outside + * of the code, at least in Jak 1. + */ Val* Compiler::compile_float(float value, Env* env, int seg) { auto obj = std::make_unique(value, seg); auto fe = get_parent_env_of_type(env); diff --git a/goalc/compiler/compilation/Block.cpp b/goalc/compiler/compilation/Block.cpp index 2828114e3..60f68beb1 100644 --- a/goalc/compiler/compilation/Block.cpp +++ b/goalc/compiler/compilation/Block.cpp @@ -1,26 +1,50 @@ +/*! + * @file Block.cpp + * Compiler implementation for blocks / gotos / labels + */ + #include "goalc/compiler/Compiler.h" #include "goalc/compiler/IR.h" using namespace goos; +/*! + * Compile "top-level" form, which is equivalent to a begin. + */ Val* Compiler::compile_top_level(const goos::Object& form, const goos::Object& rest, Env* env) { return compile_begin(form, rest, env); } +/*! + * Compile "begin" form, which compiles each segment in a row. + * TODO - determine if a GOAL begin matches this behavior for not "to_reg"ing anything. + */ Val* Compiler::compile_begin(const goos::Object& form, const goos::Object& rest, Env* env) { (void)form; Val* result = get_none(); - for_each_in_list(rest, [&](const Object& o) { result = compile_error_guard(o, env); }); + for_each_in_list(rest, [&](const Object& o) { + result = compile_error_guard(o, env); + if (!dynamic_cast(result)) { + result = result->to_reg(env); + } + }); return result; } +/*! + * Compile "block" form. Code inside of a block can "return from" the block and provide a value + * for the block. If this doesn't happen, the block takes on the value of the last thing in + * the block. + * TODO - determine if a GOAL block matches this behavior for not "to_reg"ing anything, + * and also using a gpr as a return value always. + */ Val* Compiler::compile_block(const goos::Object& form, const goos::Object& _rest, Env* env) { auto rest = &_rest; auto name = pair_car(*rest); rest = &pair_cdr(*rest); if (!rest->is_pair()) { - throw_compile_error(form, "Block form has an empty or invliad body"); + throw_compile_error(form, "Block form has an empty or invalid body"); } auto fe = get_parent_env_of_type(env); @@ -29,9 +53,8 @@ Val* Compiler::compile_block(const goos::Object& form, const goos::Object& _rest auto block_env = fe->alloc_env(env, symbol_string(name)); // we need to create a return value register, as a "return-from" statement inside the block may - // set it. for now it has a type of none, but we will set it more accurate after compiling the - // block. - // block_env->return_value = env->alloc_reg(get_base_typespec("none")); + // set it. for now it has a type of none, but we will set it after compiling the block. + // TODO - determine if GOAL blocks _always_ return gprs, or if it's possible to return xmms. block_env->return_value = env->make_gpr(m_ts.make_typespec("none")); // create label to the end of the block (we don't yet know where it is...) @@ -39,11 +62,17 @@ Val* Compiler::compile_block(const goos::Object& form, const goos::Object& _rest // compile everything in the body Val* result = get_none(); - for_each_in_list(*rest, [&](const Object& o) { result = compile_error_guard(o, block_env); }); + for_each_in_list(*rest, [&](const Object& o) { + result = compile_error_guard(o, block_env); + if (!dynamic_cast(result)) { + result = result->to_reg(env); + } + }); // if no return-from's were used, we can ignore the return_value register, and basically turn this // into a begin. this allows a block which returns a floating point value to return the value in // an xmm register, which is likely to eliminate a gpr->xmm move. + // TODO - does this happen in GOAL? if (block_env->return_types.empty()) { return result; } @@ -71,6 +100,11 @@ Val* Compiler::compile_block(const goos::Object& form, const goos::Object& _rest return block_env->return_value; } +/*! + * Compile a "return-from" statement. These can be used to return from a block and give a value. + * Note that there is a special "block" containing all code in a function called "#f", and you can + * use (return-from #f value) to return early from an entire function. + */ Val* Compiler::compile_return_from(const goos::Object& form, const goos::Object& _rest, Env* env) { const Object* rest = &_rest; auto block_name = symbol_string(pair_car(*rest)); @@ -97,23 +131,27 @@ Val* Compiler::compile_return_from(const goos::Object& form, const goos::Object& env->emit(std::move(ir_move_rv)); - // jump to end of block + // jump to end of block (by label object) auto ir_jump = std::make_unique(&block->end_label); - // we know this label is a real label. even though end_label doesn't know where it is, there is an - // actual label object. this means we won't try to resolve this label _by name_ later on when the - // block is done. - // ir_jump->resolved = true; env->emit(std::move(ir_jump)); - // In the real GOAL, there is likely a bug here where a non-none value is returned. + // In the real GOAL, there is likely a bug here where a non-none value is returned and to_gpr'd + // todo, determine if we should replicate this bug and if it can have side effects. return get_none(); } +/*! + * Compile a label form, which creates a named label that can be jumped to with goto. + */ Val* Compiler::compile_label(const goos::Object& form, const goos::Object& rest, Env* env) { auto label_name = symbol_string(pair_car(rest)); expect_empty_list(pair_cdr(rest)); // make sure we don't have a label with this name already + // note that you cannot jump out of your label space - they are not nested like lexical scopes! + // generally there is one label space per function. Inlined functions have their own label space + // and they will not permit jumping outside of the inlined function into the caller because + // that seems like a really bad idea. auto& labels = env->get_label_map(); auto kv = labels.find(label_name); if (kv != labels.end()) { @@ -121,12 +159,16 @@ Val* Compiler::compile_label(const goos::Object& form, const goos::Object& rest, form, "There are two labels named " + label_name + " in the same label environment"); } - // make a label pointing to the end of the current function env. + // make a label pointing to the end of the current function env. safe because we'll always add + // a terminating "null" instruction at the end. auto func_env = get_parent_env_of_type(env); labels[label_name] = Label(func_env, func_env->code().size()); return get_none(); } +/*! + * Compile a goto form, which unconditionally jumps to a label by name in the same label space. + */ Val* Compiler::compile_goto(const goos::Object& form, const goos::Object& rest, Env* env) { (void)form; auto label_name = symbol_string(pair_car(rest)); diff --git a/goalc/compiler/compilation/CompilerControl.cpp b/goalc/compiler/compilation/CompilerControl.cpp index 48113cb4a..b2c7ea2f1 100644 --- a/goalc/compiler/compilation/CompilerControl.cpp +++ b/goalc/compiler/compilation/CompilerControl.cpp @@ -1,9 +1,18 @@ +/*! + * @file CompilerControl.cpp + * Compiler implementation for forms which actually control the compiler. + */ + #include "goalc/compiler/Compiler.h" #include "goalc/compiler/IR.h" #include "common/util/Timer.h" #include "common/util/DgoWriter.h" #include "common/util/FileUtil.h" +/*! + * Exit the compiler. Disconnects the listener and tells the target to reset itself. + * Will actually exit the next time the REPL runs. + */ Val* Compiler::compile_exit(const goos::Object& form, const goos::Object& rest, Env* env) { (void)env; auto args = get_va(form, rest); @@ -11,10 +20,15 @@ Val* Compiler::compile_exit(const goos::Object& form, const goos::Object& rest, if (m_listener.is_connected()) { m_listener.send_reset(false); } + // flag for the REPL. m_want_exit = true; return get_none(); } +/*! + * Evaluate GOOS code. It's not possible to get the result, so this is really only useful to get + * a side effect. Used to bootstrap the GOAL/GOOS macro system. + */ Val* Compiler::compile_seval(const goos::Object& form, const goos::Object& rest, Env* env) { (void)env; try { @@ -27,6 +41,10 @@ Val* Compiler::compile_seval(const goos::Object& form, const goos::Object& rest, return get_none(); } +/*! + * Compile a file, and optionally color, save, or load. + * This should only be used for v3 "code object" files. + */ Val* Compiler::compile_asm_file(const goos::Object& form, const goos::Object& rest, Env* env) { (void)env; int i = 0; @@ -39,6 +57,7 @@ Val* Compiler::compile_asm_file(const goos::Object& form, const goos::Object& re std::vector> timing; Timer total_timer; + // parse arguments for_each_in_list(rest, [&](const goos::Object& o) { if (i == 0) { filename = as_string(o); @@ -59,43 +78,49 @@ Val* Compiler::compile_asm_file(const goos::Object& form, const goos::Object& re i++; }); + // READ Timer reader_timer; auto code = m_goos.reader.read_from_file({filename}); timing.emplace_back("read", reader_timer.getMs()); Timer compile_timer; std::string obj_file_name = filename; + + // Extract object name from file name. for (int idx = int(filename.size()) - 1; idx-- > 0;) { if (filename.at(idx) == '\\' || filename.at(idx) == '/') { obj_file_name = filename.substr(idx + 1); break; } } - obj_file_name = obj_file_name.substr(0, obj_file_name.find_last_of('.')); + + // COMPILE auto obj_file = compile_object_file(obj_file_name, code, !no_code); timing.emplace_back("compile", compile_timer.getMs()); if (color) { + // register allocation Timer color_timer; color_object_file(obj_file); timing.emplace_back("color", color_timer.getMs()); + // code/object file generation Timer codegen_timer; auto data = codegen_object_file(obj_file); timing.emplace_back("codegen", codegen_timer.getMs()); + // send to target if (load) { if (m_listener.is_connected()) { m_listener.send_code(data); } else { - printf("WARNING - couldn't load because listener isn't connected\n"); + printf("WARNING - couldn't load because listener isn't connected\n"); // todo spdlog warn } } + // save file if (write) { - // auto output_dir = as_string(get_constant_or_error(form, "*compiler-output-path*")); - // todo, change extension based on v3/v4 auto output_name = m_goos.reader.get_source_dir() + "/data/" + obj_file_name + ".o"; file_util::write_binary_file(output_name, (void*)data.data(), data.size()); } @@ -109,23 +134,29 @@ Val* Compiler::compile_asm_file(const goos::Object& form, const goos::Object& re } } - // if(truthy(get_config("print-asm-file-time"))) { - printf("F: %36s ", obj_file_name.c_str()); - for (auto& e : timing) { - printf(" %12s %4.2f", e.first.c_str(), e.second); + if (m_settings.print_timing) { + printf("F: %36s ", obj_file_name.c_str()); + timing.emplace_back("total", total_timer.getMs()); + for (auto& e : timing) { + printf(" %12s %4.2f", e.first.c_str(), e.second / 1000.f); + } + printf("\n"); } - printf("\n"); - // } return get_none(); } +/*! + * Connect the compiler to a target. Takes an optional IP address / port, defaults to + * 127.0.0.1 and 8112, which is the local computer and the default port for the DECI2 over IP + * implementation. + */ Val* Compiler::compile_listen_to_target(const goos::Object& form, const goos::Object& rest, Env* env) { (void)env; std::string ip = "127.0.0.1"; - int port = 8112; // todo, get from some constant somewhere + int port = DECI2_PORT; bool got_port = false, got_ip = false; for_each_in_list(rest, [&](const goos::Object& o) { @@ -150,6 +181,11 @@ Val* Compiler::compile_listen_to_target(const goos::Object& form, return get_none(); } +/*! + * Send the target a command to reset, which totally resets the state of the target. + * Optionally takes a :shutdown command which causes the exec_runtime function of the target + * to return after MachineShutdown. + */ Val* Compiler::compile_reset_target(const goos::Object& form, const goos::Object& rest, Env* env) { (void)env; bool shutdown = false; @@ -164,6 +200,11 @@ Val* Compiler::compile_reset_target(const goos::Object& form, const goos::Object return get_none(); } +/*! + * Send a "poke" message to the target. This can be used to check if the target is still alive and + * acknowledges commands, and also tells that target that somebody is connected so it will flush + * its outgoing buffers that have been storing data from startup. + */ Val* Compiler::compile_poke(const goos::Object& form, const goos::Object& rest, Env* env) { (void)env; auto args = get_va(form, rest); @@ -172,6 +213,9 @@ Val* Compiler::compile_poke(const goos::Object& form, const goos::Object& rest, return get_none(); } +/*! + * Enter a goos REPL. + */ Val* Compiler::compile_gs(const goos::Object& form, const goos::Object& rest, Env* env) { (void)env; auto args = get_va(form, rest); @@ -180,6 +224,9 @@ Val* Compiler::compile_gs(const goos::Object& form, const goos::Object& rest, En return get_none(); } +/*! + * Set a compiler setting by name. + */ Val* Compiler::compile_set_config(const goos::Object& form, const goos::Object& rest, Env* env) { (void)env; auto args = get_va(form, rest); @@ -188,6 +235,9 @@ Val* Compiler::compile_set_config(const goos::Object& form, const goos::Object& return get_none(); } +/*! + * Ignore the "in-package" statement and anything it contains at the top of GOAL files. + */ Val* Compiler::compile_in_package(const goos::Object& form, const goos::Object& rest, Env* env) { (void)form; (void)rest; @@ -195,6 +245,10 @@ Val* Compiler::compile_in_package(const goos::Object& form, const goos::Object& return get_none(); } +/*! + * Build dgo files. Takes a string argument pointing to the DGO description file, which is read + * and parsed here. + */ Val* Compiler::compile_build_dgo(const goos::Object& form, const goos::Object& rest, Env* env) { (void)env; auto args = get_va(form, rest); diff --git a/goalc/compiler/compilation/ControlFlow.cpp b/goalc/compiler/compilation/ControlFlow.cpp index 2cb2e47a8..d65ed7291 100644 --- a/goalc/compiler/compilation/ControlFlow.cpp +++ b/goalc/compiler/compilation/ControlFlow.cpp @@ -1,10 +1,18 @@ +/*! + * @file ControlFlow.cpp + * Compiler forms related to conditional branching and control flow. + */ + #include "goalc/compiler/Compiler.h" /*! - * Convert a condition expression into a GoalCondition for use in a conditional branch. + * Convert an expression into a GoalCondition for use in a conditional branch. + * * The reason for this design is to allow an optimization for * (if (< a b) ...) to be compiled without actually computing a true/false value for the (< a b) * expression. Instead, it will generate a cmp + jle sequence of instructions, which is much faster. + * In particular, getting GOAL "true" requires a few instructions, so it's to avoid this when + * possible. * * This can be applied to _any_ GOAL form, and will return a GoalCondition which can be used with a * Branch IR to branch if the condition is true/false. When possible it applies the optimization @@ -95,7 +103,7 @@ Condition Compiler::compile_condition(const goos::Object& condition, Env* env, b } } - // not something we can process more. Just check if we get false. + // not something we can process more. Just evaluate as normal and check if we get false. // todo - it's possible to optimize a false comparison because the false offset is zero gc.kind = invert ? ConditionKind::EQUAL : ConditionKind::NOT_EQUAL; gc.a = compile_error_guard(condition, env)->to_gpr(env); @@ -104,6 +112,12 @@ Condition Compiler::compile_condition(const goos::Object& condition, Env* env, b return gc; } +/*! + * Compile a comparison when we explicitly want a boolean result. This is used whenever a condition + * _isn't_ used as a branch condition. Like (set! x (< 1 2)) + * + * TODO, this could be optimized quite a bit. + */ Val* Compiler::compile_condition_as_bool(const goos::Object& form, const goos::Object& rest, Env* env) { @@ -124,6 +138,10 @@ Val* Compiler::compile_condition_as_bool(const goos::Object& form, return result; } +/*! + * The when-goto form is a better version of (if condition (goto x)) + * It compiles into a single conditional branch. + */ Val* Compiler::compile_when_goto(const goos::Object& form, const goos::Object& _rest, Env* env) { (void)form; auto* rest = &_rest; @@ -141,6 +159,12 @@ Val* Compiler::compile_when_goto(const goos::Object& form, const goos::Object& _ return get_none(); } +/*! + * The Scheme/Lisp "cond" form. + * Works like you expect. Return type is the lowest common ancestor of all possible return values. + * If no cases match and there's no else, returns #f. + * TODO - how should the return type work if #f can possibly be returned? + */ Val* Compiler::compile_cond(const goos::Object& form, const goos::Object& rest, Env* env) { auto result = env->make_gpr(m_ts.make_typespec("object")); @@ -170,12 +194,16 @@ Val* Compiler::compile_cond(const goos::Object& form, const goos::Object& rest, Val* case_result = get_none(); for_each_in_list(clauses, [&](const goos::Object& clause) { case_result = compile_error_guard(clause, env); + if (!dynamic_cast(case_result)) { + case_result = case_result->to_reg(env); + } }); case_result_types.push_back(case_result->type()); // optimization - if we get junk, don't bother moving it, just leave junk in return. if (!is_none(case_result)) { + // todo, what does GOAL do here? does it matter? env->emit(std::make_unique(result, case_result->to_gpr(env))); } @@ -193,10 +221,14 @@ Val* Compiler::compile_cond(const goos::Object& form, const goos::Object& rest, Val* case_result = get_none(); for_each_in_list(clauses, [&](const goos::Object& clause) { case_result = compile_error_guard(clause, env); + if (!dynamic_cast(case_result)) { + case_result = case_result->to_reg(env); + } }); case_result_types.push_back(case_result->type()); if (!is_none(case_result)) { + // todo, what does GOAL do here? env->emit(std::make_unique(result, case_result->to_gpr(env))); } @@ -210,8 +242,7 @@ Val* Compiler::compile_cond(const goos::Object& form, const goos::Object& rest, }); if (!got_else) { - // if no else, clause, return #f. But don't retype. I don't know how I feel about this typing - // setup. + // if no else, clause, return #f. But don't retype. todo what does goal do here? auto get_false = std::make_unique(result, "#f"); env->emit(std::move(get_false)); } diff --git a/goalc/compiler/compilation/Define.cpp b/goalc/compiler/compilation/Define.cpp index 7c2916e0a..69f51212e 100644 --- a/goalc/compiler/compilation/Define.cpp +++ b/goalc/compiler/compilation/Define.cpp @@ -1,6 +1,15 @@ +/*! + * @file Define.cpp + * Forms which define or set things. + */ + #include "goalc/compiler/Compiler.h" #include "goalc/logger/Logger.h" +/*! + * Define or set a global value. Has some special magic to store data for functions which may be + * inlined. + */ Val* Compiler::compile_define(const goos::Object& form, const goos::Object& rest, Env* env) { auto args = get_va(form, rest); va_check(form, args, {goos::ObjectType::SYMBOL, {}}, {}); @@ -41,6 +50,9 @@ Val* Compiler::compile_define(const goos::Object& form, const goos::Object& rest return in_gpr; } +/*! + * Inform the compiler of the type of a global. Will warn on changing type. + */ Val* Compiler::compile_define_extern(const goos::Object& form, const goos::Object& rest, Env* env) { (void)env; auto args = get_va(form, rest); @@ -52,6 +64,7 @@ Val* Compiler::compile_define_extern(const goos::Object& form, const goos::Objec auto existing_type = m_symbol_types.find(symbol_string(sym)); if (existing_type != m_symbol_types.end() && existing_type->second != new_type) { + // todo spdlog gLogger.log( MSG_WARN, "[Warning] define-extern has redefined the type of symbol %s\npreviously: %s\nnow: %s\n", @@ -62,12 +75,18 @@ Val* Compiler::compile_define_extern(const goos::Object& form, const goos::Objec return get_none(); } +/*! + * Set something to something. + * Lots of special cases. + */ Val* Compiler::compile_set(const goos::Object& form, const goos::Object& rest, Env* env) { auto args = get_va(form, rest); va_check(form, args, {{}, {}}, {}); auto& destination = args.unnamed.at(0); - // todo, I don't know if this is the correct order or not. + // todo, I don't know if this is the correct order or not. Right now the value is computed + // and to_reg'd first, then the destination is computed, if the destination requires math to + // compute. auto source = compile_error_guard(args.unnamed.at(1), env)->to_reg(env); if (destination.is_symbol()) { @@ -97,25 +116,29 @@ Val* Compiler::compile_set(const goos::Object& form, const goos::Object& rest, E } } } else { + // destination is some complex expression, so compile it and hopefully get something settable. auto dest = compile_error_guard(destination, env); auto as_mem_deref = dynamic_cast(dest); auto as_pair = dynamic_cast(dest); if (as_mem_deref) { + // setting somewhere in memory auto base = as_mem_deref->base; auto base_as_mco = dynamic_cast(base); if (base_as_mco) { + // if it is a constant offset, we can use a fancy x86-64 addressing mode to simplify auto ti = m_ts.lookup_type(base->type()); env->emit(std::make_unique( source, base_as_mco->offset, base_as_mco->base->to_gpr(env), ti->get_load_size())); return source; } else { + // nope, the pointer to dereference is some compliated thing. auto ti = m_ts.lookup_type(base->type()); env->emit(std::make_unique(source, 0, base->to_gpr(env), ti->get_load_size())); return source; - throw_compile_error(form, "Set not implemented for this (non-mco) yet"); } } else if (as_pair) { + // this could probably be part of MemoryDerefVal and not a special case here. env->emit(std::make_unique(source, as_pair->is_car ? -2 : 2, as_pair->base->to_gpr(env), 4)); return source; diff --git a/goalc/compiler/compilation/Function.cpp b/goalc/compiler/compilation/Function.cpp index 44d944fec..5caad5496 100644 --- a/goalc/compiler/compilation/Function.cpp +++ b/goalc/compiler/compilation/Function.cpp @@ -1,7 +1,15 @@ +/*! + * @file Function.cpp + * Calling and defining functions, lambdas, and inlining. + */ + #include "goalc/compiler/Compiler.h" #include "goalc/logger/Logger.h" namespace { +/*! + * Get the preference to inline of the given environment. + */ bool get_inline_preference(Env* env) { auto ile = get_parent_env_of_type(env); if (ile) { @@ -11,6 +19,9 @@ bool get_inline_preference(Env* env) { } } +/*! + * Hacky function to seek past arguments to get a goos::Object containing the body of a lambda. + */ const goos::Object& get_lambda_body(const goos::Object& def) { auto* iter = &def; while (true) { @@ -26,6 +37,14 @@ const goos::Object& get_lambda_body(const goos::Object& def) { } } // namespace +/*! + * The (inline my-func) form is like my-func, except my-func will be inlined instead of called, + * when used in a function call. This only works for immediaate function calls, you can't "save" + * an (inline my-func) into a function pointer. + * + * If inlining is not possible (function disallows inlining or didn't save its code), throw an + * error. + */ Val* Compiler::compile_inline(const goos::Object& form, const goos::Object& rest, Env* env) { (void)env; auto args = get_va(form, rest); @@ -39,17 +58,19 @@ Val* Compiler::compile_inline(const goos::Object& form, const goos::Object& rest if (kv->second->func && !kv->second->func->settings.allow_inline) { throw_compile_error(form, "Found function to inline, but it isn't allowed."); } - - // todo, this should return a "view" of the lambda which indicates its inlined - // so the correct label namespace behavior can be used. - return kv->second; + auto fe = get_parent_env_of_type(env); + return fe->alloc_val(kv->second->type(), kv->second); } +/*! + * Compile a lambda. This is used for real lambdas, lets, and defuns. So there are a million + * confusing special cases... + */ Val* Compiler::compile_lambda(const goos::Object& form, const goos::Object& rest, Env* env) { auto fe = get_parent_env_of_type(env); auto args = get_va(form, rest); if (args.unnamed.empty() || !args.unnamed.front().is_list() || - !args.only_contains_named({"name", "inline-only"})) { + !args.only_contains_named({"name", "inline-only", "segment"})) { throw_compile_error(form, "Invalid lambda form"); } @@ -65,7 +86,7 @@ Val* Compiler::compile_lambda(const goos::Object& form, const goos::Object& rest lambda_ts.add_arg(m_ts.make_typespec("object")); } else { auto param_args = get_va(o, o); - va_check(o, param_args, {goos::ObjectType::SYMBOL, goos::ObjectType::SYMBOL}, {}); + va_check(o, param_args, {goos::ObjectType::SYMBOL, {}}, {}); GoalArg parm; parm.name = symbol_string(param_args.unnamed.at(0)); @@ -77,9 +98,8 @@ Val* Compiler::compile_lambda(const goos::Object& form, const goos::Object& rest }); assert(lambda.params.size() == lambda_ts.arg_count()); - // optional name for debugging + // optional name for debugging (defun sets this) if (args.has_named("name")) { - // todo, this probably prints a nasty error if name isn't a string. lambda.debug_name = symbol_string(args.get_named("name")); } @@ -89,13 +109,36 @@ Val* Compiler::compile_lambda(const goos::Object& form, const goos::Object& rest bool inline_only = args.has_named("inline-only") && symbol_string(args.get_named("inline-only")) != "#f"; + // pick default segment to store function in. + int segment = MAIN_SEGMENT; + if (fe->segment == DEBUG_SEGMENT) { + // make anonymous lambdas in debug functions also go to debug + segment = DEBUG_SEGMENT; + } + + // override default segment. + if (args.has_named("segment")) { + auto segment_name = symbol_string(args.get_named("segment")); + if (segment_name == "main") { + segment = MAIN_SEGMENT; + } else if (segment_name == "debug") { + segment = DEBUG_SEGMENT; + } else { + throw_compile_error(form, "invalid segment override in lambda"); + } + } + if (!inline_only) { // compile a function! First create env auto new_func_env = std::make_unique(env, lambda.debug_name); - new_func_env->set_segment(MAIN_SEGMENT); // todo, how do we set debug? + new_func_env->set_segment(segment); // set up arguments - assert(lambda.params.size() < 8); // todo graceful error + if (lambda.params.size() >= 8) { + throw_compile_error(form, "lambda generating code has too many parameters!"); + } + + // set up argument register constraints. std::vector args_for_coloring; for (u32 i = 0; i < lambda.params.size(); i++) { IRegConstraint constr; @@ -117,28 +160,37 @@ Val* Compiler::compile_lambda(const goos::Object& form, const goos::Object& rest func_block_env->end_label = Label(new_func_env.get()); func_block_env->emit(std::make_unique(args_for_coloring)); - // compile the function! + // compile the function, iterating through the body. Val* result = nullptr; bool first_thing = true; for_each_in_list(lambda.body, [&](const goos::Object& o) { result = compile_error_guard(o, func_block_env); + if (!dynamic_cast(result)) { + result = result->to_reg(func_block_env); + } if (first_thing) { first_thing = false; - // you could probably cheat and do a (begin (blorp) (declare ...)) to get around this. + // you could cheat and do a (begin (blorp) (declare ...)) to get around this. + // but I see no strong reason why "declare"s need to go at the beginning, so no reason + // to make this better. new_func_env->settings.is_set = true; } }); if (result) { + // got a result, so to_gpr it and return it. auto final_result = result->to_gpr(new_func_env.get()); new_func_env->emit(std::make_unique(return_reg, final_result)); lambda_ts.add_arg(final_result->type()); } else { + // empty body, return none lambda_ts.add_arg(m_ts.make_typespec("none")); } + // put null instruction at the end so jumps to the end have somewhere to go. func_block_env->end_label.idx = new_func_env->code().size(); new_func_env->emit(std::make_unique()); new_func_env->finish(); + // save our code for possible inlining auto obj_env = get_parent_env_of_type(new_func_env.get()); assert(obj_env); if (new_func_env->settings.save_code) { @@ -150,12 +202,16 @@ Val* Compiler::compile_lambda(const goos::Object& form, const goos::Object& rest return place; } +/*! + * Compile a form which should be either a function call (possibly inline) or method call. + * Note - calling method "new" isn't handled by this. + * Again, there are way too many special cases here. + */ Val* Compiler::compile_function_or_method_call(const goos::Object& form, Env* env) { goos::Object f = form; auto fe = get_parent_env_of_type(env); auto args = get_va(form, form); - auto uneval_head = args.unnamed.at(0); Val* head = get_none(); @@ -163,7 +219,7 @@ Val* Compiler::compile_function_or_method_call(const goos::Object& form, Env* en // this logic will not trigger for a manually inlined call [using the (inline func) form] bool auto_inline = false; if (uneval_head.is_symbol()) { - // we can only auto-inline the function if its name is explicit. + // we can only auto-inline the function if its name is explicitly given. // look it up: auto kv = m_inlineable_functions.find(uneval_head.as_symbol()); if (kv != m_inlineable_functions.end()) { @@ -214,10 +270,24 @@ Val* Compiler::compile_function_or_method_call(const goos::Object& form, Env* en // LambdaPlace! so this cast will only succeed if the auto-inliner succeeded, or the user has // passed use explicitly a lambda either with the lambda form, or with the (inline ...) form. LambdaVal* head_as_lambda = nullptr; + bool got_inlined_lambda = false; if (!is_method_call) { + // try directly as a lambda head_as_lambda = dynamic_cast(head); + + if (!head_as_lambda) { + // nope, so try as an (inline x) + auto head_as_inlined_lambda = dynamic_cast(head); + if (head_as_inlined_lambda) { + // yes, remember the lambda that contains and flag that we're inlining. + head_as_lambda = head_as_inlined_lambda->lv; + got_inlined_lambda = true; + } + } } + // no lambda (not inlining or immediate), and not a method call, so we should actually get + // the function pointer. if (!head_as_lambda && !is_method_call) { head = head->to_gpr(env); } @@ -226,11 +296,12 @@ Val* Compiler::compile_function_or_method_call(const goos::Object& form, Env* en std::vector eval_args; for (uint32_t i = 1; i < args.unnamed.size(); i++) { auto intermediate = compile_error_guard(args.unnamed.at(i), env); + // todo, are the eval/to_reg'd in batches? eval_args.push_back(intermediate->to_reg(env)); } if (head_as_lambda) { - // inline the function! + // inline/immediate the function! // check args are ok if (head_as_lambda->lambda.params.size() != eval_args.size()) { @@ -242,26 +313,32 @@ Val* Compiler::compile_function_or_method_call(const goos::Object& form, Env* en Env* compile_env = lexical_env; - // if needed create a label env. - // we don't want a separate label env with lets, but we do in other cases. - if (auto_inline) { - // TODO - this misses the case of (inline func)! + // if need to, create a label env. + // we don't want a separate label env with lets, but we do for inlined functions. + // either inlined through the auto-inliner, or through an explicit (inline x) form. + if (auto_inline || got_inlined_lambda) { compile_env = fe->alloc_env(lexical_env); } // check arg types if (!head->type().arg_count()) { if (head->type().arg_count() - 1 != eval_args.size()) { - throw_compile_error(form, "invalid number of arguments to function call (inline)"); + throw_compile_error(form, + "invalid number of arguments to function call (inline or immediate " + "lambda application)"); } + // immediate lambdas (lets) will have all types as the most general object by default + // inlined functions will have real types that are checked... for (uint32_t i = 0; i < eval_args.size(); i++) { typecheck(form, head->type().get_arg(i), eval_args.at(i)->type(), - "function (inline) argument"); + "function/lambda (inline/immediate) argument"); } } // copy args... for (uint32_t i = 0; i < eval_args.size(); i++) { + // note, inlined functions will get a more specific type if possible + // todo, is this right? auto type = eval_args.at(i)->type(); auto copy = env->make_ireg(type, get_preferred_reg_kind(type)); env->emit(std::make_unique(copy, eval_args.at(i))); @@ -273,27 +350,36 @@ Val* Compiler::compile_function_or_method_call(const goos::Object& form, Env* en Val* result = get_none(); for_each_in_list(head_as_lambda->lambda.body, [&](const goos::Object& o) { result = compile_error_guard(o, compile_env); + if (!dynamic_cast(result)) { + result = result->to_reg(compile_env); + } if (first_thing) { first_thing = false; lexical_env->settings.is_set = true; } }); - // this doesn't require a return type. + // ignore the user specified return type and return the most specific type. + // todo - does this make sense for an inline function? Should we check the return type? return result; } else { - // not an inline call + // not an inlined/immediate, it's a real function call. + // todo, this order is extremely likely to be wrong, we should get the method way earlier. if (is_method_call) { + // method needs at least one argument to tell what we're calling the method on. if (eval_args.empty()) { throw_compile_error(form, "Unrecognized symbol " + uneval_head.print() + " as head of form"); } + // get the method function pointer head = compile_get_method_of_object(eval_args.front(), symbol_string(uneval_head), env); + fmt::format("method of object {} {}\n", head->print(), head->type().print()); } - // convert the head to a GPR + // convert the head to a GPR (if function, this is already done) auto head_as_gpr = head->to_gpr(env); if (head_as_gpr) { + // method calls have special rules for typing _type_ arguments. if (is_method_call) { return compile_real_function_call(form, head_as_gpr, eval_args, env, eval_args.front()->type().base_type()); @@ -310,11 +396,23 @@ Val* Compiler::compile_function_or_method_call(const goos::Object& form, Env* en return get_none(); } +namespace { +/*! + * Is the given typespec for a varargs function? Assumes typespec is a function to begin with. + */ +bool is_varargs_function(const TypeSpec& ts) { + return ts.arg_count() >= 2 && ts.get_arg(0).print() == "_varargs_"; +} +} // namespace + +/*! + * Do a real x86-64 function call. + */ Val* Compiler::compile_real_function_call(const goos::Object& form, RegVal* function, const std::vector& args, Env* env, - std::string method_type_name) { + const std::string& method_type_name) { auto fe = get_parent_env_of_type(env); fe->require_aligned_stack(); TypeSpec return_ts; @@ -322,27 +420,17 @@ Val* Compiler::compile_real_function_call(const goos::Object& form, // if the type system doesn't know what the function will return, just make it object. // the user is responsible for getting this right. return_ts = m_ts.make_typespec("object"); - gLogger.log(MSG_WARN, "[Warning] Function call could not determine return type: %s\n", - form.print().c_str()); - // todo, should this be a warning? not a great thing if we don't know what a function will - // return? + gLogger.log(MSG_WARN, "[Warning] Function call could not determine return type: %s, %s, %s\n", + form.print().c_str(), function->print().c_str(), function->type().print().c_str()); + // todo, consider making this an error once object-new works better. } else { return_ts = function->type().last_arg(); } auto return_reg = env->make_ireg(return_ts, emitter::RegKind::GPR); - // TODO - VERY IMPORTANT - // CREATE A TEMP COPY OF FUNCTION! WILL BE DESTROYED. - - // nope! not anymore. - // for(auto& arg : args) { - // // note: this has to be done in here, because we might want to const prop across lexical - // envs. arg = resolve_to_gpr(arg, env); - // } - // check arg count: - if (function->type().arg_count()) { + if (function->type().arg_count() && !is_varargs_function(function->type())) { if (function->type().arg_count() - 1 != args.size()) { throw_compile_error(form, "invalid number of arguments to function call: got " + std::to_string(args.size()) + " and expected " + @@ -366,7 +454,10 @@ Val* Compiler::compile_real_function_call(const goos::Object& form, env->emit(std::make_unique(arg_outs.back(), arg)); } - env->emit(std::make_unique(function, return_reg, arg_outs)); + // todo, there's probably a more efficient way to do this. + auto temp_function = fe->make_gpr(function->type()); + env->emit(std::make_unique(temp_function, function)); + env->emit(std::make_unique(temp_function, return_reg, arg_outs)); if (m_settings.emit_move_after_return) { auto result_reg = env->make_gpr(return_reg->type()); @@ -377,6 +468,10 @@ Val* Compiler::compile_real_function_call(const goos::Object& form, } } +/*! + * A (declare ...) form can be used to configure settings inside a function. + * Currently there aren't many useful settings, but more may be added in the future. + */ Val* Compiler::compile_declare(const goos::Object& form, const goos::Object& rest, Env* env) { auto& settings = get_parent_env_of_type(env)->settings; diff --git a/goalc/compiler/compilation/Macro.cpp b/goalc/compiler/compilation/Macro.cpp index 90e532a78..9cbc91b24 100644 --- a/goalc/compiler/compilation/Macro.cpp +++ b/goalc/compiler/compilation/Macro.cpp @@ -2,6 +2,9 @@ using namespace goos; +/*! + * Try to find a macro with the given name in the GOOS "goal_env". Return if it succeeded. + */ bool Compiler::try_getting_macro_from_goos(const goos::Object& macro_name, goos::Object* dest) { Object macro_obj; bool got_macro = false; @@ -20,6 +23,9 @@ bool Compiler::try_getting_macro_from_goos(const goos::Object& macro_name, goos: return got_macro; } +/*! + * Expand a macro, then compile the result. + */ Val* Compiler::compile_goos_macro(const goos::Object& o, const goos::Object& macro_obj, const goos::Object& rest, @@ -37,6 +43,9 @@ Val* Compiler::compile_goos_macro(const goos::Object& o, return compile_error_guard(goos_result, env); } +/*! + * Compile the #cond form, which is a compile-time conditional statement. + */ Val* Compiler::compile_gscond(const goos::Object& form, const goos::Object& rest, Env* env) { if (!rest.is_pair()) { throw_compile_error(form, "#cond must have at least one clause, which must be a form"); @@ -61,8 +70,12 @@ Val* Compiler::compile_gscond(const goos::Object& form, const goos::Object& rest // got a match! result = get_none(); - for_each_in_list(current_case.as_pair()->cdr, - [&](const Object& o) { result = compile_error_guard(o, env); }); + for_each_in_list(current_case.as_pair()->cdr, [&](const Object& o) { + result = compile_error_guard(o, env); + if (!dynamic_cast(result)) { + result = result->to_reg(env); + } + }); return result; } else { // no match, continue. @@ -76,6 +89,10 @@ Val* Compiler::compile_gscond(const goos::Object& form, const goos::Object& rest } } +/*! + * Compile (quote x) or 'x forms. + * Current only supports 'thing or '(). Static lists/pairs should be added at some point. + */ Val* Compiler::compile_quote(const goos::Object& form, const goos::Object& rest, Env* env) { auto args = get_va(form, rest); va_check(form, args, {{}}, {}); @@ -95,6 +112,9 @@ Val* Compiler::compile_quote(const goos::Object& form, const goos::Object& rest, return get_none(); } +/*! + * Compile defglobalconstant forms, which define a constant in both GOOS and GOAL. + */ Val* Compiler::compile_defglobalconstant(const goos::Object& form, const goos::Object& _rest, Env* env) { @@ -122,6 +142,9 @@ Val* Compiler::compile_defglobalconstant(const goos::Object& form, return get_none(); } +/*! + * Compile an "mlet" scoped constant/symbol macro form + */ Val* Compiler::compile_mlet(const goos::Object& form, const goos::Object& rest, Env* env) { auto defs = pair_car(rest); auto body = pair_cdr(rest); @@ -136,6 +159,11 @@ Val* Compiler::compile_mlet(const goos::Object& form, const goos::Object& rest, }); Val* result = get_none(); - for_each_in_list(body, [&](const goos::Object& o) { result = compile_error_guard(o, menv); }); + for_each_in_list(body, [&](const goos::Object& o) { + result = compile_error_guard(o, menv); + if (!dynamic_cast(result)) { + result = result->to_reg(menv); + } + }); return result; } \ No newline at end of file diff --git a/goalc/compiler/compilation/Math.cpp b/goalc/compiler/compilation/Math.cpp index 4a5db0dbd..674152f17 100644 --- a/goalc/compiler/compilation/Math.cpp +++ b/goalc/compiler/compilation/Math.cpp @@ -44,7 +44,10 @@ Val* Compiler::number_to_integer(Val* in, Env* env) { if (is_binteger(ts)) { throw std::runtime_error("Can't convert " + in->print() + " (a binteger) to an integer."); } else if (is_float(ts)) { - throw std::runtime_error("Can't convert " + in->print() + " (a float) to an integer."); + auto fe = get_parent_env_of_type(env); + auto result = fe->make_gpr(m_ts.make_typespec("int")); + env->emit(std::make_unique(result, in->to_xmm(env))); + return result; } else if (is_integer(ts)) { return in; } @@ -59,7 +62,11 @@ Val* Compiler::number_to_binteger(Val* in, Env* env) { } else if (is_float(ts)) { throw std::runtime_error("Can't convert " + in->print() + " (a float) to a binteger."); } else if (is_integer(ts)) { - throw std::runtime_error("Can't convert " + in->print() + " (an integer) to a binteger."); + auto fe = get_parent_env_of_type(env); + RegVal* input = in->to_reg(env); + auto sa = fe->make_gpr(m_ts.make_typespec("int")); + env->emit(std::make_unique(sa, 3)); + return compile_variable_shift(input, sa, env, IntegerMathKind::SHLV_64); } throw std::runtime_error("Can't convert " + in->print() + " to a binteger."); } @@ -72,7 +79,10 @@ Val* Compiler::number_to_float(Val* in, Env* env) { } else if (is_float(ts)) { return in; } else if (is_integer(ts)) { - throw std::runtime_error("Can't convert " + in->print() + " (an integer) to a float."); + auto fe = get_parent_env_of_type(env); + auto result = fe->make_xmm(m_ts.make_typespec("float")); + env->emit(std::make_unique(result, in->to_gpr(env))); + return result; } else { throw std::runtime_error("Can't convert " + in->print() + " a float."); } @@ -114,6 +124,19 @@ Val* Compiler::compile_add(const goos::Object& form, const goos::Object& rest, E } return result; } + + case MATH_FLOAT: { + auto result = env->make_xmm(first_type); + env->emit(std::make_unique(result, first_val->to_xmm(env))); + + for (size_t i = 1; i < args.unnamed.size(); i++) { + env->emit(std::make_unique( + FloatMathKind::ADD_SS, result, + to_math_type(compile_error_guard(args.unnamed.at(i), env), math_type, env) + ->to_xmm(env))); + } + return result; + } case MATH_INVALID: throw_compile_error( form, "Cannot determine the math mode for object of type " + first_type.print()); @@ -204,6 +227,30 @@ Val* Compiler::compile_sub(const goos::Object& form, const goos::Object& rest, E return result; } + case MATH_FLOAT: + if (args.unnamed.size() == 1) { + auto result = + compile_float(0, env, get_parent_env_of_type(env)->segment)->to_xmm(env); + env->emit(std::make_unique( + FloatMathKind::SUB_SS, result, + to_math_type(compile_error_guard(args.unnamed.at(0), env), math_type, env) + ->to_xmm(env))); + return result; + } else { + auto result = env->make_xmm(first_type); + env->emit(std::make_unique( + result, to_math_type(compile_error_guard(args.unnamed.at(0), env), math_type, env) + ->to_xmm(env))); + + for (size_t i = 1; i < args.unnamed.size(); i++) { + env->emit(std::make_unique( + FloatMathKind::SUB_SS, result, + to_math_type(compile_error_guard(args.unnamed.at(i), env), math_type, env) + ->to_xmm(env))); + } + return result; + } + case MATH_INVALID: throw_compile_error( form, "Cannot determine the math mode for object of type " + first_type.print()); diff --git a/goalc/compiler/compilation/Type.cpp b/goalc/compiler/compilation/Type.cpp index 681cb8363..86a55ea53 100644 --- a/goalc/compiler/compilation/Type.cpp +++ b/goalc/compiler/compilation/Type.cpp @@ -184,8 +184,6 @@ Val* Compiler::compile_defmethod(const goos::Object& form, const goos::Object& _ if (result) { auto final_result = result->to_gpr(new_func_env.get()); new_func_env->emit(std::make_unique(return_reg, final_result)); - printf("return type is %s from %s from %s\n", final_result->type().print().c_str(), - final_result->print().c_str(), result->print().c_str()); lambda_ts.add_arg(final_result->type()); } else { lambda_ts.add_arg(m_ts.make_typespec("none")); @@ -201,7 +199,8 @@ Val* Compiler::compile_defmethod(const goos::Object& form, const goos::Object& _ } place->set_type(lambda_ts); - auto info = m_ts.add_method(symbol_string(type_name), symbol_string(method_name), lambda_ts); + auto info = + m_ts.add_method(symbol_string(type_name), symbol_string(method_name), lambda_ts, false); auto type_obj = compile_get_symbol_value(symbol_string(type_name), env)->to_gpr(env); auto id_val = compile_integer(info.id, env)->to_gpr(env); auto method_val = place->to_gpr(env); @@ -309,15 +308,17 @@ Val* Compiler::compile_the(const goos::Object& form, const goos::Object& rest, E if (is_number(base->type())) { if (m_ts.typecheck(m_ts.make_typespec("binteger"), desired_ts, "", false, false)) { - throw std::runtime_error("the convert to binteger not yet supported"); + return number_to_binteger(base, env); } if (m_ts.typecheck(m_ts.make_typespec("integer"), desired_ts, "", false, false)) { - throw std::runtime_error("the convert to integer not yet supported"); + auto result = number_to_integer(base, env); + result->set_type(desired_ts); + return result; } if (m_ts.typecheck(m_ts.make_typespec("float"), desired_ts, "", false, false)) { - throw std::runtime_error("the convert to float not yet supported"); + return number_to_float(base, env); } } @@ -374,16 +375,22 @@ Val* Compiler::compile_car(const goos::Object& form, const goos::Object& rest, E auto args = get_va(form, rest); va_check(form, args, {{}}, {}); auto fe = get_parent_env_of_type(env); - return fe->alloc_val(m_ts.make_typespec("object"), - compile_error_guard(args.unnamed.at(0), env), true); + auto pair = compile_error_guard(args.unnamed.at(0), env); + if (pair->type() != m_ts.make_typespec("object")) { + typecheck(form, m_ts.make_typespec("pair"), pair->type(), "Type of argument to car"); + } + return fe->alloc_val(m_ts.make_typespec("object"), pair, true); } Val* Compiler::compile_cdr(const goos::Object& form, const goos::Object& rest, Env* env) { auto args = get_va(form, rest); va_check(form, args, {{}}, {}); auto fe = get_parent_env_of_type(env); - return fe->alloc_val(m_ts.make_typespec("object"), - compile_error_guard(args.unnamed.at(0), env), false); + auto pair = compile_error_guard(args.unnamed.at(0), env); + if (pair->type() != m_ts.make_typespec("object")) { + typecheck(form, m_ts.make_typespec("pair"), pair->type(), "Type of argument to cdr"); + } + return fe->alloc_val(m_ts.make_typespec("object"), pair, false); } // todo, consider splitting into method-of-object and method-of-type? diff --git a/goalc/emitter/IGen.h b/goalc/emitter/IGen.h index 8fe44f384..219e1efd1 100644 --- a/goalc/emitter/IGen.h +++ b/goalc/emitter/IGen.h @@ -1750,7 +1750,7 @@ class IGen { Instruction instr(0xf3); instr.set_op2(0x0f); instr.set_op3(0x2c); - instr.set_modrm_and_rex(dst.hw_id(), src.hw_id(), 3, false); + instr.set_modrm_and_rex(dst.hw_id(), src.hw_id(), 3, true); instr.swap_op0_rex(); return instr; } diff --git a/goalc/logger/Logger.cpp b/goalc/logger/Logger.cpp index 63e3596d2..e49bebf78 100644 --- a/goalc/logger/Logger.cpp +++ b/goalc/logger/Logger.cpp @@ -58,6 +58,7 @@ void Logger::log(LoggerMessageKind kind, const char* format, ...) { if (settings.color != COLOR_NORMAL) { printf("\033[0m"); + fflush(stdout); } // todo, does this make things slow? diff --git a/test/test_compiler_and_runtime.cpp b/test/test_compiler_and_runtime.cpp index 33673c812..8fb7b481a 100644 --- a/test/test_compiler_and_runtime.cpp +++ b/test/test_compiler_and_runtime.cpp @@ -111,6 +111,10 @@ struct CompilerTestRunner { } }; +std::vector get_test_pass_string(const std::string& name, int n_tests) { + return {fmt::format("Test \"{}\": {} Passes\n0\n", name, n_tests)}; +} + } // namespace TEST(CompilerAndRuntime, BuildGameAndTest) { @@ -157,6 +161,14 @@ TEST(CompilerAndRuntime, BuildGameAndTest) { runner.run_test("test-insert-cons.gc", {"((c . w) (a . b) (e . f))\n0\n"}); runner.run_test("test-new-inline-array-class.gc", {"2820\n"}); runner.run_test("test-memcpy.gc", {"13\n"}); + runner.run_test("test-memset.gc", {"11\n"}); + runner.run_test("test-binteger-print.gc", {"-17\n0\n"}); + runner.run_test("test-tests.gc", {"Test Failed On Test 0: \"unknown\"\nTest Failed On Test 0: " + "\"test\"\nTest \"test-of-test\": 1 Passes\n0\n"}); + runner.run_test("test-type-arrays.gc", {"Test \"test-type-arrays\": 3 Passes\n0\n"}); + runner.run_test("test-number-comparison.gc", {"Test \"number-comparison\": 14 Passes\n0\n"}); + runner.run_test("test-approx-pi.gc", get_test_pass_string("approx-pi", 4)); + runner.run_test("test-dynamic-type.gc", get_test_pass_string("dynamic-type", 4)); runner.print_summary();