const std = @import("std"); const Ast = @import("Ast.zig"); const Ir = @import("Ir.zig"); const Story = @import("Story.zig"); const compile = @import("compile.zig"); const InternPool = compile.InternPool; const Module = compile.Module; const assert = std.debug.assert; const Sema = @This(); gpa: std.mem.Allocator, arena: std.mem.Allocator, module: *compile.Module, ir: Ir, inst_map: std.AutoHashMapUnmanaged(Ir.Inst.Index, ValueInfo) = .empty, errors: *std.ArrayListUnmanaged(Module.Error), const InnerError = error{ OutOfMemory, AnalysisFail, TooManyConstants, InvalidJump, } || anyerror; pub const ValueInfo = union(enum) { none, stack, value: InternPool.Index, variable: InternPool.Index, knot: InternPool.Index, stitch: InternPool.Index, function: InternPool.Index, temp: u32, }; pub const Value = struct { ip_index: InternPool.Index, pub const Unwrapped = union(enum) { bool: bool, int: i64, float: f64, pub fn toFloat(v: Unwrapped) f64 { return switch (v) { .bool => |boolean| @floatFromInt(@intFromBool(boolean)), .int => |int| @floatFromInt(int), .float => |float| float, }; } pub fn isTruthy(v: Unwrapped) bool { return switch (v) { //.null => false, .bool => |boolean| boolean, .int => |int| int != 0, .float => |float| float != 0.0, //.str => true, }; } pub fn coerce(value: Unwrapped) Unwrapped { return switch (value) { .int => value, .float => value, .bool => |boolean| .{ .int = if (boolean) 1 else 0 }, //else => null, }; } }; pub fn unwrap(value: Value, ip: *InternPool) Unwrapped { switch (ip.values.items[@intFromEnum(value.toInterned())]) { .bool => |boolean| return .{ .bool = boolean }, .int => |int| return .{ .int = int }, .float => |float| return .{ .float = @bitCast(float) }, .str => @panic("String unwrapping not implemented!"), } } pub fn fromInterned(index: InternPool.Index) Value { assert(index != .none); return .{ .ip_index = index }; } pub fn toInterned(value: Value) InternPool.Index { assert(value.ip_index != .none); return value.ip_index; } }; pub const SrcLoc = struct { src_offset: u32, }; fn fail( sema: *Sema, src: SrcLoc, comptime format: []const u8, args: anytype, ) error{ OutOfMemory, AnalysisFail } { // TODO: Revisit this const source_bytes = sema.module.tree.source; const message = try std.fmt.allocPrint(sema.arena, format, args); const loc = compile.findLineColumn(source_bytes, src.src_offset); try sema.errors.append(sema.gpa, .{ .line = loc.line, .column = loc.column, .snippet = loc.source_line, .message = message, }); return error.AnalysisFail; } fn resolveInst(sema: *Sema, ref: Ir.Inst.Ref) ValueInfo { if (ref.toIndex()) |index| { return sema.inst_map.get(index).?; } return .{ .value = @enumFromInt(@intFromEnum(ref)) }; } fn resolveValue(_: *Sema, info: ValueInfo) ?Value { switch (info) { .value => |value| return .fromInterned(value), else => return null, } } pub fn lookupIdentifier( sema: *Sema, builder: *Builder, ident: InternPool.Index, src: SrcLoc, ) !Module.Namespace.Decl { return sema.lookupInNamespace(builder.namespace, ident, src); } pub fn lookupInNamespace( sema: *Sema, namespace: *Module.Namespace, ident: InternPool.Index, src: SrcLoc, ) !Module.Namespace.Decl { var scope: ?*Module.Namespace = namespace; while (scope) |s| : (scope = s.parent) { if (s.decls.get(ident)) |decl| return decl; } return sema.fail(src, "unknown identifier", .{}); } pub fn deinit(sema: *Sema) void { sema.inst_map.deinit(sema.gpa); sema.* = undefined; } pub const Builder = struct { sema: *Sema, namespace: *Module.Namespace, code: *Module.CodeChunk, constants_map: std.AutoHashMapUnmanaged(InternPool.Index, u8) = .empty, labels: std.ArrayListUnmanaged(Label) = .empty, fixups: std.ArrayListUnmanaged(Fixup) = .empty, const Label = struct { code_offset: usize, }; const Fixup = struct { mode: enum { relative, absolute, }, label_index: u32, code_offset: u32, }; const dummy_address = 0xffffffff; pub fn deinit(builder: *Builder, gpa: std.mem.Allocator) void { builder.constants_map.deinit(gpa); builder.labels.deinit(gpa); builder.fixups.deinit(gpa); } /// Reserve a stack slot for temporary variables. fn addStackSlot(builder: *Builder) u8 { const new_slot = builder.code.stack_size; builder.code.stack_size += 1; return @intCast(new_slot); } /// Reserve a stack slot for a parameter. fn addParameter(builder: *Builder) u8 { builder.code.args_count += 1; return builder.addStackSlot(); } fn addByteOp(builder: *Builder, op: Story.Opcode) error{OutOfMemory}!void { const gpa = builder.sema.gpa; const bytecode = &builder.code.bytecode; //const byte_index = bytecode.items.len; try bytecode.append(gpa, @intFromEnum(op)); //return .toIndex(); } fn addConstOp(builder: *Builder, op: Story.Opcode, arg: u8) error{OutOfMemory}!void { const gpa = builder.sema.gpa; const bytecode = &builder.code.bytecode; //const byte_index = bytecode.items.len; try bytecode.ensureUnusedCapacity(gpa, 2); bytecode.appendAssumeCapacity(@intFromEnum(op)); bytecode.appendAssumeCapacity(arg); //return .{ .index = @intCast(byte_index) }; } fn addJumpOp(builder: *Builder, op: Story.Opcode) error{OutOfMemory}!u32 { const gpa = builder.sema.gpa; const bytecode = &builder.code.bytecode; try bytecode.ensureUnusedCapacity(gpa, 3); bytecode.appendAssumeCapacity(@intFromEnum(op)); bytecode.appendAssumeCapacity(0xff); bytecode.appendAssumeCapacity(0xff); return @intCast(bytecode.items.len - 2); } fn addFixup(builder: *Builder, op: Story.Opcode, label: usize) !void { return builder.fixups.append(builder.sema.gpa, .{ .mode = .relative, .label_index = @intCast(label), .code_offset = try builder.addJumpOp(op), }); } fn addFixupAbsolute(builder: *Builder, op: Story.Opcode, label: usize) !void { return builder.fixups.append(builder.sema.gpa, .{ .mode = .absolute, .label_index = @intCast(label), .code_offset = try builder.addJumpOp(op), }); } fn addLabel(builder: *Builder) error{OutOfMemory}!usize { const label_index = builder.labels.items.len; try builder.labels.append(builder.sema.gpa, .{ .code_offset = dummy_address, }); return label_index; } fn setLabel(builder: *Builder, label_index: usize) void { const bytecode = &builder.code.bytecode; const code_offset = bytecode.items.len; assert(label_index <= builder.labels.items.len); const label_data = &builder.labels.items[label_index]; label_data.code_offset = code_offset; } fn getOrPutConstantIndex(builder: *Builder, index: InternPool.Index) !u8 { const gpa = builder.sema.gpa; const constants = &builder.code.constants; if (builder.constants_map.get(index)) |local_index| return local_index; const local_index: u8 = @intCast(constants.items.len); try constants.append(gpa, @intCast(@intFromEnum(index))); try builder.constants_map.put(gpa, index, local_index); return local_index; } pub fn finalize(builder: *Builder) !void { const bytecode = &builder.code.bytecode; for (builder.fixups.items) |fixup| { const label = builder.labels.items[fixup.label_index]; assert(label.code_offset != dummy_address); const target_offset: usize = switch (fixup.mode) { .relative => label.code_offset - fixup.code_offset - 2, .absolute => label.code_offset, }; if (target_offset >= std.math.maxInt(u16)) { std.debug.print("Too much code to jump over!\n", .{}); return error.InvalidJump; } assert(bytecode.capacity >= label.code_offset + 2); bytecode.items[fixup.code_offset] = @intCast((target_offset >> 8) & 0xff); bytecode.items[fixup.code_offset + 1] = @intCast(target_offset & 0xff); } } fn ensureLoad(self: *Builder, info: ValueInfo) InnerError!void { switch (info) { .none => unreachable, // caller should never load .none .stack => {}, .value => |index| { const local_index = try self.getOrPutConstantIndex(index); try self.addConstOp(.load_const, @intCast(local_index)); }, .variable, .knot, .stitch, .function, => |index| { const local_index = try self.getOrPutConstantIndex(index); try self.addConstOp(.load_global, @intCast(local_index)); }, .temp => |temp| try self.addConstOp(.load, @intCast(temp)), } } }; fn foldArith( lhs: Value.Unwrapped, rhs: Value.Unwrapped, op: Story.Opcode, ) !Value.Unwrapped { const l = lhs.coerce(); const r = rhs.coerce(); if (l == .int and r == .int) { return switch (op) { .add => .{ .int = l.int +% r.int }, .sub => .{ .int = l.int -% r.int }, .mul => .{ .int = l.int *% r.int }, .div => if (r.int == 0) error.DivisionByZero else .{ .int = @divTrunc(l.int, r.int) }, .mod => if (r.int == 0) error.DivisionByZero else .{ .int = @mod(l.int, r.int) }, else => unreachable, }; } const lf = l.toFloat(); const rf = r.toFloat(); return switch (op) { .add => .{ .float = lf + rf }, .sub => .{ .float = lf - rf }, .mul => .{ .float = lf * rf }, .div => if (rf == 0.0) error.DivisionByZero else .{ .float = lf / rf }, .mod => if (rf == 0.0) error.DivisionByZero else .{ .float = @mod(lf, rf) }, else => unreachable, }; } fn foldCmp( lhs: Value.Unwrapped, rhs: Value.Unwrapped, op: Story.Opcode, ) !Value.Unwrapped { switch (op) { .cmp_eq => return .{ .bool = std.meta.eql(lhs, rhs) }, .cmp_neq => return .{ .bool = !std.meta.eql(lhs, rhs) }, else => {}, } const lf = lhs.coerce().toFloat(); const rf = rhs.coerce().toFloat(); const result = switch (op) { .cmp_lt => lf < rf, .cmp_gt => lf > rf, .cmp_lte => lf <= rf, .cmp_gte => lf >= rf, else => unreachable, }; return .{ .bool = result }; } fn foldConstant( lhs: Value.Unwrapped, rhs: Value.Unwrapped, op: Story.Opcode, ) !Value.Unwrapped { switch (op) { .add, .sub, .mul, .div, .mod, => return foldArith(lhs, rhs, op), .cmp_eq, .cmp_neq, .cmp_lt, .cmp_gt, .cmp_lte, .cmp_gte, => return foldCmp(lhs, rhs, op), else => unreachable, } } fn irInt(sema: *Sema, inst: Ir.Inst.Index) InnerError!ValueInfo { const value = sema.ir.instructions[@intFromEnum(inst)].data.int; const ip_index = try sema.module.intern_pool.getOrPutInt(sema.gpa, value); return .{ .value = ip_index }; } fn irFloat(sema: *Sema, inst: Ir.Inst.Index) InnerError!ValueInfo { const value = sema.ir.instructions[@intFromEnum(inst)].data.float; const ip_index = try sema.module.intern_pool.getOrPutFloat(sema.gpa, value); return .{ .value = ip_index }; } fn irStr(sema: *Sema, inst: Ir.Inst.Index) InnerError!ValueInfo { const data = sema.ir.instructions[@intFromEnum(inst)].data.str; const ip_index = try sema.module.intern_pool.getOrPutStr(sema.gpa, data.start); return .{ .value = ip_index }; } fn irUnaryOp( sema: *Sema, builder: *Builder, inst: Ir.Inst.Index, op: Story.Opcode, ) InnerError!ValueInfo { const gpa = sema.gpa; const data = sema.ir.instructions[@intFromEnum(inst)].data.un; const ip = &sema.module.intern_pool; const lhs = sema.resolveInst(data.lhs); //const lhs_src: SrcLoc = .{ .src_offset = 0 }; //try sema.analyzeArithmeticArg(builder, lhs, lhs_src); if (sema.resolveValue(lhs)) |lhs_info| { switch (lhs_info.unwrap(ip)) { .bool => |boolean| { const new_value = switch (op) { .not => !boolean, .neg => return error.TypeError, else => unreachable, }; return .{ .value = ip.getOrPutBool(new_value) }; }, .int => |int| { const new_value: i64 = switch (op) { .not => if (int > 0) 0 else 1, .neg => -int, else => unreachable, }; return .{ .value = try ip.getOrPutInt(gpa, new_value) }; }, .float => |float| { const new_value: f64 = switch (op) { .not => if (float > 0.0) 0.0 else 1.0, .neg => -float, else => unreachable, }; return .{ .value = try ip.getOrPutFloat(gpa, new_value) }; }, } } try builder.ensureLoad(lhs); try builder.addByteOp(op); return .stack; } fn irBinaryOp( sema: *Sema, builder: *Builder, inst: Ir.Inst.Index, op: Story.Opcode, ) InnerError!ValueInfo { const gpa = sema.gpa; const ip = &sema.module.intern_pool; const data = sema.ir.instructions[@intFromEnum(inst)].data.bin; const lhs = sema.resolveInst(data.lhs); const rhs = sema.resolveInst(data.rhs); //const lhs_src: SrcLoc = .{ .src_offset = 0 }; //const rhs_src: SrcLoc = .{ .src_offset = 0 }; //try sema.analyzeArithmeticArg(builder, lhs, lhs_src); //try sema.analyzeArithmeticArg(builder, rhs, rhs_src); if (sema.resolveValue(lhs)) |lhs_value| { if (sema.resolveValue(rhs)) |rhs_value| { const lhs_coerced = lhs_value.unwrap(ip).coerce(); const rhs_coerced = rhs_value.unwrap(ip).coerce(); return switch (try foldConstant(lhs_coerced, rhs_coerced, op)) { .bool => |boolean| .{ .value = ip.getOrPutBool(boolean) }, .int => |int| .{ .value = try ip.getOrPutInt(gpa, int) }, .float => |float| .{ .value = try ip.getOrPutFloat(gpa, float) }, }; } } try builder.ensureLoad(lhs); try builder.ensureLoad(rhs); try builder.addByteOp(op); return .stack; } fn irLogicalOp( sema: *Sema, builder: *Builder, inst: Ir.Inst.Index, logical_or: bool, ) InnerError!ValueInfo { const ip = &sema.module.intern_pool; const data = sema.ir.instructions[@intFromEnum(inst)].data.bin; const lhs = sema.resolveInst(data.lhs); const rhs = sema.resolveInst(data.rhs); if (sema.resolveValue(lhs)) |lhs_info| { const lhs_value = lhs_info.unwrap(ip); if (sema.resolveValue(rhs)) |_| { return if (logical_or) if (lhs_value.isTruthy()) lhs else rhs else if (!lhs_value.isTruthy()) lhs else rhs; } if (logical_or and lhs_value.isTruthy()) return lhs; if (!logical_or and !lhs_value.isTruthy()) return lhs; try builder.ensureLoad(rhs); return .none; } const else_label = try builder.addLabel(); try builder.ensureLoad(lhs); try builder.addFixup(if (logical_or) .jmp_t else .jmp_f, else_label); try builder.addByteOp(.pop); try builder.ensureLoad(rhs); builder.setLabel(else_label); return .none; } fn irDeclRef( sema: *Sema, builder: *Builder, inst: Ir.Inst.Index, inline_block: bool, ) InnerError!ValueInfo { const data = sema.ir.instructions[@intFromEnum(inst)].data.str_tok; const ip_index = try sema.module.intern_pool.getOrPutStr(sema.gpa, data.start); const src_loc: SrcLoc = .{ .src_offset = data.src_offset }; const ident = try sema.lookupIdentifier(builder, ip_index, src_loc); if (inline_block) { switch (ident.tag) { .knot, .stitch, .function => unreachable, .var_const => return sema.resolveGlobalDecl(builder, ip_index, src_loc), .var_mut => return sema.fail( src_loc, "global variable assignments cannot refer to other variables", .{}, ), } } else { switch (ident.tag) { .knot => return .{ .knot = ip_index }, .stitch => return .{ .stitch = ip_index }, .function => return .{ .function = ip_index }, .var_mut => return .{ .variable = ip_index }, .var_const => return .{ .variable = ip_index }, } } } fn irAlloc(_: *Sema, builder: *Builder, _: Ir.Inst.Index) InnerError!ValueInfo { // TODO: Add constraints on how many temporaries we can have. // max(u8) or max(u16) are most likey appropriate. builder.code.locals_count += 1; return .{ .temp = builder.addStackSlot() }; } fn irStore(sema: *Sema, builder: *Builder, inst: Ir.Inst.Index) InnerError!void { const data = sema.ir.instructions[@intFromEnum(inst)].data.bin; const lhs = sema.resolveInst(data.lhs); const rhs = sema.resolveInst(data.rhs); const src: SrcLoc = .{ .src_offset = 0 }; try builder.ensureLoad(rhs); switch (lhs) { .none => unreachable, .stack => {}, .value => |_| return sema.fail(src, "could not assign to constant value", .{}), .variable => |index| { const local_index = try builder.getOrPutConstantIndex(index); try builder.addConstOp(.store_global, @intCast(local_index)); }, .temp => |temp| try builder.addConstOp(.store, @intCast(temp)), .knot => |_| return sema.fail(src, "could not assign to knot", .{}), .stitch => |_| return sema.fail(src, "could not assign to stitch", .{}), .function => |_| return sema.fail(src, "could not assign to function", .{}), } try builder.addByteOp(.pop); } fn irLoad(sema: *Sema, builder: *Builder, inst: Ir.Inst.Index) InnerError!ValueInfo { const data = sema.ir.instructions[@intFromEnum(inst)].data.un; const lhs = sema.resolveInst(data.lhs); if (lhs == .value) return lhs; try builder.ensureLoad(lhs); return .stack; } fn irCondBr(sema: *Sema, builder: *Builder, inst: Ir.Inst.Index) InnerError!ValueInfo { const data = sema.ir.instructions[@intFromEnum(inst)].data.payload; const extra = sema.ir.extraData(Ir.Inst.CondBr, data.extra_index); const then_body = sema.ir.bodySlice(extra.end, extra.data.then_body_len); const else_body = sema.ir.bodySlice(extra.end + then_body.len, extra.data.else_body_len); const else_label = try builder.addLabel(); const end_label = try builder.addLabel(); const condition = sema.resolveInst(extra.data.condition); if (condition != .none) try builder.ensureLoad(condition); try builder.addFixup(.jmp_f, else_label); try builder.addByteOp(.pop); _ = try analyzeBodyInner(sema, builder, then_body, false); try builder.addFixup(.jmp, end_label); builder.setLabel(else_label); try builder.addByteOp(.pop); _ = try analyzeBodyInner(sema, builder, else_body, false); builder.setLabel(end_label); return .none; } fn irBreak(sema: *Sema, inst: Ir.Inst.Index) InnerError!void { _ = sema; _ = inst; } fn irBreakInline(sema: *Sema, inst: Ir.Inst.Index) InnerError!ValueInfo { const data = sema.ir.instructions[@intFromEnum(inst)].data.bin; const rvalue_inst = sema.resolveInst(data.rhs); return rvalue_inst; } fn irBlock(sema: *Sema, builder: *Builder, inst: Ir.Inst.Index) InnerError!void { const data = sema.ir.instructions[@intFromEnum(inst)].data.payload; const extra = sema.ir.extraData(Ir.Inst.Block, data.extra_index); const body = sema.ir.bodySlice(extra.end, extra.data.body_len); _ = try analyzeBodyInner(sema, builder, body, false); } fn irSwitchBr(sema: *Sema, builder: *Builder, inst: Ir.Inst.Index) InnerError!void { const gpa = sema.gpa; const data = sema.ir.instructions[@intFromEnum(inst)].data.payload; const extra = sema.ir.extraData(Ir.Inst.SwitchBr, data.extra_index); const cases_slice = sema.ir.bodySlice(extra.end, extra.data.cases_len); var case_labels: std.ArrayListUnmanaged(usize) = .empty; try case_labels.ensureUnusedCapacity(gpa, cases_slice.len + 1); defer case_labels.deinit(gpa); const condition = sema.resolveInst(extra.data.operand); if (condition != .none) try builder.ensureLoad(condition); const exit_label = try builder.addLabel(); const cmp_var = builder.addStackSlot(); try builder.addConstOp(.store, cmp_var); for (cases_slice) |case_index| { const case_extra = sema.ir.extraData(Ir.Inst.SwitchBr.Case, @intFromEnum(case_index)); const case_expr = sema.resolveInst(case_extra.data.operand); const case_label_index = try builder.addLabel(); case_labels.appendAssumeCapacity(case_label_index); try builder.addConstOp(.load, @intCast(cmp_var)); try builder.ensureLoad(case_expr); try builder.addByteOp(.cmp_eq); try builder.addFixup(.jmp_t, case_label_index); try builder.addByteOp(.pop); } const else_label = try builder.addLabel(); try builder.addFixup(.jmp, else_label); for (cases_slice, case_labels.items) |case_index, label_index| { const case_extra = sema.ir.extraData(Ir.Inst.SwitchBr.Case, @intFromEnum(case_index)); const case_body = sema.ir.bodySlice(case_extra.end, case_extra.data.body_len); builder.setLabel(label_index); try builder.addByteOp(.pop); _ = try analyzeBodyInner(sema, builder, case_body, false); try builder.addFixup(.jmp, exit_label); } const else_body = sema.ir.bodySlice( extra.end + extra.data.cases_len, extra.data.else_body_len, ); builder.setLabel(else_label); _ = try analyzeBodyInner(sema, builder, else_body, false); builder.setLabel(exit_label); } fn irContentPush(sema: *Sema, builder: *Builder, inst: Ir.Inst.Index) InnerError!void { const data = sema.ir.instructions[@intFromEnum(inst)].data.un; const lhs = sema.resolveInst(data.lhs); if (lhs == .none) return error.AnalysisFail; if (lhs != .stack) try builder.ensureLoad(lhs); try builder.addByteOp(.stream_push); } fn irChoiceBr(sema: *Sema, builder: *Builder, inst: Ir.Inst.Index) InnerError!void { const gpa = sema.gpa; const data = sema.ir.instructions[@intFromEnum(inst)].data.payload; const choice_extra = sema.ir.extraData(Ir.Inst.ChoiceBr, data.extra_index); const options_slice = sema.ir.bodySlice(choice_extra.end, choice_extra.data.cases_len); var branch_labels: std.ArrayListUnmanaged(usize) = .empty; try branch_labels.ensureUnusedCapacity(gpa, options_slice.len + 1); defer branch_labels.deinit(gpa); for (options_slice) |option_index| { const case_extra = sema.ir.extraData(Ir.Inst.ChoiceBr.Case, @intFromEnum(option_index)); const lhs_slice = sema.ir.bodySlice( case_extra.end, case_extra.data.lhs_len, ); const mhs_slice = sema.ir.bodySlice( case_extra.end + case_extra.data.lhs_len, case_extra.data.mhs_len, ); const case_label = try builder.addLabel(); branch_labels.appendAssumeCapacity(case_label); try builder.addByteOp(.stream_mark); _ = try analyzeBodyInner(sema, builder, lhs_slice, false); _ = try analyzeBodyInner(sema, builder, mhs_slice, false); try builder.addFixupAbsolute(.br_push, case_label); } try builder.addByteOp(.br_table); try builder.addByteOp(.br_select_index); try builder.addByteOp(.br_dispatch); for (options_slice, branch_labels.items) |option_index, label| { const case_extra = sema.ir.extraData(Ir.Inst.ChoiceBr.Case, @intFromEnum(option_index)); const lhs_slice = sema.ir.bodySlice( case_extra.end, case_extra.data.lhs_len, ); const rhs_slice = sema.ir.bodySlice( case_extra.end + case_extra.data.lhs_len + case_extra.data.mhs_len, case_extra.data.rhs_len, ); const body_slice = sema.ir.bodySlice( case_extra.end + case_extra.data.lhs_len + case_extra.data.mhs_len + case_extra.data.rhs_len, case_extra.data.body_len, ); builder.setLabel(label); _ = try analyzeBodyInner(sema, builder, lhs_slice, false); _ = try analyzeBodyInner(sema, builder, rhs_slice, false); try builder.addByteOp(.stream_line); _ = try analyzeBodyInner(sema, builder, body_slice, false); } } fn irRet(sema: *Sema, builder: *Builder, inst: Ir.Inst.Index) InnerError!void { const data = sema.ir.instructions[@intFromEnum(inst)].data.un; if (data.lhs.toIndexAllowNone()) |index| { const value = sema.inst_map.get(index).?; if (value != .none) try builder.ensureLoad(value); } try builder.addByteOp(.ret); } fn irImplicitRet(_: *Sema, builder: *Builder, _: Ir.Inst.Index) InnerError!void { try builder.addByteOp(.exit); } fn irCall( sema: *Sema, builder: *Builder, inst: Ir.Inst.Index, comptime kind: enum { direct, field }, ) !ValueInfo { const ExtraType = switch (kind) { .direct => Ir.Inst.Call, .field => Ir.Inst.FieldCall, }; const data = sema.ir.instructions[@intFromEnum(inst)].data.payload; const extra = sema.ir.extraData(ExtraType, data.extra_index); const body = sema.ir.extra[extra.end..]; const callee_src: SrcLoc = .{ .src_offset = data.src_offset }; switch (kind) { .direct => { const callee = sema.resolveInst(extra.data.callee); _ = try analyzeCallTarget(sema, builder, callee_src, callee); }, .field => { const callee = sema.resolveInst(extra.data.obj_ptr); const target = try analyzeCallTarget(sema, builder, callee_src, callee); const ip_index = try sema.module.intern_pool.getOrPutStr( sema.gpa, extra.data.field_name_start, ); const e = try sema.lookupInNamespace(target.namespace.?, ip_index, callee_src); switch (e.tag) { .function => { const local_index = try builder.getOrPutConstantIndex(ip_index); _ = try builder.addConstOp(.load_attr, @intCast(local_index)); }, else => return sema.fail(callee_src, "invalid call target", .{}), } }, } const args_len = extra.data.args_len; var arg_start: u32 = args_len; var i: u32 = 0; while (i < args_len) : (i += 1) { const arg_end = sema.ir.extra[extra.end + i]; defer arg_start = arg_end; const arg_body = body[arg_start..arg_end]; const arg_value = try analyzeBodyInner(sema, builder, @ptrCast(arg_body), false); if (arg_value != .none) try builder.ensureLoad(arg_value); } try builder.addConstOp(.call, @intCast(args_len)); return .stack; } fn irDivert( sema: *Sema, builder: *Builder, inst: Ir.Inst.Index, comptime kind: enum { direct, field }, ) !void { const ExtraType = switch (kind) { .direct => Ir.Inst.Call, .field => Ir.Inst.FieldCall, }; const data = sema.ir.instructions[@intFromEnum(inst)].data.payload; const extra = sema.ir.extraData(ExtraType, data.extra_index); const body = sema.ir.extra[extra.end..]; const callee_src: SrcLoc = .{ .src_offset = data.src_offset }; switch (kind) { .direct => { const callee = sema.resolveInst(extra.data.callee); _ = try analyzeDivertTarget(sema, builder, callee_src, callee); }, .field => { const callee = sema.resolveInst(extra.data.obj_ptr); const target = try analyzeDivertTarget(sema, builder, callee_src, callee); const ip_index = try sema.module.intern_pool.getOrPutStr( sema.gpa, extra.data.field_name_start, ); const e = try sema.lookupInNamespace(target.namespace.?, ip_index, callee_src); switch (e.tag) { .knot => { const local_index = try builder.getOrPutConstantIndex(ip_index); _ = try builder.addConstOp(.load_attr, @intCast(local_index)); }, else => return sema.fail(callee_src, "invalid divert target", .{}), } }, } const args_len = extra.data.args_len; var arg_start: u32 = args_len; var i: u32 = 0; while (i < args_len) : (i += 1) { const arg_end = sema.ir.extra[extra.end + i]; defer arg_start = arg_end; const arg_body = body[arg_start..arg_end]; const arg_value = try analyzeBodyInner(sema, builder, @ptrCast(arg_body), false); if (arg_value != .none) try builder.ensureLoad(arg_value); } try builder.addConstOp(.divert, @intCast(args_len)); } fn irFieldPtr(_: *Sema, _: *Builder, _: Ir.Inst.Index) !ValueInfo { return .none; } // TODO: Check for duplicate parameters. fn irParam(_: *Sema, builder: *Builder, _: Ir.Inst.Index) !ValueInfo { // TODO: Add constraints on how many temporaries we can have. // max(u8) or max(u16) are most likey appropriate. return .{ .temp = builder.addParameter() }; } fn irDone(_: *Sema, builder: *Builder, _: Ir.Inst.Index) !ValueInfo { try builder.addByteOp(.done); return .none; } fn irExit(_: *Sema, builder: *Builder, _: Ir.Inst.Index) !ValueInfo { try builder.addByteOp(.exit); return .none; } fn analyzeArithmeticArg( sema: *Sema, builder: *Builder, arg: ValueInfo, arg_src: SrcLoc, ) !void { switch (arg) { .value => |index| { const local_index = try builder.getOrPutConstantIndex(index); try builder.addConstOp(.load_const, @intCast(local_index)); }, //.temporary => |index| { // try builder.addConstOp(.load, @intCast(index)); //}, //.variable => |index| { // const local_index = try builder.getOrPutConstantIndex(index); // try builder.addConstOp(.load_global, @intCast(local_index)); //}, .knot => return fail(sema, arg_src, "invalid operand to arithmetic expression", .{}), else => unreachable, } } fn analyzeCallTarget( sema: *Sema, builder: *Builder, src: SrcLoc, callee: ValueInfo, ) !Module.Namespace.Decl { switch (callee) { .function => |ip_index| { try builder.ensureLoad(callee); return sema.lookupIdentifier(builder, ip_index, src); }, else => return sema.fail(src, "invalid call target", .{}), } } fn analyzeDivertTarget( sema: *Sema, builder: *Builder, src: SrcLoc, callee: ValueInfo, ) !Module.Namespace.Decl { switch (callee) { .knot => |ip_index| { try builder.ensureLoad(callee); return sema.lookupIdentifier(builder, ip_index, src); }, else => return sema.fail(src, "invalid divert target", .{}), } } fn analyzeBodyInner( sema: *Sema, builder: *Builder, body: []const Ir.Inst.Index, inline_block: bool, ) InnerError!ValueInfo { var result: ValueInfo = .none; for (body) |inst| { const data = sema.ir.instructions[@intFromEnum(inst)]; result = switch (data.tag) { .file => unreachable, // never present inside block bodies .declaration => unreachable, // never present inside block bodies .decl_var => unreachable, // never present inside block bodies .decl_knot => unreachable, // never present inside block bodies .decl_stitch => unreachable, // never present inside block bodies .decl_function => unreachable, // never present inside block bodies .alloc => try irAlloc(sema, builder, inst), .store => { try irStore(sema, builder, inst); continue; }, .load => try irLoad(sema, builder, inst), .int => try irInt(sema, inst), .float => try irFloat(sema, inst), .str => try irStr(sema, inst), .add => try irBinaryOp(sema, builder, inst, .add), .sub => try irBinaryOp(sema, builder, inst, .sub), .mul => try irBinaryOp(sema, builder, inst, .mul), .div => try irBinaryOp(sema, builder, inst, .div), .mod => try irBinaryOp(sema, builder, inst, .mod), .neg => try irUnaryOp(sema, builder, inst, .neg), .not => try irUnaryOp(sema, builder, inst, .not), .bool_and => try irLogicalOp(sema, builder, inst, false), .bool_or => try irLogicalOp(sema, builder, inst, true), .cmp_eq => try irBinaryOp(sema, builder, inst, .cmp_eq), .cmp_neq => try irBinaryOp(sema, builder, inst, .cmp_neq), .cmp_lt => try irBinaryOp(sema, builder, inst, .cmp_lt), .cmp_lte => try irBinaryOp(sema, builder, inst, .cmp_lte), .cmp_gt => try irBinaryOp(sema, builder, inst, .cmp_gt), .cmp_gte => try irBinaryOp(sema, builder, inst, .cmp_gte), .decl_ref => try irDeclRef(sema, builder, inst, inline_block), .ret => { try irRet(sema, builder, inst); continue; }, .implicit_ret => { try irImplicitRet(sema, builder, inst); continue; }, .condbr => try irCondBr(sema, builder, inst), .@"break" => { try irBreak(sema, inst); continue; }, .break_inline => try irBreakInline(sema, inst), .block => { try irBlock(sema, builder, inst); continue; }, .content_push => { try irContentPush(sema, builder, inst); continue; }, .content_line => { try builder.addByteOp(.stream_line); continue; }, .content_glue => { try builder.addByteOp(.stream_glue); continue; }, .choice_br => { try irChoiceBr(sema, builder, inst); continue; }, .switch_br => { try irSwitchBr(sema, builder, inst); continue; }, .call => try irCall(sema, builder, inst, .direct), .field_call => try irCall(sema, builder, inst, .field), .divert => { try irDivert(sema, builder, inst, .direct); continue; }, .field_divert => { try irDivert(sema, builder, inst, .field); continue; }, .field_ptr => try irFieldPtr(sema, builder, inst), .param => try irParam(sema, builder, inst), .done => try irDone(sema, builder, inst), .exit => try irExit(sema, builder, inst), }; try sema.inst_map.put(sema.gpa, inst, result); } return result; } // TODO: No return allowed. pub fn analyzeStitch( sema: *Sema, builder: *Builder, inst: Ir.Inst.Index, ) !void { const data = sema.ir.instructions[@intFromEnum(inst)].data.payload; const extra = sema.ir.extraData(Ir.Inst.Stitch, data.extra_index); const body = sema.ir.bodySlice(extra.end, extra.data.body_len); _ = try analyzeBodyInner(sema, builder, body, false); } // TODO: No diverts allowed. pub fn analyzeFunction( sema: *Sema, builder: *Builder, inst: Ir.Inst.Index, ) !void { const data = sema.ir.instructions[@intFromEnum(inst)].data.payload; const extra = sema.ir.extraData(Ir.Inst.Function, data.extra_index); const body = sema.ir.bodySlice(extra.end, extra.data.body_len); _ = try analyzeBodyInner(sema, builder, body, false); } // TODO: No return allowed. pub fn analyzeKnot( sema: *Sema, builder: *Builder, inst: Ir.Inst.Index, ) !void { const data = sema.ir.instructions[@intFromEnum(inst)].data.payload; const extra = sema.ir.extraData(Ir.Inst.Knot, data.extra_index); const body = sema.ir.bodySlice(extra.end, extra.data.body_len); _ = try analyzeBodyInner(sema, builder, body, false); } fn analyzeNestedDecl( sema: *Sema, namespace: *Module.Namespace, inst: Ir.Inst.Index, ) !void { const data = sema.ir.instructions[@intFromEnum(inst)].data.payload; const extra = sema.ir.extraData(Ir.Inst.Declaration, data.extra_index).data; const decl = sema.ir.instructions[@intFromEnum(extra.value)]; const decl_name = try sema.module.intern_pool.getOrPutStr(sema.gpa, extra.name); switch (decl.tag) { .decl_stitch => { const child_namespace = try sema.module.createNamespace(namespace); try namespace.decls.put(sema.arena, decl_name, .{ .tag = .knot, .decl_inst = extra.value, .args_count = 0, .namespace = child_namespace, }); try sema.module.queueWorkItem(.{ .tag = .stitch, .decl_name = decl_name, .inst_index = extra.value, .namespace = child_namespace, }); }, else => unreachable, } } fn scanTopLevelDecl( sema: *Sema, namespace: *Module.Namespace, inst: Ir.Inst.Index, ) !void { const data = sema.ir.instructions[@intFromEnum(inst)].data.payload; const extra = sema.ir.extraData(Ir.Inst.Declaration, data.extra_index).data; const decl_inst = sema.ir.instructions[@intFromEnum(extra.value)]; const decl_name = try sema.module.intern_pool.getOrPutStr(sema.gpa, extra.name); const src_loc: SrcLoc = .{ .src_offset = data.src_offset }; switch (decl_inst.tag) { .decl_var => { const gop = try namespace.decls.getOrPut(sema.arena, decl_name); if (gop.found_existing) { return sema.fail(src_loc, "duplicate identifier", .{}); } else { gop.value_ptr.* = .{ .tag = if (extra.flags == 0x01) .var_const else .var_mut, .namespace = null, .decl_inst = extra.value, .args_count = 0, }; } }, .decl_knot => { const _data = sema.ir.instructions[@intFromEnum(extra.value)].data.payload; const _extra = sema.ir.extraData(Ir.Inst.Knot, _data.extra_index); const _body = sema.ir.bodySlice(_extra.end, _extra.data.body_len); const _stitches = sema.ir.bodySlice(_extra.end + _body.len, _extra.data.stitches_len); const child_namespace = try sema.module.createNamespace(namespace); const gop = try namespace.decls.getOrPut(sema.arena, decl_name); if (gop.found_existing) { return sema.fail(src_loc, "duplicate identifier", .{}); } else { gop.value_ptr.* = .{ .tag = .knot, .decl_inst = extra.value, .args_count = 0, .namespace = child_namespace, }; } try sema.module.queueWorkItem(.{ .tag = .knot, .decl_name = decl_name, .inst_index = extra.value, .namespace = child_namespace, }); for (_stitches) |st| { try analyzeNestedDecl(sema, child_namespace, st); } }, .decl_stitch => { const child_namespace = try sema.module.createNamespace(namespace); const gop = try namespace.decls.getOrPut(sema.arena, decl_name); if (gop.found_existing) { return sema.fail(src_loc, "duplicate identifier", .{}); } else { gop.value_ptr.* = .{ .tag = .stitch, .decl_inst = extra.value, .args_count = 0, .namespace = child_namespace, }; } try sema.module.queueWorkItem(.{ .tag = .stitch, .decl_name = decl_name, .inst_index = extra.value, .namespace = child_namespace, }); }, .decl_function => { const child_namespace = try sema.module.createNamespace(namespace); const gop = try namespace.decls.getOrPut(sema.arena, decl_name); if (gop.found_existing) { return sema.fail(src_loc, "duplicate identifier", .{}); } else { gop.value_ptr.* = .{ .tag = .function, .decl_inst = extra.value, .args_count = 0, .namespace = child_namespace, }; } try sema.module.queueWorkItem(.{ .tag = .function, .decl_name = decl_name, .inst_index = extra.value, .namespace = child_namespace, }); }, else => unreachable, } } fn resolveGlobalDecl( sema: *Sema, builder: *Builder, decl_name: InternPool.Index, src_loc: SrcLoc, ) InnerError!ValueInfo { const entry = builder.namespace.decls.getPtr(decl_name).?; switch (entry.resolution) { .unresolved => { const data = sema.ir.instructions[@intFromEnum(entry.decl_inst)].data.payload; const extra = sema.ir.extraData(Ir.Inst.Var, data.extra_index); const body = sema.ir.bodySlice(extra.end, extra.data.body_len); entry.resolution = .in_progress; const val = try sema.analyzeBodyInner(builder, body, true); entry.resolution = .{ .resolved = val }; return val; }, .in_progress => return sema.fail( src_loc, "cycle detected in constant initializer", .{}, ), .resolved => |val| return val, } } pub fn scanTopLevelDecls( sema: *Sema, namespace: *Module.Namespace, decls: []const Ir.Inst.Index, ) !void { const gpa = sema.gpa; for (decls) |decl_index| { try sema.scanTopLevelDecl(namespace, decl_index); } var builder: Builder = .{ .sema = sema, .code = undefined, .namespace = namespace, }; defer builder.deinit(gpa); var iter = namespace.decls.iterator(); while (iter.next()) |entry| { const key = entry.key_ptr.*; const value = entry.value_ptr.*; switch (value.tag) { .var_mut, .var_const => { // TODO: Set a proper source offset for this. const src_loc: SrcLoc = .{ .src_offset = 0 }; const result = try sema.resolveGlobalDecl(&builder, key, src_loc); const initial_value = sema.resolveValue(result).?; try sema.module.globals.append(gpa, .{ .key = key, .value = initial_value.toInterned(), }); }, else => {}, } } }