From ed6c32291731a7b4296e7d088e38d98e88140eb7 Mon Sep 17 00:00:00 2001 From: chloe caruso Date: Sat, 27 Jul 2024 02:27:50 -0700 Subject: [PATCH] feat: implement result types and inferred struct literals (#76) result types (not to be confused with Rust's `Result`) are the semantics of being able to reflect the type of an expression at compile time before it is analyzed. this is how inferred struct literals can figure out what type they are before the actual coercion happens. --- CHANGELOG.md | 2 + build.zig | 1 + doc/language-manual.md | 7 +- src/core/Ast.zig | 13 +- src/core/Parse.zig | 54 +- src/core/Report.zig | 90 +-- src/core/Sema.zig | 207 +++++-- src/core/StoredTrace.zig | 65 ++ src/core/Teg.Dump.zig | 64 +- src/core/Teg.zig | 626 +++++++++++--------- src/core/Transform.zig | 1208 +++++++++++++++++++++----------------- src/core/main.zig | 5 - src/core/mod.zig | 3 + 13 files changed, 1335 insertions(+), 1010 deletions(-) create mode 100644 src/core/StoredTrace.zig diff --git a/CHANGELOG.md b/CHANGELOG.md index ff2f16993cefa6a4e096634687b07948833207b5..203b85d4b1f119d04100705c5470f32f9d610dec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ ## 0.1.5 - feat: clarify "defined runtime representation" and "can be represented at runtime" +- feat: implement inferred structs + result type semantics +- chore: remove group expressions, use single expression blocks instead - fix(sema): fix passing structs as function params - fix: diagnostic box drawing characters line up after line 9 - fix: diagnostics on last line of file print correctly diff --git a/build.zig b/build.zig index 1d89a00731183659d272831ae7928c5871cf622a..54c19b08bf70601da6aa4e9b8020b6b13dc4da79 100644 --- a/build.zig +++ b/build.zig @@ -103,6 +103,7 @@ pub fn build(b: *std.Build) !void { const manual_out = manual_run.addOutputFileArg("language-reference.html"); const install_manual = b.addInstallFile(manual_out, "doc/README.html"); b.step("manual", "build language reference").dependOn(&install_manual.step); + b.install_tls.step.dependOn(&install_manual.step); // const wasm_api = b.addExecutable(.{ // .name = "compiler", diff --git a/doc/language-manual.md b/doc/language-manual.md index 7ba4ed14cb4f7d4c96bb6823c8507c5867504919..3a03a2dca825a00ef1861210225be936d15449d6 100644 --- a/doc/language-manual.md +++ b/doc/language-manual.md @@ -176,13 +176,12 @@ print(center.x, center.y); fn addPositions(a: Position, b: Position) -> Position { // To allow less verbose code, the struct type can be inferred with `.` - // TODO: that is not implemented ;-; - Position{ x: a.x + b.x, y: a.y + b.y } + .{ x: a.x + b.x, y: a.y + b.y } } let added = addPositions( - Position{ x: 1, y: -4 }, - Position{ x: 5, y: 8 }, + .{ x: 1, y: -4 }, + .{ x: 5, y: 8 }, ); print("Add result:", added.x, added.y); ``` diff --git a/src/core/Ast.zig b/src/core/Ast.zig index a80f54645797091cf27f3f3f8f3456835a367b40..788346e7ec2f57c55194232e3a3d420e0127313a 100644 --- a/src/core/Ast.zig +++ b/src/core/Ast.zig @@ -1,7 +1,7 @@ //! The Abstract Syntax Tree preserves most details of the source code, //! as it is used for formatting. Before semantic analysis, this //! structure is normalized into `Teg`, which removes constructs like -//! grouping, unneeded let bindings, and other transforms. +//! whitespace, unneeded let bindings, and other transforms. //! //! `ast.nodes` contains nodes stored as three parts: //! - tag: the type of the node @@ -130,10 +130,6 @@ pub const Node = struct { /// main_token is the `break` /// data is .@"break" @"break", - /// `(expr)` - /// main_token is the `(` - /// data is .un - group, /// `lhs + rhs`. main_token is the operator. add, /// `lhs - rhs`. main_token is the operator. @@ -222,6 +218,10 @@ pub const Node = struct { array_type, /// Array initialization array_init, + /// Tuple type + tuple_type, + /// Tuple Init + tuple_init, pub fn isAssignBin(op: Tag) bool { return switch (op) { @@ -309,7 +309,6 @@ pub const Node = struct { .fn_decl = .function, .fn_expr = .function, .ge = .bin, - .group = .un, .gt = .bin, .id = .none, .index = .bin, @@ -344,6 +343,8 @@ pub const Node = struct { .sub = .bin, .sub_assign = .bin, .var_decl = .var_decl, + .tuple_type = .list, + .tuple_init = .list, // sort-lines:end }); diff --git a/src/core/Parse.zig b/src/core/Parse.zig index cea204e7dbcd9ae887bd7a7b9faee639d418d3f1..039fb77d812ea42d4285774c7406179fe225c896 100644 --- a/src/core/Parse.zig +++ b/src/core/Parse.zig @@ -31,7 +31,7 @@ pub fn parse(gpa: std.mem.Allocator, source: [:0]const u8, ast_error: *AstError. .err = err, .byte = err_byte, } }, - .trace = Report.DebugStoredTrace.from(@errorReturnTrace()), + .trace = DebugStoredTrace.from(@errorReturnTrace()), }; return error.ParseFail; }, @@ -143,7 +143,7 @@ pub const AstError = union(enum) { array_type_flipped_hint: Token, - pub fn toReport(err: AstError, comp: *Comp, file: *Comp.File, trace: Report.DebugStoredTrace) !Report { + pub fn toReport(err: AstError, comp: *Comp, file: *Comp.File, trace: DebugStoredTrace) !Report { return (Report.Err{ .ast = err }).toReport(comp, file, trace); } @@ -193,7 +193,7 @@ pub const AstError = union(enum) { pub const WithTrace = struct { err: AstError, - trace: Report.DebugStoredTrace, + trace: DebugStoredTrace, pub fn toReport(err: WithTrace, comp: *Comp, file: *Comp.File) !Report { return err.err.toReport(comp, file, err.trace); @@ -856,7 +856,7 @@ fn parseExpressionPart(p: *Parse, mode: ParseExprMode) Error!NodeIndex { .kw_return => try p.parseReturn(), .start_brace => try p.parseBlock(), .start_bracket => try p.parseArrayValueOrType(mode), - .start_paren => try p.parseGroup(), + .start_paren => try p.parseTupleType(), // Unary operators inline .bang, @@ -1136,25 +1136,6 @@ fn parseElseValue(p: *Parse) Error!NodeIndex { }; } -fn parseGroup(p: *Parse) Error!NodeIndex { - const trace = t.trace(@src()); - defer trace.end(); - - const main_token = p.assertToken(.start_paren); - - const expr = try p.parseExpression(.expression); - - _ = try p.expect(.end_paren); - - return try p.addNode(.{ - .tag = .group, - .main_token = main_token, - .data = .{ .un = .{ - .expr = expr, - } }, - }); -} - fn parseAnonInstantiation(p: *Parse) Error!NodeIndex { const trace = t.trace(@src()); defer trace.end(); @@ -1266,6 +1247,16 @@ fn parseArrayValueOrType(p: *Parse, mode: ParseExprMode) Error!NodeIndex { }); } +fn parseTupleType(p: *Parse) Error!NodeIndex { + const main_token = p.assertToken(.start_bracket); + const list = try p.parseExpressionList(.end_paren, .expression); + return p.addNode(.{ + .tag = .tuple_type, + .main_token = main_token, + .data = .{ .list = list }, + }); +} + // helpers /// Append a node and return its index. @@ -1292,7 +1283,7 @@ fn addExtra(p: *Parse, comptime T: type, extra: T) !ExtraIndex { fn fail(p: *Parse, ast_error: AstError) Error { p.ast_error = .{ .err = ast_error, - .trace = Report.DebugStoredTrace.capture(@returnAddress()), + .trace = DebugStoredTrace.capture(@returnAddress()), }; return Error.ParseFail; } @@ -1366,10 +1357,6 @@ fn nextToken(p: *Parse) TokenIndex { return result; } -fn tokensOnSameLine(p: *Parse, token1: TokenIndex, token2: TokenIndex) bool { - return std.mem.indexOfScalar(u8, p.source[p.token_starts[token1]..p.token_starts[token2]], '\n') == null; -} - fn fullToken(p: *Parse, token: TokenIndex) Token { return Token{ .tag = p.token_tags[token], @@ -1398,11 +1385,14 @@ const mem = std.mem; const Allocator = mem.Allocator; const ArrayListUnmanaged = std.ArrayListUnmanaged; -const Tokenizer = @import("Tokenizer.zig"); +const core = @import("mod.zig"); +const DebugStoredTrace = core.DebugStoredTrace; + +const Tokenizer = core.Tokenizer; const Token = Tokenizer.Token; const ByteOffset = Tokenizer.ByteOffset; -const Ast = @import("Ast.zig"); +const Ast = core.Ast; const Node = Ast.Node; const Data = Node.Data; const SubList = Ast.SubList; @@ -1418,5 +1408,5 @@ const optionalExtraIndex = OptionalExtraIndex.init; const t = @import("tracer"); -const Report = @import("Report.zig"); -const Comp = @import("Comp.zig"); +const Report = core.Report; +const Comp = core.Comp; diff --git a/src/core/Report.zig b/src/core/Report.zig index 86c8df1f8bf7d39d1f66ff2819448a91ebb0a289..18fc277abcce5dd1e39f0ea8cf9aab2f7a264ff5 100644 --- a/src/core/Report.zig +++ b/src/core/Report.zig @@ -135,6 +135,17 @@ pub const Err = union(enum) { ); } }, + missing_result_type: struct { + node: NodeIndex, + + fn build(data: @This(), ctx: *MsgContext) !void { + try ctx.msgNode( + data.node, + "Cannot infer struct initialization type", + .{}, + ); + } + }, unresolved_label: struct { node: NodeIndex, @@ -523,62 +534,6 @@ pub const Warn = union(enum) { }, }; -/// A variant of std.builtin.StackTrace that stores its data within itself -/// instead of being a pointer. This allows capturing the stack trace and -/// saving it for later printing. -pub const StoredTrace = struct { - data: [32]usize, - index: usize, - - pub const empty = .{ - .data = .{0} ** 32, - .index = 0, - }; - - fn trace(stored: *StoredTrace) std.builtin.StackTrace { - return .{ - .index = stored.index, - .instruction_addresses = &stored.data, - }; - } - - pub fn capture(begin: ?usize) StoredTrace { - var stored: StoredTrace = undefined; - var frame = stored.trace(); - std.debug.captureStackTrace(begin orelse @returnAddress(), &frame); - stored.index = frame.index; - return stored; - } - - pub fn from(stack_trace: ?*std.builtin.StackTrace) StoredTrace { - if (stack_trace) |stack| { - var data: [32]usize = undefined; - @memset(&data, 0); - const items = @min(stack.instruction_addresses.len, 32); - @memcpy(data[0..items], stack.instruction_addresses[0..items]); - return .{ - .data = data, - .index = @min(items, stack.index), - }; - } else { - return empty; - } - } -}; - -pub const DebugStoredTrace = if (builtin.mode == .Debug) - StoredTrace -else - struct { - pub fn capture(_: ?usize) @This() { - return .{}; - } - - pub fn from(_: ?*std.builtin.StackTrace) @This() { - return .{}; - } - }; - pub const UnusedKind = enum { param, constant, @@ -817,7 +772,7 @@ pub const Builder = struct { try b.errors.append(b.gpa, .{ .data = .{ .@"error" = @unionInit(Err, @tagName(kind), data) }, .url = try b.currentFileString(), - .debug_stack_trace = if (builtin.mode == .Debug) StoredTrace.capture(@returnAddress()) else .{}, + .debug_stack_trace = DebugStoredTrace.capture(@returnAddress()), }); } @@ -825,7 +780,7 @@ pub const Builder = struct { try b.warnings.append(b.gpa, .{ .data = .{ .warn = @unionInit(Warn, @tagName(kind), data) }, .url = try b.currentFileString(), - .debug_stack_trace = if (builtin.mode == .Debug) StoredTrace.capture(@returnAddress()) else .{}, + .debug_stack_trace = DebugStoredTrace.capture(@returnAddress()), }); } @@ -1050,7 +1005,7 @@ pub const LazyNode = struct { const node_tag = ctx.ast.nodeTag(base_node); switch (teg.instTag(lazy.inst)) { - .as => switch (node_tag) { + .coercion => switch (node_tag) { .var_decl => switch (lazy.operand) { .lhs => return data.var_decl.type.unwrap().?, .rhs => return data.var_decl.init, @@ -1121,6 +1076,10 @@ pub const LazyNode = struct { const items = data.list.slice(ctx.ast); return items[lazy.operand.getArgId()]; }, + .field => { + assert(node_tag == .field); + return base_node; + }, else => {}, } @@ -1358,19 +1317,22 @@ const builtin = @import("builtin"); const unicode_props = @import("unicode_props.zig"); -const Tokenizer = @import("Tokenizer.zig"); +const core = @import("mod.zig"); +const DebugStoredTrace = core.DebugStoredTrace; + +const Tokenizer = core.Tokenizer; const ByteOffset = Tokenizer.ByteOffset; -const Ast = @import("Ast.zig"); +const Ast = core.Ast; const TokenIndex = Ast.TokenIndex; const NodeIndex = Ast.NodeIndex; const Location = Ast.Location; -const Teg = @import("Teg.zig"); +const Teg = core.Teg; -const Comp = @import("Comp.zig"); +const Comp = core.Comp; const File = Comp.File; -const InternPool = @import("InternPool.zig"); +const InternPool = core.InternPool; const Value = InternPool.Value; const Type = InternPool.Type; diff --git a/src/core/Sema.zig b/src/core/Sema.zig index d54ba755c8d44f1c23211e7d2cb63e1d2263f644..4a3922ab18ee7b83a2b3510e5da7552429f15199 100644 --- a/src/core/Sema.zig +++ b/src/core/Sema.zig @@ -66,24 +66,30 @@ pub const Error = error{ OutOfMemory, } || Comp.File.GetTegError; -/// Instead of using a Hash map for this, just pre-allocate a list long enough to hold everything. -/// This is fine because all instructions are going to be adjacent in a single declaration. +/// Instead of using a Hash map for this, all of the information is pre-allocated. +/// This is only possible because all instructions are adjacent in Teg. const InstMap = struct { items: []Air.Inst.Ref.Optional, start: Teg.Inst.Index, + debug_traces: if (std.debug.runtime_safety) []StoredTrace else void, pub fn init(allocator: Allocator, code: *const Teg, inst_start: Teg.Inst.Index) !InstMap { const decl = code.declData(inst_start); const insts = try allocator.alloc(Air.Inst.Ref.Optional, decl.instruction_count); + const debug_traces = if (std.debug.runtime_safety) + try allocator.alloc(StoredTrace, decl.instruction_count); @memset(insts, .none); return .{ .items = insts, .start = inst_start, + .debug_traces = debug_traces, }; } pub fn deinit(map: *InstMap, allocator: Allocator) void { allocator.free(map.items); + if (std.debug.runtime_safety) + allocator.free(map.debug_traces); map.* = undefined; } @@ -95,16 +101,21 @@ const InstMap = struct { } pub fn add(map: *InstMap, from: Teg.Inst.Index, to: Air.Inst.Ref) !void { + const source_i = @intFromEnum(from); + const start_i = @intFromEnum(map.start); + const index = source_i - start_i; if (assertFailed(map.get(from) == null)) { const s: *const Sema = @fieldParentPtr("inst_map", map); + std.debug.print("Original was added here:\n", .{}); + std.debug.dumpStackTrace(map.debug_traces[index].trace()); std.debug.panic("%{d} is already mapped (to {})", .{ @intFromEnum(from), map.get(from).?.dump(s.ip), }); } - const source_i = @intFromEnum(from); - const start_i = @intFromEnum(map.start); - map.items[source_i - start_i] = to.toOptional(); + map.items[index] = to.toOptional(); + if (std.debug.runtime_safety) + map.debug_traces[index] = StoredTrace.capture(@returnAddress()); } }; @@ -216,36 +227,44 @@ pub fn analyzeFunctionDeclaration(s: *Sema) Error!Value { s.params[i] = ty; } - const return_type = try s.resolveType( - s.inst_map.start.op(.return_type), - fn_decl.return_type, - ); + var return_type, const early_value = if (fn_decl.return_type.unwrap()) |ty| brk: { + const return_type = try s.resolveType( + s.inst_map.start.op(.return_type), + ty, + ); + const value = try s.makeValue(.{ + .func = .{ + .ty = try s.makeType(.{ + .function_type = .{ + .param_types = s.params, + .return_type = return_type, + }, + }), + .owner_decl = s.decl_index, + .teg_body_inst = s.decl.teg_decl_index, + }, + }); - const func_prototype = (try s.ip.get(s.gpa, .{ - .function_type = .{ - .param_types = s.params, - .return_type = return_type, - }, - })).toTypeUnsafe(s.ip); - const value = try s.ip.get(s.gpa, .{ - .func = .{ - .ty = func_prototype, - .owner_decl = s.decl_index, - .teg_body_inst = s.decl.teg_decl_index, - }, - }); + s.decl.raw_value = value.typeOf(s.ip).value(); + s.decl.analysis = .in_progress_type_available; - s.decl.raw_value = value.typeOf(s.ip).value(); - s.decl.analysis = .in_progress_type_available; + break :brk .{ return_type, value }; + } else .{ null, null }; const root_block = decl.body; switch (root_block.decode()) { .value => |val| { - const coerced = try s.coerceValue( - s.inst_map.start.op(.operand), - val, - return_type, - ); + const coerced = if (return_type) |ty| + try s.coerceValue( + s.inst_map.start.op(.operand), + val, + ty, + ) + else brk: { + return_type = val.typeOf(s.ip); + break :brk val; + }; + const block = try s.addBlock(); _ = try s.addInstRef(.@"break", .{ .block_ref = block.block_index, @@ -253,11 +272,30 @@ pub fn analyzeFunctionDeclaration(s: *Sema) Error!Value { }); }, .index => |index| { - try s.temp_result_map.putNoClobber(s.arena, index, return_type); - _ = try s.analyzeBlock(index); + if (return_type) |ty| { + try s.temp_result_map.putNoClobber(s.arena, index, ty); + } + + const out = try s.analyzeBlock(index); + + return_type = return_type orelse s.typeOfRef(out); }, } + const value = early_value orelse try s.makeValue(.{ + .func = .{ + .ty = try s.makeType(.{ + .function_type = .{ + .param_types = s.params, + .return_type = return_type orelse + unreachable, // type is reassigned above + }, + }), + .owner_decl = s.decl_index, + .teg_body_inst = s.decl.teg_decl_index, + }, + }); + s.decl.analysis = .complete; s.decl.raw_value = value; @@ -342,7 +380,7 @@ fn analyzeCodeInner(s: *Sema, in: Teg.Inst.Index) Error!Air.Inst.Ref { return s.addInstRef(.js_global_ref, operand.asString(s.ip)); }, .field => { - const field: Teg.Inst.Bin = s.code.instPayload(.field, in); + const field: Teg.Bin = s.code.instPayload(.field, in); const base = try s.resolveRef(field.lhs); const rhs = try s.resolveCoerceValue(in.op(.rhs), field.rhs, .string); @@ -402,7 +440,7 @@ fn analyzeCodeInner(s: *Sema, in: Teg.Inst.Index) Error!Air.Inst.Ref { }); }, .load_decl => { - const request: Teg.Inst.StrNode = s.code.instData(.load_decl, in); + const request: Teg.StrNode = s.code.instData(.load_decl, in); const decl_name = try s.ip.getOrPutString( s.gpa, request.string.slice(s.code.string_bytes), @@ -416,7 +454,7 @@ fn analyzeCodeInner(s: *Sema, in: Teg.Inst.Index) Error!Air.Inst.Ref { return resolved.toAir(); }, .param => { - const param: Teg.Inst.DataNode = s.code.instData(.param, in); + const param: Teg.DataNode = s.code.instData(.param, in); return s.addInstRef(.arg, .{ .type = s.params[param.data], .arg_number = param.data, @@ -430,7 +468,7 @@ fn analyzeCodeInner(s: *Sema, in: Teg.Inst.Index) Error!Air.Inst.Ref { .gt, .eq, => |tag| { - const bin: Teg.Inst.Bin = s.code.instPayload(tag, in); + const bin: Teg.Bin = s.code.instPayload(tag, in); const op = @field(BinaryOp, @tagName(tag)); const lhs = try s.resolveRef(bin.lhs); @@ -458,7 +496,7 @@ fn analyzeCodeInner(s: *Sema, in: Teg.Inst.Index) Error!Air.Inst.Ref { }); }, .bool_not => { - const un: Teg.Inst.RefNode = s.code.instData(.bool_not, in); + const un: Teg.RefNode = s.code.instData(.bool_not, in); const operand = try s.resolveCoerceRef(in.op(.operand), un.operand, .bool); @@ -474,7 +512,7 @@ fn analyzeCodeInner(s: *Sema, in: Teg.Inst.Index) Error!Air.Inst.Ref { return s.addInstRef(.bool_not, .{ .operand = operand }); }, .negate => { - const un: Teg.Inst.RefNode = s.code.instData(.negate, in); + const un: Teg.RefNode = s.code.instData(.negate, in); const operand = try s.resolveRef(un.operand); @@ -496,7 +534,7 @@ fn analyzeCodeInner(s: *Sema, in: Teg.Inst.Index) Error!Air.Inst.Ref { return s.addInstRef(.negate, .{ .operand = operand }); }, .@"break" => { - const brk: Teg.Inst.Break = s.code.instPayload(.@"break", in); + const brk: Teg.Break = s.code.instPayload(.@"break", in); const operand = try s.resolveRef(brk.operand); const block_ref = s.inst_map.get(brk.block).?.decode().index; // block ref is resolved by teg @@ -511,7 +549,7 @@ fn analyzeCodeInner(s: *Sema, in: Teg.Inst.Index) Error!Air.Inst.Ref { }); }, .call => { - const call: Teg.Inst.Call = s.code.instPayload(.call, in); + const call: Teg.Call = s.code.instPayload(.call, in); const base = try s.resolveRef(call.base); const base_type = s.typeOfRef(base); @@ -568,7 +606,7 @@ fn analyzeCodeInner(s: *Sema, in: Teg.Inst.Index) Error!Air.Inst.Ref { }); }, .js_new => { - const call: Teg.Inst.JsNew = s.code.instPayload(.js_new, in); + const call: Teg.JsNew = s.code.instPayload(.js_new, in); const base = try s.resolveRef(call.base); const scratch = try s.arena.alloc(Air.Inst.Ref, call.args.len); @@ -612,7 +650,7 @@ fn analyzeCodeInner(s: *Sema, in: Teg.Inst.Index) Error!Air.Inst.Ref { return InternPool.Value.void.toAir(); }, .@"if" => { - const data: Teg.Inst.If = s.code.instData(.@"if", in).extra_index.get(&s.code); + const data: Teg.If = s.code.instData(.@"if", in).extra_index.get(&s.code); const cond_raw = try s.resolveRef(data.cond); const cond = try s.coerceRef(in.op(.operand), cond_raw, .bool); @@ -661,7 +699,7 @@ fn analyzeCodeInner(s: *Sema, in: Teg.Inst.Index) Error!Air.Inst.Ref { }); }, .store => { - const data: Teg.Inst.Store = s.code.instPayload(.store, in); + const data: Teg.Store = s.code.instPayload(.store, in); assert(s.code.instTag(data.memory) == .memory); // corrupt store instruction const location = s.mapTeg(data.memory.toRef()).decode().index; @@ -677,24 +715,24 @@ fn analyzeCodeInner(s: *Sema, in: Teg.Inst.Index) Error!Air.Inst.Ref { .operand = operand_coerced, }); }, - .as => { - const as: Teg.Inst.As = s.code.instPayload(.as, in); + .coercion => { + const as: Teg.Coercion = s.code.instPayload(.coercion, in); const coerce_to = try s.resolveCoerceValue(in.op(.lhs), as.type, .type); const operand = try s.resolveRef(as.operand); return try s.coerceRef(in.op(.rhs), operand, coerce_to.toType(s.ip).?); }, .@"comptime" => { - const node: Teg.Inst.RefNode = s.code.instData(.@"comptime", in); + const node: Teg.RefNode = s.code.instData(.@"comptime", in); const value = try s.resolveValue(node.operand); return value.toAir(); }, .type_of => { - const node: Teg.Inst.RefNode = s.code.instData(.type_of, in); + const node: Teg.RefNode = s.code.instData(.type_of, in); const value = try s.resolveValue(node.operand); return value.typeOf(s.ip).value().toAir(); }, .compile_error => { - const node: Teg.Inst.RefNode = s.code.instData(.compile_error, in); + const node: Teg.RefNode = s.code.instData(.compile_error, in); const value = try s.resolveCoerceValue( in.op(operandArgument(0)), node.operand, @@ -707,7 +745,7 @@ fn analyzeCodeInner(s: *Sema, in: Teg.Inst.Index) Error!Air.Inst.Ref { }); }, .struct_decl => { - const node: Teg.Inst.StructDecl = s.code.instPayload(.struct_decl, in); + const node: Teg.StructDecl = s.code.instPayload(.struct_decl, in); const scratch = try s.scratch.reserveMulti( s.arena, @@ -732,7 +770,7 @@ fn analyzeCodeInner(s: *Sema, in: Teg.Inst.Index) Error!Air.Inst.Ref { } }); }, .struct_init => { - const node: Teg.Inst.StructInit = s.code.instPayload(.struct_init, in); + const node: Teg.StructInit = s.code.instPayload(.struct_init, in); // Ensure we have a valid struct to operate on const ty = try s.resolveType(in.op(.operand), node.base_type); @@ -835,9 +873,8 @@ fn analyzeCodeInner(s: *Sema, in: Teg.Inst.Index) Error!Air.Inst.Ref { }); } }, - .array_init => { - const node: Teg.Inst.ArrayInit = s.code.instPayload(.array_init, in); + const node: Teg.ArrayInit = s.code.instPayload(.array_init, in); assert(node.list.len > 0); @@ -894,13 +931,72 @@ fn analyzeCodeInner(s: *Sema, in: Teg.Inst.Index) Error!Air.Inst.Ref { }); } }, - .array_type => { - const un: Teg.Inst.RefNode = s.code.instData(.array_type, in); + const un: Teg.RefNode = s.code.instData(.array_type, in); const operand = try s.resolveType(in.op(.operand), un.operand); return s.makeValueAir(.{ .array_type = .{ .item_type = operand } }); }, + .result_type_argument => { + const result: Teg.ResultTypeArgument = s.code.instData(.result_type_argument, in); + + const operand = try s.resolveRef(result.call_base); + const ty = s.typeOfRef(operand); + + const func: InternPool.Key.FunctionType = ty.toFunctionPrototype(s.ip) orelse + return Value.opaque_type.toAir(); + + if (result.argument >= func.param_types.len) + return Value.opaque_type.toAir(); + + return func.param_types[result.argument].value().toAir(); + }, + + .result_type_array_value => { + const result: Teg.ResultTypeArrayValue = s.code.instData(.result_type_array_value, in); + + const operand = try s.resolveValue(result.type); + const ty = operand.toType(s.ip) orelse + return Value.opaque_type; + + const array_type = ty.toArrayType(s.ip) orelse + return Value.opaque_type.toAir(); + + return array_type.item_type.value().toAir(); + }, + + .result_type_tuple_field => { + const result: Teg.ResultTypeTupleField = s.code.instData(.result_type_array_value, in); + + const operand = try s.resolveValue(result.type); + const ty = operand.toType(s.ip) orelse + return Value.opaque_type; + + _ = ty; + + @panic("TODO: tuple result type resolution"); // tuples are not implemented + }, + + .result_type_struct_field => { + const result: Teg.ResultTypeStructField = s.code.instData(.result_type_struct_field, in); + + const operand = try s.resolveValue(result.type); + const ty = operand.toType(s.ip) orelse + return Value.opaque_type; + + const struct_type = ty.toStructType(s.ip) orelse + return Value.opaque_type.toAir(); + + const to_match = result.field.slice(s.code.string_bytes); + for (struct_type.field_keys, struct_type.field_types) |k, v| { + if (k.eqlSlice(to_match, s.ip)) { + return v.value().toAir(); + } + } + + return Value.opaque_type.toAir(); + }, + else => |tag| std.debug.panic("TODO: analyze of .{s}", .{@tagName(tag)}), } } @@ -1001,13 +1097,13 @@ fn resolveValue(s: *Sema, ref: Teg.Inst.Ref) !Value { const break_inst = indexes[1]; if (s.code.instTag(break_inst) != .@"break") break :special_cased; - const break_data: Teg.Inst.Break = s.code.instPayload(.@"break", break_inst); + const break_data: Teg.Break = s.code.instPayload(.@"break", break_inst); if (break_data.operand != load_inst.toRef()) break :special_cased; if (break_data.block != index) break :special_cased; if (s.code.instTag(load_inst) != .load_decl) break :special_cased; - const request: Teg.Inst.StrNode = s.code.instData(.load_decl, load_inst); + const request: Teg.StrNode = s.code.instData(.load_decl, load_inst); const decl_name = try s.ip.getOrPutString( s.gpa, request.string.slice(s.code.string_bytes), @@ -1620,6 +1716,7 @@ const DynamicBitSetUnmanaged = std.DynamicBitSetUnmanaged; const core = @import("mod.zig"); const assertFailed = core.assertFailed; +const StoredTrace = core.StoredTrace; const Comp = core.Comp; const File = Comp.File; diff --git a/src/core/StoredTrace.zig b/src/core/StoredTrace.zig new file mode 100644 index 0000000000000000000000000000000000000000..c8ca441a35b2a087ba9ebae134cfec15c49c96bc --- /dev/null +++ b/src/core/StoredTrace.zig @@ -0,0 +1,65 @@ +//! A variant of std.builtin.StackTrace that stores its data within itself +//! instead of being a pointer. This allows capturing the stack trace and +//! saving it for later printing. + +data: [31]usize, +index: usize, + +pub const empty = .{ + .data = .{0} ** 31, + .index = 0, +}; + +pub fn trace(stored: *StoredTrace) std.builtin.StackTrace { + return .{ + .index = stored.index, + .instruction_addresses = &stored.data, + }; +} + +pub fn capture(begin: ?usize) StoredTrace { + var stored: StoredTrace = undefined; + var frame = stored.trace(); + std.debug.captureStackTrace(begin orelse @returnAddress(), &frame); + stored.index = frame.index; + return stored; +} + +pub fn from(stack_trace: ?*std.builtin.StackTrace) StoredTrace { + if (stack_trace) |stack| { + var data: [31]usize = undefined; + @memset(&data, 0); + const items = @min(stack.instruction_addresses.len, 32); + @memcpy(data[0..items], stack.instruction_addresses[0..items]); + return .{ + .data = data, + .index = @min(items, stack.index), + }; + } else { + return empty; + } +} + +pub const BlankImpl = struct { + pub fn trace(_: *BlankImpl) std.builtin.StackTrace { + @compileError("trace is not available with StoredTrace.BlankImpl"); + } + + pub fn capture(_: ?usize) @This() { + return .{}; + } + + pub fn from(_: ?*std.builtin.StackTrace) @This() { + return .{}; + } +}; + +pub const DebugStoredTrace = if (builtin.mode == .Debug) + StoredTrace +else + BlankImpl; + +pub const StoredTrace = @This(); + +const std = @import("std"); +const builtin = @import("builtin"); diff --git a/src/core/Teg.Dump.zig b/src/core/Teg.Dump.zig index ebb02f0270df35a178c69589d84eba5f02d5a6f2..91c3f7556fedf350c5ef076b22e53674bf8f1c95 100644 --- a/src/core/Teg.Dump.zig +++ b/src/core/Teg.Dump.zig @@ -68,9 +68,11 @@ const State = struct { try writer.writeAll("params: "); try s.dumpRefList(writer, fn_data.params.slice(teg), '[', ']'); try s.newline(writer); - try writer.writeAll("return_type: "); - try s.dumpRef(writer, fn_data.return_type); - try s.newline(writer); + if (fn_data.return_type.unwrap()) |return_type| { + try writer.writeAll("return_type: "); + try s.dumpRef(writer, return_type); + try s.newline(writer); + } }, .immutable => {}, } @@ -129,8 +131,8 @@ const State = struct { try s.dumpRefMarker(writer, data.rhs); try writer.writeAll(")"); }, - .as => { - const data = teg.instData(.as, inst).extra_index.get(teg); + .coercion => { + const data = teg.instData(.coercion, inst).extra_index.get(teg); try writer.writeAll("("); try s.dumpRefMarker(writer, data.type); try writer.writeAll(", "); @@ -138,7 +140,7 @@ const State = struct { try writer.writeAll(")"); }, .@"break" => { - const data: Teg.Inst.Break = teg.instData(.@"break", inst).extra_index.get(teg); + const data: Teg.Break = teg.instData(.@"break", inst).extra_index.get(teg); try writer.writeAll("("); try s.dumpRefMarker(writer, data.block.toRef()); try writer.writeAll(", "); @@ -146,7 +148,7 @@ const State = struct { try writer.writeAll(")"); }, .store => { - const data: Teg.Inst.Store = teg.instData(.store, inst).extra_index.get(teg); + const data: Teg.Store = teg.instData(.store, inst).extra_index.get(teg); try writer.writeAll("("); try s.dumpRefMarker(writer, data.memory.toRef()); try writer.writeAll(", "); @@ -164,7 +166,6 @@ const State = struct { inline .memory, .discard, .ensure_void, - .ensure_bool, .negate, .bool_not, .bit_not, @@ -181,7 +182,7 @@ const State = struct { try writer.writeAll(")"); }, .call => { - const call: Teg.Inst.Call = teg.instData(.call, inst).extra_index.get(teg); + const call: Teg.Call = teg.instData(.call, inst).extra_index.get(teg); try writer.writeAll("("); try s.dumpRefMarker(writer, call.base); try writer.writeAll(","); @@ -189,7 +190,7 @@ const State = struct { try writer.writeAll(")"); }, .js_new => { - const call: Teg.Inst.JsNew = teg.instData(.js_new, inst).extra_index.get(teg); + const call: Teg.JsNew = teg.instData(.js_new, inst).extra_index.get(teg); try writer.writeAll("("); try s.dumpRefMarker(writer, call.base); try writer.writeAll(","); @@ -197,14 +198,14 @@ const State = struct { try writer.writeAll(")"); }, .load_decl => { - const call: Teg.Inst.StrNode = teg.instData(.load_decl, inst); + const call: Teg.StrNode = teg.instData(.load_decl, inst); try tty.setColor(writer, .red); try writer.print(" {s}", .{call.string.slice(teg.string_bytes)}); try tty.setColor(writer, .reset); }, .@"if" => { - const data: Teg.Inst.If = teg.instPayload(.@"if", inst); + const data: Teg.If = teg.instPayload(.@"if", inst); try writer.writeAll("("); try s.dumpRefMarker(writer, data.cond); try writer.writeAll(") {"); @@ -220,13 +221,13 @@ const State = struct { try writer.writeAll("}"); }, .@"comptime" => { - const data: Teg.Inst.RefNode = teg.instData(.@"comptime", inst); + const data: Teg.RefNode = teg.instData(.@"comptime", inst); try writer.writeAll("("); try s.dumpRef(writer, data.operand); try writer.writeAll(")"); }, .struct_decl => { - const data: Teg.Inst.StructDecl = teg.instPayload(.struct_decl, inst); + const data: Teg.StructDecl = teg.instPayload(.struct_decl, inst); try writer.writeAll(" {"); s.indent += 1; try s.newline(writer); @@ -241,7 +242,7 @@ const State = struct { try writer.writeAll("}"); }, .struct_init => { - const data: Teg.Inst.StructInit = teg.instPayload(.struct_init, inst); + const data: Teg.StructInit = teg.instPayload(.struct_init, inst); try writer.writeAll("("); try s.dumpRefMarker(writer, data.base_type); try writer.writeAll(") {"); @@ -258,7 +259,7 @@ const State = struct { try writer.writeAll("}"); }, .array_init => { - const data: Teg.Inst.ArrayInit = teg.instPayload(.array_init, inst); + const data: Teg.ArrayInit = teg.instPayload(.array_init, inst); if (data.type_hint.unwrap()) |type_hint| { try writer.writeAll("("); @@ -284,6 +285,37 @@ const State = struct { if (data.type_hint != .none) try writer.writeAll(")"); }, + .result_type_argument => { + const data: Teg.ResultTypeArgument = teg.instData(.result_type_argument, inst); + + try writer.writeAll("("); + try s.dumpRefMarker(writer, data.call_base); + try writer.print(", {d}", .{data.argument}); + try writer.writeAll(")"); + }, + .result_type_struct_field => { + const data: Teg.ResultTypeStructField = teg.instData(.result_type_struct_field, inst); + + try writer.writeAll("("); + try s.dumpRefMarker(writer, data.type); + try writer.print(", {s}", .{data.field.slice(s.teg.string_bytes)}); + try writer.writeAll(")"); + }, + .result_type_tuple_field => { + const data: Teg.ResultTypeTupleField = teg.instData(.result_type_tuple_field, inst); + + try writer.writeAll("("); + try s.dumpRefMarker(writer, data.type); + try writer.print(", {d}", .{data.item}); + try writer.writeAll(")"); + }, + .result_type_array_value => { + const data: Teg.ResultTypeArrayValue = teg.instData(.result_type_array_value, inst); + + try writer.writeAll("("); + try s.dumpRefMarker(writer, data.type); + try writer.writeAll(")"); + }, } } diff --git a/src/core/Teg.zig b/src/core/Teg.zig index d194ba986f8ba8a409e4c41bbd0640e40f4d8ca8..5916b860690a9213b6fac0fbdb8157e01d458ef4 100644 --- a/src/core/Teg.zig +++ b/src/core/Teg.zig @@ -6,184 +6,200 @@ extra: ExtraStore = .{}, report: Report, pub const ExtraStore = dod.ExtraStore(.teg, .@"const"); -pub const EncodedData = dod.EncodedData(Inst.Tag, u64); +pub const EncodedData = dod.EncodedData(Tag, u64); pub const ExtraIndex = ExtraStore.Index; pub const Dump = @import("Teg.Dump.zig"); +pub const Tag = enum { + /// Either the top level module or a `module` keyword. Contains a list of `.declarations` + /// Every analyzed module gets a `Comp.Namespace` + module, + /// Smallest unit of re-compilation. Every analyzed declaration gets a `Comp.Decl` + declaration, + /// Retrieves a declaration value via it's name. Since declarations are + /// analyzed in isolation of each other, all references to instructions + /// outside of the current declaration must be in the form of this. + load_decl, + /// Blocks are a list of instructions. The block will end with a `.break`, + /// which will assign the block a return value. + block, + /// Ends a block (current or any parent) with a value. + /// `return` expressions are lowered to this. + @"break", + /// Creates a memory cell in the current scope with a starting value. + memory, + /// Re-assign a memory cell with a new ref. + store, + /// Performs a type coercion, such as in variable declarations. + /// The resulting Air Ref will be guaranteed to be the coerced type. + coercion, + /// Load a parameter + param, + /// Asserts the operand is not void. Used for discard statements. + discard, + /// Asserts the operand is `void`. Used for undiscarded expressions. + ensure_void, + /// Numeric constant. + number, + /// String literal constant. + string, + /// Branch takes a boolean condition, and yes and no paths. + @"if", + + // The following instructions are used to implement `Transform.ResultType` + result_type_argument, + result_type_struct_field, + result_type_tuple_field, + result_type_array_value, + + struct_decl, + struct_init, + + array_type, + array_init, + + add, + sub, + mul, + div, + rem, + pow, + @"and", + @"or", + bit_and, + bit_or, + bit_xor, + shl, + shr, + eq, + ne, + lt, + le, + gt, + ge, + + negate, + bool_not, + bit_not, + field, + index, + call, + @"comptime", + + /// Implements the `@js` builtin + js, + /// Implements the `@jsNew` builtin + js_new, + /// Implements the `@import` builtin + import, + /// Implements the `@fetch` builtin + fetch, + /// Implements the `@compileError` builtin + compile_error, + /// Implements the `@typeOf` builtin + type_of, + + pub fn Data(comptime tag: Tag) type { + return switch (tag) { + .module => PlNode(IndexList), + .declaration => PlNode(Decl), + .block => PlNode(IndexList), + .coercion => PlNode(Coercion), + .@"break" => PlNode(Break), + .number => PlNode(Float), + .string => PlNode(SlicedString), + .call => PlNode(Call), + .@"if" => PlNode(If), + .load_decl => StrNode, + .param => DataNode, + .store => PlNode(Store), + .js_new => PlNode(JsNew), + .struct_decl => PlNode(StructDecl), + .struct_init => PlNode(StructInit), + .array_init => PlNode(ArrayInit), + + .result_type_argument => ResultTypeArgument, + .result_type_struct_field => ResultTypeStructField, + .result_type_tuple_field => ResultTypeTupleField, + .result_type_array_value => ResultTypeArrayValue, + + // Single instruction operand + .memory, + .discard, + .ensure_void, + .negate, + .bool_not, + .bit_not, + .js, + .import, + .fetch, + .compile_error, + .@"comptime", + .type_of, + .array_type, + => RefNode, + + // Two instruction operands + .field, + .index, + .add, + .sub, + .mul, + .div, + .rem, + .pow, + .@"and", + .@"or", + .bit_and, + .bit_or, + .bit_xor, + .shl, + .shr, + .eq, + .ne, + .lt, + .le, + .gt, + .ge, + => PlNode(Bin), + }; + } + + pub fn airTag(comptime tag: Tag) Air.Inst.Tag { + return @field(Air.Inst.Tag, @tagName(tag)); + } +}; + +pub fn PlNode(comptime Payload: type) type { + return struct { + pub const T = Payload; + + src_node: NodeOffset, + extra_index: ExtraIndex(Payload), + }; +} + +pub const RefNode = struct { + src_node: NodeOffset, + operand: Inst.Ref, +}; + +pub const StrNode = struct { + src_node: NodeOffset, + string: NullTerminatedString, +}; + +pub const DataNode = struct { + src_node: NodeOffset, + data: u32, +}; + pub const Inst = struct { pub const List = std.MultiArrayList(Inst); tag: Tag, data: EncodedData, - // TODO: fully document every thing - pub const Tag = enum { - module, - declaration, - - block, - /// Creates a memory cell in the current scope. - memory, - /// Performs a type coercion, such as in variable declarations. - /// Emits a sema error if the type conversion fails. - /// `let x: i32 = 1` - as, - load_decl, - /// Load a parameter - param, - store, - discard, - ensure_void, - ensure_bool, - /// lowered from `break` or `return` - /// ends a block with a resulting value - @"break", - number, - string, - - @"if", - - struct_decl, - struct_init, - - array_type, - array_init, - - add, - sub, - mul, - div, - rem, - pow, - @"and", - @"or", - bit_and, - bit_or, - bit_xor, - shl, - shr, - eq, - ne, - lt, - le, - gt, - ge, - - negate, - bool_not, - bit_not, - field, - index, - call, - @"comptime", - - /// Implements the `@js` builtin - js, - /// Implements the `@jsNew` builtin - js_new, - /// Implements the `@import` builtin - import, - /// Implements the `@fetch` builtin - fetch, - /// Implements the `@compileError` builtin - compile_error, - /// Implements the `@typeOf` builtin - type_of, - - pub fn Data(comptime tag: Tag) type { - return switch (tag) { - .module => PlNode(IndexList), - .declaration => PlNode(Decl), - .block => PlNode(IndexList), - .as => PlNode(As), - .@"break" => PlNode(Break), - .number => PlNode(Float), - .string => PlNode(SlicedString), - .call => PlNode(Call), - .@"if" => PlNode(If), - .load_decl => StrNode, - .param => DataNode, - .store => PlNode(Store), - .js_new => PlNode(JsNew), - .struct_decl => PlNode(StructDecl), - .struct_init => PlNode(StructInit), - .array_init => PlNode(ArrayInit), - - // Single instruction operand - .memory, - .discard, - .ensure_void, - .ensure_bool, - .negate, - .bool_not, - .bit_not, - .js, - .import, - .fetch, - .compile_error, - .@"comptime", - .type_of, - .array_type, - => RefNode, - - // Two instruction operands - .field, - .index, - .add, - .sub, - .mul, - .div, - .rem, - .pow, - .@"and", - .@"or", - .bit_and, - .bit_or, - .bit_xor, - .shl, - .shr, - .eq, - .ne, - .lt, - .le, - .gt, - .ge, - => PlNode(Bin), - }; - } - - pub fn airTag(comptime tag: Tag) Air.Inst.Tag { - return @field(Air.Inst.Tag, @tagName(tag)); - } - }; - - // TODO: make this an enum - const NodeOffset = i32; - - pub fn PlNode(comptime Payload: type) type { - return struct { - pub const T = Payload; - - src_node: NodeOffset, - extra_index: ExtraIndex(Payload), - }; - } - - pub const RefNode = struct { - src_node: NodeOffset, - operand: Inst.Ref, - }; - - pub const StrNode = struct { - src_node: NodeOffset, - string: NullTerminatedString, - }; - - pub const DataNode = struct { - src_node: NodeOffset, - data: u32, - }; - /// The position of a Teg instruction within the `Teg` instructions array. pub const Index = enum(u32) { /// Teg is structured so that the outermost "main" struct of any file @@ -197,36 +213,28 @@ pub const Inst = struct { return @enumFromInt(static_len + @intFromEnum(i)); } - pub fn toOptional(i: Index) OptionalIndex { + pub fn toOptional(i: Index) Optional { return @enumFromInt(@intFromEnum(i)); } pub fn op(i: Index, operand: Report.Operand) InstOp { return .{ .index = i, .op = operand }; } - }; - pub const OptionalIndex = enum(u32) { - /// Teg is structured so that the outermost "main" module of any file - /// is always at index 0. - main_module_inst = 0, - ref_start_index = Index.static_len, - none = std.math.maxInt(u32), - _, + pub const Optional = enum(u32) { + none = std.math.maxInt(u32), + _, - pub fn unwrap(oi: OptionalIndex) ?Index { - return if (oi == .none) null else @enumFromInt(@intFromEnum(oi)); - } + pub fn unwrap(oi: Optional) ?Index { + return if (oi == .none) null else @enumFromInt(@intFromEnum(oi)); + } + }; }; - /// A reference to an instruction, or to an InternPool index, or to neither. + /// A reference to an instruction, or to a static InternPool value. /// /// If the integer tag value is < InternPool.static_len, then it - /// corresponds to an InternPool index. Otherwise, this refers to a ZIR - /// instruction. - /// - /// The tag type is specified so that it is safe to bitcast between `[]u32` - /// and `[]Ref`. + /// corresponds to a Value. Otherwise, it is an instruction index. pub const Ref = enum(u32) { opaque_type, bool_type, @@ -282,6 +290,10 @@ pub const Inst = struct { return @enumFromInt(ref_int); } + pub fn toOptional(ref: Ref) Optional { + return @enumFromInt(@intFromEnum(ref)); + } + pub const Optional = enum(u32) { none = std.math.maxInt(u32), _, @@ -291,113 +303,143 @@ pub const Inst = struct { } }; }; +}; - pub const Bin = struct { - lhs: Inst.Ref, - rhs: Inst.Ref, - }; +pub const Bin = struct { + lhs: Inst.Ref, + rhs: Inst.Ref, +}; - pub const Store = struct { - memory: Inst.Index, - operand: Inst.Ref, - }; +pub const Store = struct { + memory: Inst.Index, + operand: Inst.Ref, +}; - pub const As = struct { - type: Inst.Ref, - operand: Inst.Ref, - }; +pub const Coercion = struct { + type: Inst.Ref, + operand: Inst.Ref, +}; - pub const Break = struct { - block: Inst.Index, - operand: Inst.Ref, - }; +pub const Break = struct { + block: Inst.Index, + operand: Inst.Ref, +}; - pub const Call = struct { - base: Inst.Ref, - args: RefList, - }; +pub const Call = struct { + base: Inst.Ref, + args: RefList, +}; - pub const If = struct { - cond: Inst.Ref, - yes: Inst.Ref, - no: Inst.Ref, - }; +pub const If = struct { + cond: Inst.Ref, + yes: Inst.Ref, + no: Inst.Ref, +}; - pub const JsNew = struct { - base: Inst.Ref, - args: RefList, - }; +pub const JsNew = struct { + base: Inst.Ref, + args: RefList, +}; - /// Index into extra which contains `field_len * 2` u32s. The first half - /// are the keys (NullTerminatedString) and the second half are field types - /// (Ref). This basically is like a specialized MultiArrayList. - pub const StructDecl = struct { - payload_index: u32, - field_len: u32, - - pub fn keys(decl: StructDecl, teg: *const Teg) []const NullTerminatedString { - return @ptrCast( - teg.extra.list.items[decl.payload_index..][0..decl.field_len], - ); - } - - pub fn types(decl: StructDecl, teg: *const Teg) []const Ref { - return @ptrCast( - teg.extra.list.items[decl.payload_index + decl.field_len ..][0..decl.field_len], - ); - } - }; +/// Index into extra which contains `field_len * 2` u32s. The first half +/// are the keys (NullTerminatedString) and the second half are field types +/// (Ref). This basically is like a specialized MultiArrayList. +pub const StructDecl = struct { + payload_index: u32, + field_len: u32, + + pub fn keys(decl: StructDecl, teg: *const Teg) []const NullTerminatedString { + return @ptrCast( + teg.extra.list.items[decl.payload_index..][0..decl.field_len], + ); + } + + pub fn types(decl: StructDecl, teg: *const Teg) []const Inst.Ref { + return @ptrCast( + teg.extra.list.items[decl.payload_index + decl.field_len ..][0..decl.field_len], + ); + } +}; - /// This memory is laid out with the same strategy described in StructDecl - pub const StructInit = struct { - base_type: Ref, - payload_index: u32, - field_len: u32, - - pub fn keys(decl: StructInit, teg: *const Teg) []const NullTerminatedString { - return @ptrCast( - teg.extra.list.items[decl.payload_index..][0..decl.field_len], - ); - } - - pub fn inits(decl: StructInit, teg: *const Teg) []const Ref { - return @ptrCast( - teg.extra.list.items[decl.payload_index + decl.field_len ..][0..decl.field_len], - ); - } - }; +/// This memory is laid out with the same strategy described in StructDecl +pub const StructInit = struct { + base_type: Inst.Ref, + payload_index: u32, + field_len: u32, + + pub fn keys(decl: StructInit, teg: *const Teg) []const NullTerminatedString { + return @ptrCast( + teg.extra.list.items[decl.payload_index..][0..decl.field_len], + ); + } + + pub fn inits(decl: StructInit, teg: *const Teg) []const Inst.Ref { + return @ptrCast( + teg.extra.list.items[decl.payload_index + decl.field_len ..][0..decl.field_len], + ); + } +}; - pub const ArrayInit = struct { - /// Optionally provide a result type for more reliable type coercion. - /// If specified, this must be an array type. Otherwise, the first - /// element's type is used. - type_hint: Ref.Optional, - /// The items in the array - list: RefList, - }; +pub const ArrayInit = struct { + /// Optionally provide a result type for more reliable type coercion. + /// If specified, this must be an array type. Otherwise, the first + /// element's type is used. + type_hint: Inst.Ref.Optional, + /// The items in the array + list: RefList, +}; - /// This will contain trailing data depending on the .kind - pub const Decl = struct { - name: NullTerminatedString, - hash: Cache.BinDigest, - kind: Kind, - instruction_count: u32, - body: Ref, - - pub const Kind = enum { - // TODO: remove function, see #45 - function, - immutable, - }; +/// This will contain trailing data depending on the .kind +pub const Decl = struct { + name: NullTerminatedString, + hash: Cache.BinDigest, + kind: Kind, + instruction_count: u32, + body: Inst.Ref, + + pub const Kind = enum { + // TODO: remove function, see #45 + function, + immutable, }; +}; - /// Trailing data for function declarations - pub const DeclFn = struct { - params: RefList, - return_type: Ref, - }; +/// Trailing data for function declarations +pub const DeclFn = struct { + params: RefList, + return_type: Inst.Ref.Optional, +}; + +pub const ResultTypeArgument = struct { + /// The VALUE that is going to be called. + call_base: Inst.Ref, + /// Which argument + argument: u32, +}; + +pub const ResultTypeStructField = struct { + /// The type of the struct itself, + type: Inst.Ref, + /// Which field + field: NullTerminatedString, +}; + +pub const ResultTypeTupleField = struct { + /// The type of the tuple itself, + type: Inst.Ref, + /// Which item + item: u32, +}; + +pub const ResultTypeArrayValue = struct { + /// The type of the array itself, + type: Inst.Ref, + unused: u32 = 0, }; +// TODO: make this an enum +const NodeOffset = i32; + pub const InstOp = struct { index: Inst.Index, op: Report.Operand, @@ -470,7 +512,7 @@ pub fn deinit(teg: *Teg, gpa: std.mem.Allocator) void { teg.* = undefined; } -pub fn instTag(teg: *const Teg, index: Inst.Index) Inst.Tag { +pub fn instTag(teg: *const Teg, index: Inst.Index) Teg.Tag { return teg.insts.items(.tag)[@intFromEnum(index)]; } @@ -478,17 +520,25 @@ pub fn instEncodedData(teg: *const Teg, index: Inst.Index) EncodedData { return teg.insts.items(.data)[@intFromEnum(index)]; } -pub fn instData(teg: *const Teg, comptime tag: Inst.Tag, index: Inst.Index) tag.Data() { +pub fn instData(teg: *const Teg, comptime tag: Teg.Tag, index: Inst.Index) tag.Data() { return teg.instEncodedData(index).decode(tag, teg.instTag(index)); } -pub fn instPayload(teg: *const Teg, comptime tag: Inst.Tag, index: Inst.Index) tag.Data().T { +pub fn instPayload(teg: *const Teg, comptime tag: Teg.Tag, index: Inst.Index) tag.Data().T { return teg.instData(tag, index).extra_index.get(teg); } pub fn instNodeOffset(teg: *const Teg, index: Inst.Index) i32 { return switch (teg.instTag(index)) { - .declaration => 0, + // these nodes never get errors, but Teg.Dump will print these regardless + // TODO: + .declaration, + .result_type_argument, + .result_type_struct_field, + .result_type_tuple_field, + .result_type_array_value, + => 0, + inline else => |tag| teg.instData(tag, index).src_node, }; } @@ -497,10 +547,10 @@ pub fn blockInstructions(teg: *const Teg, index: Inst.Index) []const Inst.Index return teg.instData(.block, index).extra_index.get(teg).slice(teg); } -pub fn fullFuncDecl(teg: *const Teg, index: Inst.Index) struct { Inst.Decl, Inst.DeclFn } { +pub fn fullFuncDecl(teg: *const Teg, index: Inst.Index) struct { Teg.Decl, Teg.DeclFn } { const decl, var trail = teg.instData(.declaration, index).extra_index.getTrail(teg); assert(decl.kind == .function); - const decl_fn = trail.read(&teg.extra, Inst.DeclFn); + const decl_fn = trail.read(&teg.extra, Teg.DeclFn); return .{ decl, decl_fn }; } @@ -512,7 +562,7 @@ pub const FullDecl = struct { body: Inst.Ref, const Data = union(enum) { - function: Inst.DeclFn, + function: Teg.DeclFn, immutable: struct {}, }; }; @@ -537,7 +587,7 @@ pub fn fullDecl(teg: *const Teg, index: Inst.Index) FullDecl { }; } -pub fn declData(teg: *const Teg, index: Inst.Index) Inst.Decl { +pub fn declData(teg: *const Teg, index: Inst.Index) Teg.Decl { return teg.instData(.declaration, index).extra_index.get(teg); } @@ -565,7 +615,7 @@ pub fn rootFunction(teg: *const Teg) Inst.Index { pub fn declName(teg: *const Teg, decl_index: Inst.Index) []const u8 { const decl = teg.instData(.declaration, decl_index); - const decl_data: Inst.Decl = decl.extra_index.get(teg); + const decl_data: Teg.Decl = decl.extra_index.get(teg); return decl_data.name.slice(teg.string_bytes); } diff --git a/src/core/Transform.zig b/src/core/Transform.zig index a1dcf9f122e3610bdef97ba6603103f2a586eea0..1a24a2bd5da49c04ce1fa1798a7fe75d13be8588 100644 --- a/src/core/Transform.zig +++ b/src/core/Transform.zig @@ -1,26 +1,35 @@ //! Transform pass converts an Ast into a high-level/untyped instruction format. //! See Teg.zig for more details about the IR. Transforming handles: //! +//! - Flattening nested expressions into instruction sequences. //! - Resolving all local variables into reference or declaration names. -//! - Resolving breaks and returns to their block id. +//! - Resolving breaks and returns to their generated block id. //! - Annotating expression statements with `.discard` and `.ensure_void` -//! - Flattening nested expressions into instruction sequences. +//! - Resolving implicit expression with their corresponding result type //! //! Some trivial diagnostics such as unused variables, unknown result types, //! invalid assignment targets, and other style issues are also reported here. + gpa: Allocator, +arena: Allocator, + ast: Ast, + +// these values are allocated through `gpa`, so that resulting IR can be +// trivially shrunk before returning. + insts: Inst.List = .{}, -string_bytes: std.ArrayListUnmanaged(u8) = .{}, extra: ExtraStore = .{}, +string_bytes: std.ArrayListUnmanaged(u8) = .{}, +report: Report.Builder, -scratch: std.ArrayListUnmanaged(u32) = .{}, +// however, these are allocated through `arena`, since they are always freed at +// the end of the pass. + +scratch: core.U32Stack = .{}, id_decode_buf: std.ArrayList(u8), - scopes: std.ArrayListUnmanaged(Scope) = .{}, -report: Report.Builder, - pub const Error = error{ /// A `Report` will contain at least one error. TransformFail, @@ -40,6 +49,8 @@ pub fn transform( var t: Transform = .{ .ast = ast, .gpa = gpa, + // TODO: Currently this is illusion of free will + .arena = gpa, .report = .{ .gpa = gpa, .current_file = file }, .id_decode_buf = std.ArrayList(u8).initCapacity(sfa.get(), 4096) catch unreachable, }; @@ -57,9 +68,7 @@ pub fn transform( return error.TransformFail; } - assert(@intFromEnum(module) == 0); - - try t.insts.setCapacity(t.gpa, t.insts.len); + assert(module == .main_module_inst); return .{ .insts = t.insts.toOwnedSlice(), @@ -69,27 +78,166 @@ pub fn transform( }; } -pub fn deinit(t: *Transform) void { - t.scratch.deinit(t.gpa); +fn deinit(t: *Transform) void { assert(t.scopes.items.len == 0); - t.scopes.deinit(t.gpa); + t.id_decode_buf.deinit(); + t.scratch.list.deinit(t.arena); + t.scopes.deinit(t.arena); + t.id_decode_buf.deinit(); t.string_bytes.deinit(t.gpa); t.insts.deinit(t.gpa); t.extra.deinit(t.gpa); - t.id_decode_buf.deinit(); t.* = undefined; } +/// Represents how the parent node is going to implicitly cast this expression. +/// +/// Consider an expression like `let x: S = .{ y: 1 }`. The type from the +/// declaration must impact the visited struct initialization expression. This +/// behavior is recursive, so something more complex can properly infer: +/// +/// struct S { a: int } +/// struct Y {} +/// let x: [](S, []Y) = [ .{ .{ a: 1 }, [ .{} ] } ]; +/// +/// The innermost `.{}` must eventually analyze to `Y{}`, which can be unwrapped +/// from the type expression via specialized instructions for this purpose. +const ResultType = union(enum) { + /// There is no result type available + none, + /// The result type was consumed + used, + /// The expression will be cast to this ref. + ref: Inst.Ref, + /// The expression will be cast to the type of this ref + type_of: Inst.Ref, + /// The expression is an argument to a function. + argument: Teg.ResultTypeArgument, + /// The expression is a field in a struct initialization + struct_field: Teg.ResultTypeStructField, + /// The expression is a field in a tuple initialization + tuple_field: Teg.ResultTypeTupleField, + /// The expression is a field in an array initialization + array_value: Inst.Ref, + + /// This will return an instruction pointing to the the type + pub fn consume(result: *ResultType, t: *Transform) !?Inst.Ref { + if (result.* == .none) return null; + defer result.* = .used; + return switch (result.*) { + .none => unreachable, + .used => unreachable, // Use after free + .ref => |r| r, + .type_of => |r| try t.addRef(.type_of, .{ + // Result types don't have associated nodes, which is ok because + // type_of cannot fail in the context we are using it. Instead, + // we will use a predictable poison value, so that an attempt + // to print this will crash. + .src_node = std.math.maxInt(i32), + .operand = r, + }), + .argument => |r| try t.addRef(.result_type_argument, r), + .struct_field => |r| try t.addRef(.result_type_struct_field, r), + .tuple_field => |r| try t.addRef(.result_type_tuple_field, r), + .array_value => |r| try t.addRef(.result_type_array_value, .{ .type = r }), + }; + } +}; + +const BlockContext = struct { + decl_node_offset: i32, + return_target: ?*ReturnTarget = null, + break_target: ?*BreakTarget = null, + + const ReturnTarget = struct { + generated_block: ?Inst.Index = null, + result_type: ResultType, + }; + + /// This struct is always stored on the stack; the idea is to pass mutable + /// pointers to it. When `break :brk` is encountered, it will generate the + /// block instruction. This way, redundant blocks are never created. + const BreakTarget = struct { + label: ?[]const u8, + /// This allows `continue` as well as implicit break targeting. + is_loop: bool, + /// If `null`, this label was unused. + block_index: ?Inst.Index = null, + parent: ?*BreakTarget = null, + result_type: *ResultType, + + fn addChild(parent: *BreakTarget, label: ?[]const u8, is_loop: bool) BreakTarget { + return .{ + .parent = parent, + .label = label, + .is_loop = is_loop, + }; + } + + fn getInst(target: *BreakTarget, t: *Transform) !Inst.Index { + return target.block_index orelse { + const inst = try t.addUntracked(.block, undefined); + target.block_index = inst; + return inst; + }; + } + }; + + fn init(root_node: NodeIndex) BlockContext { + return .{ + .decl_node_offset = @intCast(root_node), + }; + } + + fn nodeOffset(self: BlockContext, node: NodeIndex) i32 { + return @as(i32, @intCast(node)) - self.decl_node_offset; + } + + fn declOffset(self: BlockContext, node: NodeIndex) BlockContext { + return .{ + .decl_node_offset = self.nodeOffset(node), + }; + } + + fn withReturn(self: BlockContext, return_target: ?*ReturnTarget) BlockContext { + return .{ + .decl_node_offset = self.decl_node_offset, + .return_target = return_target, + }; + } + + fn withBreak(self: BlockContext, break_target: *BreakTarget) BlockContext { + return .{ + .decl_node_offset = self.decl_node_offset, + .return_target = self.return_target, + .break_target = break_target, + }; + } + + fn lookupNamedBreak(self: BlockContext, label: []const u8) ?*BreakTarget { + var break_target = self.break_target; + while (break_target) |target| : (break_target = target.parent) { + if (target.label) |target_label| { + if (mem.eql(u8, label, target_label)) { + return target; + } + } + } + return null; + } +}; + const Scope = struct { symbols: std.StringArrayHashMapUnmanaged(Symbol) = .{}, + // TODO: optimize this structure's size. const Symbol = struct { node: NodeIndex, data: Symbol.Data, - // TODO: optimize this structure used: bool = false, mutated: bool = false, discarded: ?NodeIndex = null, + type: SymbolResultType, const Data = union(enum) { declaration, @@ -98,6 +246,21 @@ const Scope = struct { immutable: Inst.Ref, }; + const SymbolResultType = union(enum) { + /// Points to the resolved result type (.ref), as is. + present: Inst.Ref, + /// This points to the value. Which means a `type_of` instruction + /// must be inserted before it may be used. + missing: Inst.Ref, + + pub fn resolve(srt: SymbolResultType) ResultType { + return switch (srt) { + .present => |type_ref| .{ .ref = type_ref }, + .missing => |value_ref| .{ .type_of = value_ref }, + }; + } + }; + pub fn addWarnings(symbol: Symbol, t: *Transform) !void { if (!symbol.mutated and symbol.data == .mutable) { try t.report.warn(.not_mutated_var, .{ @@ -142,86 +305,6 @@ const Scope = struct { } }; -const BlockContext = struct { - node_base: i32, - return_target: ?*?Inst.Index = null, - break_target: ?*BreakTarget = null, - - /// This struct is always stored on the stack; the idea is to pass mutable - /// pointers to it. When `break :brk` is encountered, it will generate the - /// block instruction. This way, redundant blocks are never created. - const BreakTarget = struct { - label: ?[]const u8, - /// This allows `continue` as well as implicit break targeting. - is_loop: bool, - /// If `null`, this label was unused. - block_index: ?Inst.Index = null, - parent: ?*BreakTarget = null, - - fn addChild(parent: *BreakTarget, label: ?[]const u8, is_loop: bool) BreakTarget { - return .{ - .parent = parent, - .label = label, - .is_loop = is_loop, - }; - } - - fn getInst(target: *BreakTarget, t: *Transform) !Inst.Index { - return target.block_index orelse { - const inst = try t.addRootInst(.{ - .tag = .block, - .data = undefined, - }); - target.block_index = inst; - return inst; - }; - } - }; - - fn init(root_node: NodeIndex) BlockContext { - return .{ - .node_base = @intCast(root_node), - }; - } - - fn nodeOffset(self: BlockContext, node: NodeIndex) i32 { - return @as(i32, @intCast(node)) - self.node_base; - } - - fn declOffset(self: BlockContext, node: NodeIndex) BlockContext { - return .{ - .node_base = self.nodeOffset(node), - }; - } - - fn withReturn(self: BlockContext, return_target: ?*?Inst.Index) BlockContext { - return .{ - .node_base = self.node_base, - .return_target = return_target, - }; - } - - fn withBreak(self: BlockContext, break_target: *BreakTarget) BlockContext { - return .{ - .node_base = self.node_base, - .return_target = self.return_target, - .break_target = break_target, - }; - } - - fn findAndMarkBreak(self: BlockContext, t: *Transform, label: []const u8) !?Inst.Index { - var break_target = self.break_target; - while (break_target) |target| : (break_target = target.parent) { - if (target.label) |target_label| { - if (mem.eql(u8, label, target_label)) { - return try target.getInst(t); - } - } - } - return null; - } -}; - const Liveness = enum { alive, dead }; const BuiltInIds = std.StaticStringMap(Inst.Ref).initComptime(.{ @@ -242,10 +325,7 @@ fn transformModule(t: *Transform, module_node: NodeIndex) Allocator.Error!Inst.I const trace = tracer.trace(@src()); defer trace.end(); - const module = try t.addRootInst(.{ - .tag = .module, - .data = undefined, - }); + const module = try t.addUntracked(.module, undefined); const decl = BlockContext.init(module_node); @@ -271,10 +351,7 @@ fn transformModule(t: *Transform, module_node: NodeIndex) Allocator.Error!Inst.I } } - const top_level_fn = try t.addRootInst(.{ - .tag = .declaration, - .data = undefined, - }); + const top_level_fn = try t.addUntracked(.declaration, undefined); temp[temp.len - 1] = top_level_fn; @@ -285,42 +362,39 @@ fn transformModule(t: *Transform, module_node: NodeIndex) Allocator.Error!Inst.I try t.extra.list.appendSlice(t.gpa, @ptrCast(temp)); const top_level_code_ref: Inst.Ref = ref: { - const scratch_top = t.scratch.items.len; - defer t.scratch.shrinkRetainingCapacity(scratch_top); + const scratch = t.scratch.mark(); + defer scratch.deinit(); for (statements) |statement| { try t.transformModuleStatement(decl, statement); } // if there are no new instructions, we dont have to create a block context - if (scratch_top == t.scratch.items.len) { + if (scratch.len() == 0) { break :ref .void; } - const block = try t.addRootInst(.{ - .tag = .block, - .data = undefined, - }); + const block = try t.addUntracked(.block, undefined); t.getInstData(block).* = EncodedData.encode(.block, .{ .src_node = decl.nodeOffset(module_node), - .extra_index = try t.extra.append(t.gpa, IndexList, try t.scratchToIndexList(scratch_top)), + .extra_index = try t.extra.append(t.gpa, IndexList, try t.scratchToIndexList(scratch)), }); break :ref block.toRef(); }; - const decl_extra, var trail = try t.extra.appendTrailing(t.gpa, Inst.Decl, .{ + const decl_extra, var trail = try t.extra.appendTrailing(t.gpa, Teg.Decl, .{ .kind = .function, .name = NullTerminatedString.empty, .hash = .{0} ** 16, .instruction_count = @as(u32, @intCast(t.insts.len)) - @intFromEnum(top_level_fn), .body = top_level_code_ref, - }, dod.extraSizeOf(Inst.DeclFn)); + }, extra.extraSizeOf(Teg.DeclFn)); - trail.writeStruct(Inst.DeclFn, .{ + trail.writeStruct(Teg.DeclFn, .{ .params = .{ .start = 0, .len = 0 }, - .return_type = Inst.Ref.void, + .return_type = Inst.Ref.void.toOptional(), }); t.getInstData(top_level_fn).* = EncodedData.encode(.declaration, .{ @@ -361,10 +435,14 @@ fn previsitModuleStatement(t: *Transform, node: NodeIndex) Allocator.Error!void gop.value_ptr.* = .{ .node = node, .data = .declaration, + .type = .{ .present = .void }, }; }, .struct_decl => { const decl = t.ast.fullStructDecl(node); + var id, const id_allocated = try t.decodeIdentifierExtra(decl.id); + if (id_allocated) + id = try t.gpa.dupe(u8, id); const gop = try t.scope().symbols.getOrPut(t.gpa, decl.id); if (gop.found_existing) { try t.report.err(.duplicate_function, .{ @@ -377,6 +455,7 @@ fn previsitModuleStatement(t: *Transform, node: NodeIndex) Allocator.Error!void gop.value_ptr.* = .{ .node = node, .data = .declaration, + .type = .{ .present = .void }, }; }, else => {}, @@ -397,14 +476,14 @@ fn transformModuleStatement(t: *Transform, decl: BlockContext, node: NodeIndex) .struct_decl, => {}, // declarations are analyzed separately - else => _ = try t.transformBlockStatement(decl, node, false), + else => _ = try t.transformBlockStatement(decl, node, null), } } fn transformDecl(t: *Transform, outer_decl: BlockContext, node: NodeIndex) Allocator.Error!Inst.Index { return switch (t.ast.nodeTag(node)) { .fn_decl => t.transformFunction(outer_decl, node), - .struct_decl => t.transformStruct(outer_decl, node), + .struct_decl => t.transformStructDeclaration(outer_decl, node), else => unreachable, // not a declaration }; } @@ -416,33 +495,40 @@ fn transformFunction(t: *Transform, outer_decl: BlockContext, node: NodeIndex) A const decl = outer_decl.declOffset(node); const fn_decl = t.ast.fullFnDecl(node); - const decl_inst = try t.addRootInst(.{ - .tag = .declaration, - .data = undefined, - }); + const decl_inst = try t.addUntracked(.declaration, undefined); try t.pushScope(); defer t.popScope(); const param_list = brk: { - const scratch_top = t.scratch.items.len; - defer t.scratch.shrinkRetainingCapacity(scratch_top); + const scratch = t.scratch.mark(); + defer scratch.deinit(); for (fn_decl.params) |param| { - try t.addScratch(Inst.Ref, try t.transformParamDecl(decl, param)); + try t.scratch.add(t.arena, Inst.Ref, try t.transformParamDecl(decl, param)); } - break :brk try t.scratchToRefList(scratch_top); + break :brk try t.scratchToRefList(scratch); }; const return_type = if (fn_decl.return_type) |return_type_node| - try t.transformExpressionToBlock(decl, return_type_node, false) + try t.transformToBlock( + decl, + return_type_node, + .result_without_coercion, + .{ .ref = .type_type }, + ) else - Inst.Ref.void; + null; - const body = try t.transformExpressionToBlock(decl, fn_decl.body, true); + const body = try t.transformToBlock( + decl, + fn_decl.body, + .function_body, + if (return_type) |ref| .{ .ref = ref } else .none, + ); - const decl_extra = try t.extra.append(t.gpa, Inst.Decl, .{ + const decl_extra = try t.extra.append(t.gpa, Teg.Decl, .{ .kind = .function, .name = try t.addNullTerminatedString(fn_decl.id), .hash = .{0} ** 16, @@ -450,9 +536,9 @@ fn transformFunction(t: *Transform, outer_decl: BlockContext, node: NodeIndex) A .body = body, }); - _ = try t.extra.append(t.gpa, Inst.DeclFn, .{ + _ = try t.extra.append(t.gpa, Teg.DeclFn, .{ .params = param_list, - .return_type = return_type, + .return_type = if (return_type) |r| r.toOptional() else .none, }); t.getInstData(decl_inst).* = EncodedData.encode(.declaration, .{ @@ -465,21 +551,18 @@ fn transformFunction(t: *Transform, outer_decl: BlockContext, node: NodeIndex) A return decl_inst; } -fn transformStruct(t: *Transform, outer_decl: BlockContext, node: NodeIndex) Allocator.Error!Inst.Index { +fn transformStructDeclaration(t: *Transform, outer_decl: BlockContext, node: NodeIndex) Allocator.Error!Inst.Index { const trace = tracer.trace(@src()); defer trace.end(); const decl = outer_decl.declOffset(node); const decl_data = t.ast.fullStructDecl(node); - const decl_inst = try t.addRootInst(.{ - .tag = .declaration, - .data = undefined, - }); + const decl_inst = try t.addUntracked(.declaration, undefined); - const body = try t.transformExpressionToBlock(decl, node, false); + const body = try t.transformToBlock(decl, node, .result_without_coercion, .none); - const decl_extra = try t.extra.append(t.gpa, Inst.Decl, .{ + const decl_extra = try t.extra.append(t.gpa, Teg.Decl, .{ .kind = .immutable, .name = try t.addNullTerminatedString(decl_data.id), .hash = .{0} ** 16, @@ -495,76 +578,83 @@ fn transformStruct(t: *Transform, outer_decl: BlockContext, node: NodeIndex) All return decl_inst; } -const TransformFunctionCtx = struct { - param_list: RefList, +const TransformToBlockMode = enum { + tracked_result, + result_without_coercion, + coerced_result, + function_body, + + fn ResultType(mode: TransformToBlockMode) type { + return switch (mode) { + .tracked_result => *Transform.ResultType, + else => Transform.ResultType, + }; + } }; -/// Converts expressions into blocks with a break. This is important -/// for places where a block is not allowed, such as a parameter list. -/// This is also used for the function body, even though it is a block. -/// Simple refs turn into just the ref -fn transformExpressionToBlock( +/// guh +fn transformToBlock( t: *Transform, - decl: BlockContext, + ctx: BlockContext, node: NodeIndex, - comptime is_function: bool, + comptime mode: TransformToBlockMode, + result_type: mode.ResultType(), ) Allocator.Error!Inst.Ref { const trace = tracer.trace(@src()); defer trace.end(); - const node_tag = t.ast.nodes.items(.tag)[node]; - switch (node_tag) { - .id => { - // Lower builtin ids without using a block - const id = try t.decodeIdentifier(t.ast.basicNodeSlice(node)); - if (BuiltInIds.get(id)) |ref| { - return ref; - } - }, + const scratch = t.scratch.mark(); + defer scratch.deinit(); - else => {}, - } - - var block: ?Inst.Index = null; - - const scratch_top = t.scratch.items.len; - defer t.scratch.shrinkRetainingCapacity(scratch_top); - - if (is_function) { + // For function blocks, add .param instructions for all of the parameters + if (mode == .function_body) { const s = t.scopes.getLast(); for (s.symbols.values(), 0..) |*param, i| { assert(param.data == .param); - const ref = try t.addInstRef(.{ - .tag = .param, - .data = EncodedData.encode(.param, .{ - .src_node = decl.nodeOffset(node), - .data = @intCast(i), - }), + const ref = try t.addRef(.param, .{ + .src_node = ctx.nodeOffset(node), + .data = @intCast(i), }); param.data = .{ .immutable = ref }; } } + // The result type is only passed by reference if mode is tracked_result. + const copies_result_type = mode.ResultType() == ResultType; + var result_type_storage = if (copies_result_type) result_type; + + var ret = if (mode == .function_body) + BlockContext.ReturnTarget{ .result_type = result_type_storage }; + const expr = try t.transformExpression( - if (is_function) - decl.withReturn(&block) + // Function blocks are allowed to return, in which + // it will return to this block. + if (mode == .function_body) + ctx.withReturn(&ret) else - decl, + ctx, + // Pass result type pointer + if (copies_result_type) + &result_type_storage + else + result_type, node, ); // if there are no new instructions, we dont have to create a block context - if (scratch_top == t.scratch.items.len) { - const result = expr orelse unreachable; // null means we *added* break or return + if (scratch.len() == 0) { + const result = expr orelse + unreachable; // null means we *added* break or return - // TODO: evaluate if `if` blocks would be fine in sema with instruction refs - // with this extra if check here, we filter out non-blocks. if (result.value() != null) { - assert(block == null); + if (mode == .function_body) + assert(ret.generated_block == null); // return was called without an instruction + return result; } } + // If we analyzed a block, then just use that. No need for an extra wrapper block. if (expr) |result| { if (result.index()) |index| { if (t.insts.items(.tag)[@intFromEnum(index)] == .block) { @@ -573,32 +663,26 @@ fn transformExpressionToBlock( } } - const ret_block = if (block) |b| - b - else - try t.addRootInst(.{ - .tag = .block, - .data = undefined, - }); + // Otherwise try using the function return block, or create a new one + const maybe_generated_block = if (mode == .function_body) ret.generated_block else null; + const ret_block = maybe_generated_block orelse try t.addUntracked(.block, undefined); + // The block must end with a break if (expr) |result| { - _ = try t.addInstRef(.{ - .tag = .@"break", - .data = EncodedData.encode(.@"break", .{ - .src_node = decl.nodeOffset(node), - .extra_index = try t.extra.append(t.gpa, Inst.Break, .{ - .block = ret_block, - .operand = result, - }), + _ = try t.addIndex(.@"break", .{ + .src_node = ctx.nodeOffset(node), + .extra_index = try t.extra.append(t.gpa, Teg.Break, .{ + .block = ret_block, + .operand = result, }), }); } - const content = try t.scratchToIndexList(scratch_top); + const content = try t.scratchToIndexList(scratch); assert(content.len > 0); t.getInstData(ret_block).* = EncodedData.encode(.block, .{ - .src_node = decl.nodeOffset(node), + .src_node = ctx.nodeOffset(node), .extra_index = try t.extra.append(t.gpa, IndexList, content), }); @@ -610,7 +694,13 @@ fn transformParamDecl(t: *Transform, decl: BlockContext, param: NodeIndex) Alloc defer trace.end(); const param_decl = t.ast.fullParamDecl(param); - const value = try t.transformExpressionToBlock(decl, param_decl.type orelse @panic("TODO: inferred param types"), false); + const ty = try t.transformToBlock( + decl, + param_decl.type orelse + @panic("TODO: inferred param types"), + .result_without_coercion, + .{ .ref = .type_type }, + ); var id, const id_allocated = try t.decodeIdentifierExtra(param_decl.id); if (id_allocated) @@ -631,9 +721,10 @@ fn transformParamDecl(t: *Transform, decl: BlockContext, param: NodeIndex) Alloc sym.value_ptr.* = .{ .node = param, .data = .param, + .type = .{ .present = ty }, }; - return value; + return ty; } pub const Builtin = enum { @@ -664,14 +755,12 @@ pub const Builtin = enum { return .void; } - const source = try t.transformExpression(decl, args[0]) orelse return null; + var result: ResultType = .{ .ref = .string_type }; + const source = try t.transformExpression(decl, &result, args[0]) orelse return null; - return try t.addInstRef(.{ - .tag = .js, - .data = EncodedData.encode(.js, .{ - .src_node = decl.nodeOffset(args[0]), - .operand = source, - }), + return try t.addRef(.js, .{ + .src_node = decl.nodeOffset(args[0]), + .operand = source, }); } @@ -684,17 +773,15 @@ pub const Builtin = enum { return .void; } - const base = try t.transformExpression(decl, args[0]) orelse return null; - const args_list = try t.transformExpressionList(decl, args[1..]) orelse return null; + var opaque_type: ResultType = .{ .ref = .opaque_type }; + const base = try t.transformExpression(decl, &opaque_type, args[0]) orelse return null; + const args_list = try t.transformExpressionList(decl, .{ .ref = .opaque_type }, args[1..]) orelse return null; - return try t.addInstRef(.{ - .tag = .js_new, - .data = EncodedData.encode(.js_new, .{ - .src_node = decl.nodeOffset(args[0]), - .extra_index = try t.extra.append(t.gpa, Inst.JsNew, .{ - .base = base, - .args = args_list, - }), + return try t.addRef(.js_new, .{ + .src_node = decl.nodeOffset(args[0]), + .extra_index = try t.extra.append(t.gpa, Teg.JsNew, .{ + .base = base, + .args = args_list, }), }); } @@ -708,15 +795,13 @@ pub const Builtin = enum { return .void; } - const source = try t.transformExpression(decl, args[0]) orelse + var string_type: ResultType = .{ .ref = .string_type }; + const source = try t.transformExpression(decl, &string_type, args[0]) orelse return null; - return try t.addInstRef(.{ - .tag = .compile_error, - .data = EncodedData.encode(.compile_error, .{ - .src_node = decl.nodeOffset(node), - .operand = source, - }), + return try t.addRef(.compile_error, .{ + .src_node = decl.nodeOffset(node), + .operand = source, }); } @@ -729,22 +814,25 @@ pub const Builtin = enum { return .void; } - const source = try t.transformExpression(decl, args[0]) orelse + var result: ResultType = .none; + const source = try t.transformExpression(decl, &result, args[0]) orelse return null; - return try t.addInstRef(.{ - .tag = .type_of, - .data = EncodedData.encode(.type_of, .{ - .src_node = decl.nodeOffset(node), - .operand = source, - }), + return try t.addRef(.type_of, .{ + .src_node = decl.nodeOffset(node), + .operand = source, }); } }; /// This is the primary transformation function: converting an AST expression into instructions. /// `null` represents a `return` or `break`, aka this branch of code is dead. -fn transformExpression(t: *Transform, decl: BlockContext, node: NodeIndex) Allocator.Error!?Inst.Ref { +fn transformExpression( + t: *Transform, + ctx: BlockContext, + result: *ResultType, + node: NodeIndex, +) Allocator.Error!?Inst.Ref { const node_tag = t.ast.nodes.items(.tag)[node]; const trace = tracer.traceFmt(@src(), "{s}", .{@tagName(node_tag)}); @@ -775,12 +863,9 @@ fn transformExpression(t: *Transform, decl: BlockContext, node: NodeIndex) Alloc lookup.used = true; return switch (lookup.data) { - .declaration => try t.addInstRef(.{ - .tag = .load_decl, - .data = EncodedData.encode(.load_decl, .{ - .src_node = decl.nodeOffset(node), - .string = try t.addNullTerminatedString(id), - }), + .declaration => try t.addRef(.load_decl, .{ + .src_node = ctx.nodeOffset(node), + .string = try t.addNullTerminatedString(id), }), .mutable => |idx| idx.toRef(), .immutable => |ref| ref, @@ -800,41 +885,32 @@ fn transformExpression(t: *Transform, decl: BlockContext, node: NodeIndex) Alloc try t.report.warn(.zero_prefixed_literal, .{ .token = t.ast.nodeToken(node) }); } - return try t.addInstRef(.{ - .tag = .number, - .data = EncodedData.encode(.number, .{ - .src_node = decl.nodeOffset(node), - .extra_index = try t.extra.append(t.gpa, Float, Float.encode(value)), - }), + return try t.addRef(.number, .{ + .src_node = ctx.nodeOffset(node), + .extra_index = try t.extra.append(t.gpa, Float, Float.encode(value)), }); }, .string => { const str = t.ast.basicNodeSlice(node); const sliced = try t.addDecodedSlicedString(str[1 .. str.len - 1]); - return try t.addInstRef(.{ - .tag = .string, - .data = EncodedData.encode(.string, .{ - .src_node = decl.nodeOffset(node), - .extra_index = try t.extra.append(t.gpa, SlicedString, sliced), - }), + return try t.addRef(.string, .{ + .src_node = ctx.nodeOffset(node), + .extra_index = try t.extra.append(t.gpa, SlicedString, sliced), }); }, .assign => { const ast_bin = t.ast.nodeData(node).bin; - const lhs = try t.transformAssignmentTarget(decl, ast_bin.lhs) orelse + const lhs, var result_type = try t.transformAssignmentTarget(ctx, ast_bin.lhs) orelse return .void; - const rhs = try t.transformExpression(decl, ast_bin.rhs) orelse + const rhs = try t.transformExpression(ctx, &result_type, ast_bin.rhs) orelse return null; - const assign_inst = try t.addInstRef(.{ - .tag = .store, - .data = EncodedData.encode(.store, .{ - .src_node = decl.nodeOffset(node), - .extra_index = try t.extra.append(t.gpa, Inst.Store, .{ - .memory = lhs, - .operand = rhs, - }), + const assign_inst = try t.addRef(.store, .{ + .src_node = ctx.nodeOffset(node), + .extra_index = try t.extra.append(t.gpa, Teg.Store, .{ + .memory = lhs, + .operand = rhs, }), }); return assign_inst; @@ -877,41 +953,39 @@ fn transformExpression(t: *Transform, decl: BlockContext, node: NodeIndex) Alloc // compound compute + assign operation. const is_assign = comptime tag.isAssignBin(); const teg_tag = @field( - Teg.Inst.Tag, + Teg.Tag, if (is_assign) @tagName(tag)[0 .. @tagName(tag).len - "_assign".len] else @tagName(tag), ); const ast_bin = t.ast.nodeData(node).bin; - const lhs = if (is_assign) - try t.transformAssignmentTarget(decl, ast_bin.lhs) orelse + var lhs_result_type: ResultType = .none; + const lhs, var assign_result_type: ResultType = if (is_assign) + try t.transformAssignmentTarget(ctx, ast_bin.lhs) orelse return .void else - try t.transformExpression(decl, ast_bin.lhs) orelse - return null; - const rhs = try t.transformExpression(decl, ast_bin.rhs) orelse return null; + .{ + try t.transformExpression(ctx, &lhs_result_type, ast_bin.lhs) orelse + return null, + .none, + }; + const rhs = try t.transformExpression(ctx, &assign_result_type, ast_bin.rhs) orelse return null; - const compute_inst = try t.addInstRef(.{ - .tag = teg_tag, - .data = EncodedData.encode(teg_tag, .{ - .src_node = decl.nodeOffset(node), - .extra_index = try t.extra.append(t.gpa, Inst.Bin, .{ - .lhs = if (is_assign) lhs.toRef() else lhs, - .rhs = rhs, - }), + const compute_inst = try t.addRef(teg_tag, .{ + .src_node = ctx.nodeOffset(node), + .extra_index = try t.extra.append(t.gpa, Teg.Bin, .{ + .lhs = if (is_assign) lhs.toRef() else lhs, + .rhs = rhs, }), }); if (is_assign) { - const assign_inst = try t.addInstRef(.{ - .tag = .store, - .data = EncodedData.encode(.store, .{ - .src_node = decl.nodeOffset(node), - .extra_index = try t.extra.append(t.gpa, Inst.Store, .{ - .memory = lhs, - .operand = compute_inst, - }), + const assign_inst = try t.addRef(.store, .{ + .src_node = ctx.nodeOffset(node), + .extra_index = try t.extra.append(t.gpa, Teg.Store, .{ + .memory = lhs, + .operand = compute_inst, }), }); return assign_inst; @@ -926,29 +1000,31 @@ fn transformExpression(t: *Transform, decl: BlockContext, node: NodeIndex) Alloc .array_type, => |tag| { const ast_un = t.ast.nodeData(node).un; - const teg_tag = @field(Teg.Inst.Tag, @tagName(tag)); + const teg_tag = @field(Teg.Tag, @tagName(tag)); - const operand = try t.transformExpression(decl, ast_un.expr) orelse + var operand_type: ResultType = switch (tag) { + .negate => .none, // any numeric type allowed + .bool_not => .{ .ref = .bool_type }, + .bit_not => .{ .ref = .int32_type }, + .array_type => .{ .ref = .type_type }, + else => @compileError(unreachable), + }; + + const operand = try t.transformExpression(ctx, &operand_type, ast_un.expr) orelse return null; - return try t.addInstRef(.{ - .tag = teg_tag, - .data = EncodedData.encode(teg_tag, .{ - .src_node = decl.nodeOffset(node), - .operand = operand, - }), + return try t.addRef(teg_tag, .{ + .src_node = ctx.nodeOffset(node), + .operand = operand, }); }, .@"comptime" => { const ast_un = t.ast.nodeData(node).un; - const operand = try t.transformExpressionToBlock(decl, ast_un.expr, false); - return try t.addInstRef(.{ - .tag = .@"comptime", - .data = EncodedData.encode(.@"comptime", .{ - .src_node = decl.nodeOffset(node), - .operand = operand, - }), + const operand = try t.transformToBlock(ctx, ast_un.expr, .tracked_result, result); + return try t.addRef(.@"comptime", .{ + .src_node = ctx.nodeOffset(node), + .operand = operand, }); }, @@ -957,43 +1033,42 @@ fn transformExpression(t: *Transform, decl: BlockContext, node: NodeIndex) Alloc const label = t.ast.tokenSlice(t.ast.nodeToken(node)); var target: BlockContext.BreakTarget = .{ - .parent = decl.break_target, + .parent = ctx.break_target, .label = label, .is_loop = false, + .result_type = result, }; - const inner_ctx = decl.withBreak(&target); + const inner_ctx = ctx.withBreak(&target); - const scratch_top = t.scratch.items.len; + const scratch = t.scratch.mark(); - const inner_expr = try t.transformExpression(inner_ctx, expr); + const inner_expr = try t.transformExpression(inner_ctx, result, expr); // if there are no new instructions, we dont have to create a block context - if (scratch_top == t.scratch.items.len) - return inner_expr orelse unreachable; // null means we added break or return + if (scratch.len() == 0) + return inner_expr orelse + unreachable; // null means we added break or return if (target.block_index) |block| { - if (inner_expr) |result| { - _ = try t.addInstRef(.{ - .tag = .@"break", - .data = EncodedData.encode(.@"break", .{ - .src_node = decl.nodeOffset(node), - .extra_index = try t.extra.append(t.gpa, Inst.Break, .{ - .block = block, - .operand = result, - }), + if (inner_expr) |out| { + _ = try t.addRef(.@"break", .{ + .src_node = ctx.nodeOffset(node), + .extra_index = try t.extra.append(t.gpa, Teg.Break, .{ + .block = block, + .operand = out, }), }); } - const content = try t.scratchToIndexList(scratch_top); - t.scratch.shrinkRetainingCapacity(scratch_top); + const content = try t.scratchToIndexList(scratch); + scratch.deinit(); t.getInstData(block).* = EncodedData.encode(.block, .{ - .src_node = decl.nodeOffset(node), + .src_node = ctx.nodeOffset(node), .extra_index = try t.extra.append(t.gpa, IndexList, content), }); - try t.addScratch(Inst.Index, block); + try t.scratch.add(t.arena, Inst.Index, block); return block.toRef(); } else { @@ -1012,9 +1087,12 @@ fn transformExpression(t: *Transform, decl: BlockContext, node: NodeIndex) Alloc for (statements, 1..) |statement, i| { const is_last = i == statements.len; last = try t.transformBlockStatement( - decl, + ctx, statement, - tag == .block_implicit_return and is_last, + if (tag == .block_implicit_return and is_last) + result + else + null, ) orelse { return null; }; @@ -1023,8 +1101,7 @@ fn transformExpression(t: *Transform, decl: BlockContext, node: NodeIndex) Alloc return if (tag == .block_implicit_return) last else .void; }, - .builtin_call, - => { + .builtin_call => { const builtin = Builtin.map.get(t.ast.basicNodeSlice(node)[1..]) orelse { try t.report.err(.invalid_builtin, .{ .node = node }); return .void; @@ -1034,12 +1111,12 @@ fn transformExpression(t: *Transform, decl: BlockContext, node: NodeIndex) Alloc const args = base.slice(&t.ast); return switch (builtin) { - .js => Builtin.builtin_js(t, decl, node, args), - .js_new => Builtin.builtin_jsNew(t, decl, node, args), + .js => Builtin.builtin_js(t, ctx, node, args), + .js_new => Builtin.builtin_jsNew(t, ctx, node, args), // .import => Builtin.import(t, decl, node, args), // .fetch => Builtin.fetch(t, decl, node, args), - .compile_error => Builtin.builtin_compileError(t, decl, node, args), - .type_of => Builtin.builtin_typeOf(t, decl, node, args), + .compile_error => Builtin.builtin_compileError(t, ctx, node, args), + .type_of => Builtin.builtin_typeOf(t, ctx, node, args), else => @panic("TODO"), }; }, @@ -1048,17 +1125,35 @@ fn transformExpression(t: *Transform, decl: BlockContext, node: NodeIndex) Alloc const call = t.ast.nodeData(node).call; const args = t.ast.extraData(call.extra, Ast.Call).args.slice(&t.ast); - const base = try t.transformExpression(decl, call.base) orelse return null; - const args_list = try t.transformExpressionList(decl, args) orelse return null; - - const call_inst = try t.addInstRef(.{ - .tag = .call, - .data = EncodedData.encode(.call, .{ - .src_node = decl.nodeOffset(node), - .extra_index = try t.extra.append(t.gpa, Inst.Call, .{ - .base = base, - .args = args_list, - }), + var base_result: ResultType = .none; + const base = try t.transformExpression(ctx, &base_result, call.base) orelse return null; + + const args_list: RefList = call_args: { + const extra_top = try t.extra.alloc(t.gpa, args.len); + + for (args, 0.., extra_top..) |arg, arg_id, index| { + var arg_result: ResultType = .{ .argument = .{ + .call_base = base, + .argument = @intCast(arg_id), + } }; + t.extra.setU32( + index, + @intFromEnum(try t.transformExpression(ctx, &arg_result, arg) orelse + return null), + ); + } + + break :call_args .{ + .start = @intCast(extra_top), + .len = @intCast(args.len), + }; + }; + + const call_inst = try t.addRef(.call, .{ + .src_node = ctx.nodeOffset(node), + .extra_index = try t.extra.append(t.gpa, Teg.Call, .{ + .base = base, + .args = args_list, }), }); @@ -1068,28 +1163,23 @@ fn transformExpression(t: *Transform, decl: BlockContext, node: NodeIndex) Alloc .field => { const field = t.ast.nodeData(node).field; - const base = try t.transformExpression(decl, field.base) orelse return null; + var base_result: ResultType = .none; + const base = try t.transformExpression(ctx, &base_result, field.base) orelse return null; // TODO: handle identifier escapes const id = t.ast.tokenSlice(field.field_token); const prop_name_str = try t.addSlicedString(id); - const string_ref = try t.addInstRef(.{ - .tag = .string, - .data = EncodedData.encode(.string, .{ - .src_node = decl.nodeOffset(node), - .extra_index = try t.extra.append(t.gpa, SlicedString, prop_name_str), - }), + const string_ref = try t.addRef(.string, .{ + .src_node = ctx.nodeOffset(node), + .extra_index = try t.extra.append(t.gpa, SlicedString, prop_name_str), }); - return try t.addInstRef(.{ - .tag = .field, - .data = EncodedData.encode(.field, .{ - .src_node = decl.nodeOffset(node), - .extra_index = try t.extra.append(t.gpa, Inst.Bin, .{ - .lhs = base, - .rhs = string_ref, - }), + return try t.addRef(.field, .{ + .src_node = ctx.nodeOffset(node), + .extra_index = try t.extra.append(t.gpa, Teg.Bin, .{ + .lhs = base, + .rhs = string_ref, }), }); }, @@ -1098,45 +1188,36 @@ fn transformExpression(t: *Transform, decl: BlockContext, node: NodeIndex) Alloc const if_node = t.ast.nodeData(node).@"if"; const if_data = t.ast.extraData(if_node.extra, Ast.If); - const cond = try t.transformExpression(decl, if_node.cond) orelse + var cond_result: ResultType = .{ .ref = .bool_type }; + const cond = try t.transformExpression(ctx, &cond_result, if_node.cond) orelse return null; - const yes = try t.transformExpressionToBlock( - decl, - if_data.yes, - false, - ); + var yes_result = result.*; + const yes = try t.transformToBlock(ctx, if_data.yes, .tracked_result, &yes_result); + + // TODO: can this re-use the resolved ref from above + var no_result: ResultType = if (if_data.no == .none) .used else result.*; const no = if (if_data.no.unwrap()) |no| - try t.transformExpressionToBlock( - decl, + try t.transformToBlock( + ctx, no, - false, + .tracked_result, + &no_result, ) else Inst.Ref.void; - if (yes == .void and no == .void) { - // when both branches do nothing, we can omit the branch, - // but we still need to type-check the condition operand - _ = try t.addInstRef(.{ - .tag = .ensure_bool, - .data = EncodedData.encode(.ensure_bool, .{ - .src_node = decl.nodeOffset(node), - .operand = cond, - }), - }); - return .void; + // Mark the incoming result type as used if both branches use the result type + if (result.* != .none and yes_result == .none and no_result == .none) { + result.* = .used; } - return try t.addInstRef(.{ - .tag = .@"if", - .data = EncodedData.encode(.@"if", .{ - .src_node = decl.nodeOffset(node), - .extra_index = try t.extra.append(t.gpa, Inst.If, .{ - .cond = cond, - .yes = yes, - .no = no, - }), + return try t.addRef(.@"if", .{ + .src_node = ctx.nodeOffset(node), + .extra_index = try t.extra.append(t.gpa, Teg.If, .{ + .cond = cond, + .yes = yes, + .no = no, }), }); }, @@ -1144,36 +1225,35 @@ fn transformExpression(t: *Transform, decl: BlockContext, node: NodeIndex) Alloc .@"return" => { const ret_node = t.ast.nodeData(node).@"return"; - const ret_block_ptr = decl.return_target orelse { + const target = ctx.return_target orelse { try t.report.err(.invalid_return, .{ .node = node }); return .void; }; - const ret_block = if (ret_block_ptr.*) |block| + const ret_block = if (target.generated_block) |block| block else append_block: { - const block = try t.addRootInst(.{ - .tag = .block, - .data = undefined, - }); - ret_block_ptr.* = block; + const block = try t.addUntracked(.block, undefined); + target.generated_block = block; break :append_block block; }; + var return_result = target.result_type; + const operand: Inst.Ref = if (ret_node.expr.unwrap()) |expr| - try t.transformExpression(decl.withReturn(null), expr) orelse - return null + try t.transformExpression( + ctx.withReturn(null), + &return_result, + expr, + ) orelse return null else .void; - _ = try t.addInstRef(.{ - .tag = .@"break", - .data = EncodedData.encode(.@"break", .{ - .src_node = decl.nodeOffset(node), - .extra_index = try t.extra.append(t.gpa, Inst.Break, .{ - .block = ret_block, - .operand = operand, - }), + _ = try t.addIndex(.@"break", .{ + .src_node = ctx.nodeOffset(node), + .extra_index = try t.extra.append(t.gpa, Teg.Break, .{ + .block = ret_block, + .operand = operand, }), }); @@ -1187,36 +1267,29 @@ fn transformExpression(t: *Transform, decl: BlockContext, node: NodeIndex) Alloc @panic("TODO: unlabelled breaks"), ); - const block = try decl.findAndMarkBreak(t, label) orelse { + const block: *BlockContext.BreakTarget = ctx.lookupNamedBreak(label) orelse { try t.report.err(.unresolved_label, .{ .node = node }); return null; }; + var break_result = block.result_type.*; const operand: Inst.Ref = if (brk_node.value.unwrap()) |expr| - try t.transformExpression(decl.withReturn(null), expr) orelse + try t.transformExpression(ctx.withReturn(null), &break_result, expr) orelse return null else .void; - _ = try t.addInstRef(.{ - .tag = .@"break", - .data = EncodedData.encode(.@"break", .{ - .src_node = decl.nodeOffset(node), - .extra_index = try t.extra.append(t.gpa, Inst.Break, .{ - .block = block, - .operand = operand, - }), + _ = try t.addIndex(.@"break", .{ + .src_node = ctx.nodeOffset(node), + .extra_index = try t.extra.append(t.gpa, Teg.Break, .{ + .block = try block.getInst(t), + .operand = operand, }), }); return null; // control flow ends }, - .group => { - const group_node = t.ast.nodeData(node).un; - return try t.transformExpression(decl, group_node.expr); - }, - .struct_decl => { const struct_node = t.ast.fullStructDecl(node); const payload_index = try t.extra.alloc(t.gpa, struct_node.fields.len * 2); @@ -1229,32 +1302,39 @@ fn transformExpression(t: *Transform, decl: BlockContext, node: NodeIndex) Alloc const field = t.ast.fullStructField(field_node); const id = try t.decodeIdentifier(field.id); + const key = try t.addNullTerminatedString(id); - t.extra.setU32(i, @intFromEnum(try t.addNullTerminatedString(id))); - t.extra.setU32(j, @intFromEnum(try t.transformExpressionToBlock(decl, field.type, false))); + var type_result: ResultType = .{ .ref = .type_type }; + const value = try t.transformExpression(ctx, &type_result, field.type) orelse + return null; + + t.extra.setU32(i, @intFromEnum(key)); + t.extra.setU32(j, @intFromEnum(value)); } - return try t.addInstRef(.{ - .tag = .struct_decl, - .data = EncodedData.encode(.struct_decl, .{ - .src_node = decl.nodeOffset(node), - .extra_index = try t.extra.append(t.gpa, Inst.StructDecl, .{ - .payload_index = payload_index, - .field_len = @intCast(struct_node.fields.len), - }), + return try t.addRef(.struct_decl, .{ + .src_node = ctx.nodeOffset(node), + .extra_index = try t.extra.append(t.gpa, Teg.StructDecl, .{ + .payload_index = payload_index, + .field_len = @intCast(struct_node.fields.len), }), }); }, .struct_init => { const struct_node = t.ast.fullStructInit(node); + // TODO: use t.scratch for this const payload_index = try t.extra.alloc(t.gpa, struct_node.fields.len * 2); - const base_type = try t.transformExpression( - decl, - struct_node.type orelse @panic("TODO: TEG implicit struct"), - ) orelse - @panic("TODO: print better error"); + var result_type: ResultType = .{ .ref = .type_type }; + const base_type = if (struct_node.type) |ty| + try t.transformExpression(ctx, &result_type, ty) orelse + return null + else + (try result.consume(t)) orelse { + try t.report.err(.missing_result_type, .{ .node = node }); + return .void; + }; for ( struct_node.fields, @@ -1264,21 +1344,30 @@ fn transformExpression(t: *Transform, decl: BlockContext, node: NodeIndex) Alloc const field = t.ast.fullStructField(field_node); const id = try t.decodeIdentifier(field.id); + const field_name = try t.addNullTerminatedString(id); - t.extra.setU32(i, @intFromEnum(try t.addNullTerminatedString(id))); - t.extra.setU32(j, @intFromEnum((try t.transformExpression(decl, field.type)) orelse - @panic("TODO: print better error"))); + var field_result_type: ResultType = .{ .struct_field = .{ + .type = base_type, + .field = field_name, + } }; + const field_value = (try t.transformExpression( + ctx, + &field_result_type, + field.type, + )) orelse return null; + + t.extra.setU32(i, @intFromEnum(field_name)); + t.extra.setU32(j, @intFromEnum( + field_value, + )); } - return try t.addInstRef(.{ - .tag = .struct_init, - .data = EncodedData.encode(.struct_init, .{ - .src_node = decl.nodeOffset(node), - .extra_index = try t.extra.append(t.gpa, Inst.StructInit, .{ - .base_type = base_type, - .payload_index = payload_index, - .field_len = @intCast(struct_node.fields.len), - }), + return try t.addRef(.struct_init, .{ + .src_node = ctx.nodeOffset(node), + .extra_index = try t.extra.append(t.gpa, Teg.StructInit, .{ + .base_type = base_type, + .payload_index = payload_index, + .field_len = @intCast(struct_node.fields.len), }), }); }, @@ -1286,17 +1375,19 @@ fn transformExpression(t: *Transform, decl: BlockContext, node: NodeIndex) Alloc .array_init => { const array_init = t.ast.nodeData(node).list; - const items = try t.transformExpressionList(decl, array_init.slice(&t.ast)) orelse - return null; + const array_type = try result.consume(t); - return try t.addInstRef(.{ - .tag = .array_init, - .data = EncodedData.encode(.array_init, .{ - .src_node = decl.nodeOffset(node), - .extra_index = try t.extra.append(t.gpa, Inst.ArrayInit, .{ - .type_hint = .none, - .list = items, - }), + const items = try t.transformExpressionList( + ctx, + if (array_type) |ty| .{ .array_value = ty } else .none, + array_init.slice(&t.ast), + ) orelse return null; + + return try t.addRef(.array_init, .{ + .src_node = ctx.nodeOffset(node), + .extra_index = try t.extra.append(t.gpa, Teg.ArrayInit, .{ + .type_hint = .none, + .list = items, }), }); }, @@ -1305,12 +1396,16 @@ fn transformExpression(t: *Transform, decl: BlockContext, node: NodeIndex) Alloc } } -fn transformExpressionList(t: *Transform, decl: BlockContext, args: []const NodeIndex) !?RefList { +fn transformExpressionList(t: *Transform, decl: BlockContext, result: ResultType, args: []const NodeIndex) !?RefList { const extra_top = try t.extra.alloc(t.gpa, args.len); for (args, extra_top..) |arg, i| { - t.extra.setU32(i, @intFromEnum(try t.transformExpression(decl, arg) orelse - return null)); + var result_mut = result; + t.extra.setU32( + i, + @intFromEnum(try t.transformExpression(decl, &result_mut, arg) orelse + return null), + ); } return .{ @@ -1319,7 +1414,25 @@ fn transformExpressionList(t: *Transform, decl: BlockContext, args: []const Node }; } -fn transformAssignmentTarget(t: *Transform, decl: BlockContext, node: NodeIndex) Allocator.Error!?Inst.Index { +/// The left-hand side of an assignment is known as the "assignment target". +/// This returns the assignment target's Ref, as well as a result type to visit +/// the right-hand side with. Note that only a few expressions are valid here: +/// +/// - A plain identifier, which is resolved directly to a `memory` instruction +/// - A `field` node: TODO +/// - An `index` node: TODO +/// +/// Note that these three are the root nodes, it is legal to have: +/// +/// get().x = 1; // `field` of a `call` node +/// +/// In the end, the assignment target must comprise either a ref to a `memory` +/// or a value with a reference type; and optionally a sub property. +fn transformAssignmentTarget( + t: *Transform, + decl: BlockContext, + node: NodeIndex, +) Allocator.Error!?struct { Inst.Index, ResultType } { const node_tag = t.ast.nodes.items(.tag)[node]; const trace = tracer.traceFmt(@src(), "{s}", .{@tagName(node_tag)}); @@ -1345,7 +1458,7 @@ fn transformAssignmentTarget(t: *Transform, decl: BlockContext, node: NodeIndex) return null; } - const lookup = t.lookupId(id) orelse { + const lookup: *Scope.Symbol = t.lookupId(id) orelse { try t.report.err(.unknown_identifier, .{ .node = node }); return null; }; @@ -1353,7 +1466,7 @@ fn transformAssignmentTarget(t: *Transform, decl: BlockContext, node: NodeIndex) return switch (lookup.data) { .mutable => |inst| { lookup.mutated = true; - return inst; + return .{ inst, lookup.type.resolve() }; }, else => { lookup.used = true; @@ -1371,6 +1484,7 @@ fn transformAssignmentTarget(t: *Transform, decl: BlockContext, node: NodeIndex) }, }, }); + return null; }, }; @@ -1379,10 +1493,13 @@ fn transformAssignmentTarget(t: *Transform, decl: BlockContext, node: NodeIndex) // TODO: allow field expressions, but these need to emit a different store node else => { - _ = try t.transformExpression(decl, node); + var result_type: ResultType = .none; + _ = try t.transformExpression(decl, &result_type, node); + try t.report.err(.invalid_assignment_target, .{ .node = node, }); + return null; }, } @@ -1390,10 +1507,13 @@ fn transformAssignmentTarget(t: *Transform, decl: BlockContext, node: NodeIndex) fn transformBlockStatement( t: *Transform, - decl: BlockContext, + ctx: BlockContext, node: NodeIndex, - is_implicit_return_expression: bool, + maybe_result: ?*ResultType, ) !?Inst.Ref { + var void_result_storage: ResultType = .{ .ref = .void }; + const result = maybe_result orelse &void_result_storage; + const node_tag = t.ast.nodes.items(.tag)[node]; const trace = tracer.traceFmt(@src(), "{s}", .{@tagName(node_tag)}); @@ -1437,16 +1557,16 @@ fn transformBlockStatement( return .void; }, - else => try t.transformExpression(decl, var_decl.init) orelse - return null, + else => ref: { + var empty_type: ResultType = .none; + break :ref try t.transformExpression(ctx, &empty_type, var_decl.init) orelse + return null; + }, }; - return try t.addInstRef(.{ - .tag = .discard, - .data = EncodedData.encode(.discard, .{ - .src_node = decl.nodeOffset(node), - .operand = ref, - }), + return try t.addRef(.discard, .{ + .src_node = ctx.nodeOffset(node), + .operand = ref, }); } @@ -1464,36 +1584,37 @@ fn transformBlockStatement( return .void; } + var type_result: ResultType = .{ .ref = .type_type }; const type_ref = if (var_decl.type) |data_type| - try t.transformExpression(decl, data_type) + try t.transformExpression(ctx, &type_result, data_type) else null; - const data_ref = try t.transformExpression(decl, var_decl.init) orelse + + var value_result: ResultType = if (type_ref) |ty| + .{ .ref = ty } + else + .none; + + const data_ref = try t.transformExpression(ctx, &value_result, var_decl.init) orelse return null; - const data_coerced_ref = if (type_ref) |ref| - try t.addInstRef(.{ - .tag = .as, - .data = EncodedData.encode(.as, .{ - .src_node = decl.nodeOffset(node), - .extra_index = try t.extra.append(t.gpa, Inst.As, .{ - .type = ref, - .operand = data_ref, - }), - }), - }) - else - data_ref; - const mem_ref = if (var_decl.is_const) - data_coerced_ref - else - try t.addInstRef(.{ - .tag = .memory, - .data = EncodedData.encode(.memory, .{ - .src_node = decl.nodeOffset(node), - .operand = data_coerced_ref, - }), - }); + // Add a .coerce instruction if there was a type declared. + const data_coerced_ref = coerced: { + if (type_ref) |ref| { + // We can avoid the coercion instruction if the result type was used. + if (value_result != .used) { + break :coerced try t.addRef(.coercion, .{ + .src_node = ctx.nodeOffset(node), + .extra_index = try t.extra.append(t.gpa, Teg.Coercion, .{ + .type = ref, + .operand = data_ref, + }), + }); + } + } + + break :coerced data_ref; + }; const gop = try t.scope().symbols.getOrPut(t.gpa, id); @@ -1505,16 +1626,26 @@ fn transformBlockStatement( gop.value_ptr.* = .{ .node = node, - .data = switch (var_decl.is_const) { - true => .{ .immutable = mem_ref }, - false => .{ .mutable = mem_ref.index().? }, - }, + .data = if (var_decl.is_const) + .{ .immutable = data_coerced_ref } + else + .{ + .mutable = try t.addIndex(.memory, .{ + .src_node = ctx.nodeOffset(node), + .operand = data_coerced_ref, + }), + }, + .type = if (type_ref) |ty| + .{ .present = ty } + else + .{ .missing = data_ref }, }; return .void; }, .fn_decl, + .struct_decl, => |tag| { std.debug.panic("TODO: block statement {s}", .{@tagName(tag)}); }, @@ -1530,18 +1661,17 @@ fn transformBlockStatement( // discard, instead of appending an .ensure_void check, // we will add a .discard instruction instead. // this will emit a warning if given void. - const result = try t.transformExpression(decl, bin.rhs) orelse + var result_type: ResultType = .none; + const value = try t.transformExpression(ctx, &result_type, bin.rhs) orelse return null; - return try t.addInstRef(.{ - .tag = .discard, - .data = EncodedData.encode(.discard, .{ - .src_node = decl.nodeOffset(node), - .operand = result, - }), + + return try t.addRef(.discard, .{ + .src_node = ctx.nodeOffset(node), + .operand = value, }); } else { // process this assignment as usual, do not append .ensure_void - return try t.transformExpression(decl, node); + return try t.transformExpression(ctx, result, node); } }, @@ -1560,50 +1690,51 @@ fn transformBlockStatement( .shr_assign, => { // do not append a .ensure_void check - return try t.transformExpression(decl, node); + return try t.transformExpression(ctx, result, node); }, else => { - const result = try t.transformExpression(decl, node) orelse + const value = try t.transformExpression(ctx, result, node) orelse return null; - if (!is_implicit_return_expression and result != .void) { + if (maybe_result == null and value != .void) { if (t.ast.nodeTag(node) == .id) { try t.report.warn(.dangling_identifier, .{ .node = node, }); - return result; } - return try t.addInstRef(.{ - .tag = .ensure_void, - .data = EncodedData.encode(.ensure_void, .{ - .src_node = decl.nodeOffset(node), - .operand = result, - }), + return try t.addRef(.ensure_void, .{ + .src_node = ctx.nodeOffset(node), + .operand = value, }); } - return result; + return value; }, } } -fn addRootInst(t: *Transform, inst: Inst) !Inst.Index { - try t.insts.append(t.gpa, inst); +fn addUntracked(t: *Transform, comptime tag: Teg.Tag, data: tag.Data()) !Inst.Index { + try t.insts.append(t.gpa, .{ + .tag = tag, + .data = EncodedData.encode(tag, data), + }); return @enumFromInt(t.insts.len - 1); } -fn addInstRef(t: *Transform, inst: Inst) !Inst.Ref { - try t.scratch.ensureUnusedCapacity(t.gpa, 1); - try t.insts.append(t.gpa, inst); - const index: Inst.Index = @enumFromInt(t.insts.len - 1); - t.scratch.appendAssumeCapacity(@intFromEnum(index)); - return index.toRef(); +fn addIndex(t: *Transform, comptime tag: Teg.Tag, data: tag.Data()) !Inst.Index { + const index = try t.addUntracked(tag, data); + try t.scratch.add(t.arena, Inst.Index, index); + return index; +} + +fn addRef(t: *Transform, comptime tag: Teg.Tag, data: tag.Data()) !Inst.Ref { + return (try t.addIndex(tag, data)).toRef(); } fn pushScope(t: *Transform) !void { - try t.scopes.append(t.gpa, .{}); + try t.scopes.append(t.arena, .{}); } fn popScope(t: *Transform) void { @@ -1620,22 +1751,18 @@ fn getInstData(t: *Transform, index: Inst.Index) *EncodedData { return &t.insts.items(.data)[@intFromEnum(index)]; } -fn addScratch(t: *Transform, comptime T: type, value: T) !void { - try t.scratch.append(t.gpa, @intFromEnum(value)); -} - -fn scratchToRefList(t: *Transform, scratch_top: usize) !RefList { - const items = t.scratch.items[scratch_top..]; +fn scratchToRefList(t: *Transform, scratch_top: core.U32Stack.Mark) !RefList { + const items = scratch_top.slice(u32); const extra_top = t.extra.list.items.len; - try t.extra.list.appendSlice(t.gpa, t.scratch.items[scratch_top..]); + try t.extra.list.appendSlice(t.gpa, items); return .{ .start = @intCast(extra_top), .len = @intCast(items.len), }; } -fn scratchToIndexList(t: *Transform, scratch_top: usize) !IndexList { - const items = t.scratch.items[scratch_top..]; +fn scratchToIndexList(t: *Transform, scratch: core.U32Stack.Mark) !IndexList { + const items = scratch.slice(u32); const extra_top = t.extra.list.items.len; try t.extra.list.appendSlice(t.gpa, items); return .{ @@ -1715,8 +1842,8 @@ fn addDecodedSlicedString(t: *Transform, escaped: []const u8) !SlicedString { // string. this is done so that duplicate strings will get de-duplicated // in string_bytes. It is more likely that there will be remaining bytes // in `.scratch` vs `.id_decode_buf` - try t.scratch.ensureUnusedCapacity(t.gpa, @divFloor(len, @sizeOf(u32)) + 1); - const buf = mem.sliceAsBytes(t.scratch.unusedCapacitySlice()); + try t.scratch.list.ensureUnusedCapacity(t.gpa, @divFloor(len, @sizeOf(u32)) + 1); + const buf = mem.sliceAsBytes(t.scratch.list.unusedCapacitySlice()); break :brk Tokenizer.decodeStringBuf(escaped, buf); } else escaped; @@ -1767,7 +1894,8 @@ const mem = std.mem; const Allocator = mem.Allocator; const assert = std.debug.assert; -const Teg = @import("Teg.zig"); +const core = @import("mod.zig"); +const Teg = core.Teg; const Inst = Teg.Inst; const NullTerminatedString = Teg.NullTerminatedString; const SlicedString = Teg.SlicedString; @@ -1776,15 +1904,15 @@ const IndexList = Teg.IndexList; const EncodedData = Teg.EncodedData; const ExtraIndex = Teg.ExtraIndex; -const dod = @import("array.zig"); -const ExtraStore = dod.ExtraStore(.teg, .@"var"); -const Float = dod.Float; +const extra = core.array; +const ExtraStore = extra.ExtraStore(.teg, .@"var"); +const Float = extra.Float; -const Tokenizer = @import("Tokenizer.zig"); +const Tokenizer = core.Tokenizer; const Token = Tokenizer.Token; const ByteOffset = Tokenizer.ByteOffset; -const Ast = @import("Ast.zig"); +const Ast = core.Ast; const Unused = Ast.Unused; const Node = Ast.Node; const Data = Node.Data; @@ -1798,9 +1926,9 @@ const optionalTokenIndex = OptionalTokenIndex.init; const optionalNodeIndex = OptionalNodeIndex.init; const optionalExtraIndex = OptionalExtraIndex.init; -const InternPool = @import("InternPool.zig"); +const InternPool = core.InternPool; -const Report = @import("Report.zig"); -const Comp = @import("Comp.zig"); +const Report = core.Report; +const Comp = core.Comp; const tracer = @import("tracer"); diff --git a/src/core/main.zig b/src/core/main.zig index dbe1cb55392467ffb1024f22a35fcc03aed2a3f7..c7bb7ce0fc99035cc82189db440f4a8e6890f409 100644 --- a/src/core/main.zig +++ b/src/core/main.zig @@ -22,11 +22,6 @@ pub fn main() !void { try unicode_props.init(); - if (builtin.mode == .Debug and std.io.getStdErr().isTty()) { - // zig build system doesnt print a newline :/ - std.debug.print("\r\x1b[K", .{}); - } - const use_gpa = builtin.mode == .Debug; var gpa = if (use_gpa) std.heap.GeneralPurposeAllocator(.{ diff --git a/src/core/mod.zig b/src/core/mod.zig index cc3a1e17802775e1f7583fd91c2e5237571d1db1..a7c78c625208ae3f521622d5c35a1598945bc9fa 100644 --- a/src/core/mod.zig +++ b/src/core/mod.zig @@ -32,6 +32,9 @@ pub const Sema = @import("Sema.zig"); pub const Stack = @import("stack.zig").Stack; pub const U32Stack = Stack(u32); +pub const StoredTrace = @import("StoredTrace.zig"); +pub const DebugStoredTrace = StoredTrace.DebugStoredTrace; + /// Untyped Intermediate Representation. Flat instruction list pub const Teg = @import("Teg.zig"); -- 2.54.0