/* 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 = ---; } } } #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); }); } mem :: #import,file "../memory/module.jai"; basic :: #import "Basic"; // @future pp :: #import "Program_Print"; // @future compiler :: #import "Compiler"; // @future