308 lines
9.2 KiB
Text
308 lines
9.2 KiB
Text
/*
|
|
Allows structs to be copied and assigned inline.
|
|
|
|
For example, copying a Vector3 while changing a single value:
|
|
old := Vector3.{ 10, 20, 30 }; // 10, 20, 30
|
|
new := with(old, .{ y = -20 }); // 10, -20, 30
|
|
*/
|
|
with :: (old: $T, $fields: Code, location := #caller_location) -> T
|
|
#modify { return T.(*Type_Info).type == .STRUCT, "with can only be used on structs"; }
|
|
#expand {
|
|
#insert,scope() -> string {
|
|
using compiler;
|
|
|
|
ensure :: (cond: bool, message: string, args: ..Any, loc := location) {
|
|
if !cond compiler_report(basic.tprint(message, ..args), loc);
|
|
}
|
|
|
|
b: basic.String_Builder;
|
|
root := compiler_get_nodes(fields).(*Code_Literal);
|
|
ensure(root.kind == .LITERAL, "argument must be a struct literal");
|
|
ensure(root.value_type == .STRUCT, "argument must be a struct literal");
|
|
|
|
t_info := T.(*Type_Info_Struct);
|
|
|
|
// Ensure the literal we were given is of type T so this operator is more predictable.
|
|
// i.e. disallowing Vector3.{} | SomeRandomType.{ x = 10 };
|
|
n_type := root.struct_literal_info.type_expression;
|
|
if n_type != null {
|
|
tmp: basic.String_Builder;
|
|
pp.print_expression(*tmp, n_type);
|
|
n_typename := basic.builder_to_string(*tmp);
|
|
ensure(n_type.result == t_info, "mismatched types, % vs. %", t_info.name, n_typename);
|
|
}
|
|
|
|
// @note(judah): if the value we're trying to copy is a constant,
|
|
// we have to create a local so we can assign to the fields.
|
|
// Doing this allows this usecase: 'with(Default_Entity, .{ kind = .Player })'
|
|
receiver := "old";
|
|
#if is_constant(old) {
|
|
receiver = "copy";
|
|
basic.print_to_builder(*b, "% := old;\n", receiver);
|
|
}
|
|
|
|
for root.struct_literal_info.arguments {
|
|
op := it.(*Code_Binary_Operator);
|
|
ident := op.left.(*Code_Ident);
|
|
|
|
ensure(op.kind == .BINARY_OPERATOR, "copy-assign requires named fields", loc = make_location(op));
|
|
ensure(op.operator_type == #char "=", "copy-assign requires named field assignment", loc = make_location(op));
|
|
ensure(ident.kind == .IDENT, "must be an identifier", loc = make_location(op));
|
|
|
|
// Catch any incorrect field assignments before the compiler does for better error reporting
|
|
exists := false;
|
|
for t_info.members if it.name == ident.name exists = true;
|
|
ensure(exists, "field % does not exist within %", ident.name, t_info.name, loc = make_location(ident));
|
|
|
|
// receiver.field = value;
|
|
basic.append(*b, receiver);
|
|
basic.append(*b, ".");
|
|
pp.print_expression(*b, op);
|
|
basic.append(*b, ";\n");
|
|
}
|
|
|
|
basic.print_to_builder(*b, "return %;\n", receiver);
|
|
return basic.builder_to_string(*b);
|
|
}
|
|
}
|
|
|
|
// @note(judah): I like doing this with an operator, but your mileage may vary
|
|
operator | :: with;
|
|
|
|
|
|
/*
|
|
Creates a named block that can exit early (via 'break' or 'continue').
|
|
|
|
This mostly replaces the case where you'd like to jump to
|
|
the end of a scope based on some logic within. Without
|
|
gotos, this is the next best thing.
|
|
|
|
Usage:
|
|
// within a loop
|
|
for this_block() { // this is named 'block' by default
|
|
if !moving break;
|
|
// do movement here
|
|
}
|
|
for this_block("render_player") {
|
|
if invisible break render_player;
|
|
// do rendering here
|
|
}
|
|
*/
|
|
this_block :: ($name: string = Default_Name) -> Named_Block(name) #expand { return .{}; }
|
|
|
|
/*
|
|
Drop-in loop unrolling macro.
|
|
|
|
Usage:
|
|
for unroll(5) {
|
|
// duplicates this body 5 times exactly
|
|
}
|
|
|
|
known_size: [3]float;
|
|
for unroll(known_size) {
|
|
// duplicates this body 3 times exactly
|
|
}
|
|
|
|
var_size: []float;
|
|
for unroll(var_size) {
|
|
// duplicates this body a set number of times,
|
|
// falling back to a regular for loop to handle
|
|
// the remaining iterations.
|
|
}
|
|
*/
|
|
unroll :: ($count: int) -> Unrolled_Loop(count) { return .{}; }
|
|
unroll :: (arr: [$N]$T) -> Unrolled_Loop(N, T) { return .{ array = arr }; }
|
|
unroll :: (arr: []$T) -> Unrolled_Loop(-1, T) { return .{ array = arr }; }
|
|
|
|
// Call #c_call procedures inline with the current context: 'c_call(some_c_call_proc(10, 20))'
|
|
c_call :: (call: Code) #expand {
|
|
push_context context { #insert,scope(call) call; }
|
|
}
|
|
|
|
// Call #c_call procedures inline with a custom context: 'c_call(some_c_call_proc(10, 20), c_context)'
|
|
c_call :: (call: Code, ctx: #Context) #expand {
|
|
push_context ctx { #insert,scope(call) call; }
|
|
}
|
|
|
|
// @note(judah): for_expansions have to be exported
|
|
|
|
for_expansion :: (v: *Named_Block, code: Code, _: For_Flags) #expand {
|
|
#insert #run basic.tprint(#string END
|
|
for `%: 0..0 {
|
|
`it :: #run mem.zero_of(void);
|
|
`it_index :: #run mem.zero_of(void);
|
|
#insert,scope(code) code;
|
|
}
|
|
END,
|
|
// @note(judah): guards against calling this_block with
|
|
// an empty string which results in weird error messages.
|
|
ifx v.NAME.count != 0 v.NAME else Default_Name);
|
|
}
|
|
|
|
for_expansion :: (loop: *Unrolled_Loop, body: Code, flags: For_Flags, loc := #caller_location) #expand {
|
|
#assert flags & .REVERSE == 0 "reverse iteration not supported with loop unrolling (for now)";
|
|
#assert flags & .POINTER == 0 "pointer iteration not supported with loop unrolling (for now)";
|
|
|
|
// runtime unroll
|
|
#if loop.N == -1 {
|
|
for i: 0..loop.array.count - 1 {
|
|
`it := #no_abc loop.array[i];
|
|
`it_index := i;
|
|
#insert,scope(body) body;
|
|
}
|
|
|
|
// @todo(judah): below doesn't properly handle counts not divisible by 4,
|
|
// so we end up going over the bounds of the array.
|
|
|
|
// UNROLL_AMOUNT :: 4; // @todo(judah): make this configurable?
|
|
// unrolled_loops := loop.array.count / UNROLL_AMOUNT;
|
|
// remainder_loops := loop.array.count % UNROLL_AMOUNT;
|
|
// `it: loop.T;
|
|
// for i: 0..unrolled_loops #no_abc {
|
|
// basic.print("I: %\n", i);
|
|
// index := i * UNROLL_AMOUNT;
|
|
|
|
// it_index = index + 0;
|
|
// it = loop.array[it_index];
|
|
// #insert,scope(body) body;
|
|
|
|
// it_index = index + 1;
|
|
// it = loop.array[it_index];
|
|
// #insert,scope(body) body;
|
|
|
|
// it_index = index + 2;
|
|
// it = loop.array[it_index];
|
|
// #insert,scope(body) body;
|
|
|
|
// it_index = index + 3;
|
|
// it = loop.array[it_index];
|
|
// #insert,scope(body) body;
|
|
// }
|
|
|
|
// after_big_loop := it_index;
|
|
// for i: 0..remainder_loops #no_abc {
|
|
// it_index = after_big_loop + i;
|
|
// it = loop.array[it_index];
|
|
// #insert,scope(body) body;
|
|
// }
|
|
}
|
|
// compile-time unroll
|
|
else {
|
|
`it_index := 0;
|
|
|
|
#insert -> string {
|
|
b: basic.String_Builder;
|
|
basic.print_to_builder(*b, "// inserted unrolled loop (N = %) at %:%\n", loop.N, loc.fully_pathed_filename, loc.line_number);
|
|
|
|
if loop.T == void {
|
|
basic.append(*b, "`it: int = ---;\n");
|
|
}
|
|
else {
|
|
basic.append(*b, "`it: loop.T = ---;\n");
|
|
}
|
|
|
|
for 0..loop.N - 1 {
|
|
basic.append(*b, "{\n");
|
|
|
|
if loop.T == void {
|
|
basic.print_to_builder(*b, "\tit = %;\n", it);
|
|
}
|
|
else {
|
|
basic.print_to_builder(*b, "\tit = #no_abc loop.array[%];\n", it);
|
|
}
|
|
|
|
basic.print_to_builder(*b, "\tit_index = %;\n", it);
|
|
basic.append(*b, "\t#insert,scope(body) body;\n");
|
|
basic.append(*b, "}\n");
|
|
}
|
|
|
|
return basic.builder_to_string(*b);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
#scope_file;
|
|
|
|
Default_Name :: "block";
|
|
Named_Block :: struct(NAME: string) {}
|
|
|
|
Unrolled_Loop :: struct(N: int, T: Type = void) {
|
|
// Only store arrays when we absolutely have to.
|
|
#if T != void {
|
|
// @todo(judah): because this will only be created via 'unroll',
|
|
// should these be pointers to the underlying arrays so we don't
|
|
// pay for a copy?
|
|
#if N == -1 {
|
|
array: []T = ---;
|
|
}
|
|
else {
|
|
array: [N]T = ---;
|
|
}
|
|
}
|
|
}
|
|
|
|
basic :: #import "Basic"; // @future
|
|
pp :: #import "Program_Print"; // @future
|
|
compiler :: #import "Compiler"; // @future
|
|
|
|
|
|
// ----------------------------------------------------------
|
|
// TESTS
|
|
// ----------------------------------------------------------
|
|
|
|
#if RUN_TESTS #run {
|
|
test.run("this_block", (t) => {
|
|
i := 0;
|
|
|
|
for this_block() {
|
|
i += 1;
|
|
for this_block() {
|
|
break block;
|
|
}
|
|
|
|
for this_block() {
|
|
continue block;
|
|
}
|
|
|
|
if i == 1 {
|
|
break block;
|
|
}
|
|
|
|
i += 2;
|
|
}
|
|
|
|
j := 0;
|
|
for this_block("named") {
|
|
for 0..10 {
|
|
break;
|
|
}
|
|
|
|
if i != 1 {
|
|
break named;
|
|
}
|
|
|
|
j = 1;
|
|
}
|
|
|
|
test.expect(t, i == 1, "i was %", i);
|
|
test.expect(t, j == 1, "j was %", j);
|
|
});
|
|
|
|
test.run("copy assign", (t) => {
|
|
Value :: struct {
|
|
x: float;
|
|
y: float;
|
|
z: float;
|
|
}
|
|
|
|
a := Value.{ 10, 20, 30 };
|
|
b := with(a, .{ x = 1, z = 1 });
|
|
c := b | .{ y = 500 };
|
|
|
|
test.expect(t, b.x == 1, "was %", b.x);
|
|
test.expect(t, b.z == 1, "was %", b.z);
|
|
test.expect(t, c.y == 500, "was %", c.y);
|
|
});
|
|
}
|
|
|