From 4c3e0c2241261637620ce10461adfdfd5558e422 Mon Sep 17 00:00:00 2001 From: dave caruso Date: Mon, 16 Sep 2024 03:06:29 -0400 Subject: [PATCH] finish implementing tuples --- src/cli_compiler.zig | 1 + src/core/Air.Dump.zig | 24 ++- src/core/Air.zig | 30 ++- src/core/InternPool.zig | 37 +++- src/core/Sema.zig | 414 +++++++++++++++++++++++++++++++++------- src/core/Teg.zig | 5 +- src/core/Tokenizer.zig | 6 +- src/core/Transform.zig | 8 +- src/core/js/Link.zig | 13 +- src/core/js/NodeGen.zig | 44 ++++- src/core/stack.zig | 37 +++- test/behavior/tuple | 9 +- 12 files changed, 524 insertions(+), 104 deletions(-) diff --git a/src/cli_compiler.zig b/src/cli_compiler.zig index 9f5fbd66dc895351e25461f0fe33bf29e7506942..4e1a6a189acbea0aa9b2d29bd0f9e23f5845872e 100644 --- a/src/cli_compiler.zig +++ b/src/cli_compiler.zig @@ -44,6 +44,7 @@ pub fn main() !u8 { // Command line argument parsing. const program_name = "zig build compiler --"; _, const args = try Cli.programArgsAlloc(alloc, program_name); + defer Cli.programArgsFree(alloc, args); const opts = arg_config.parse(program_name, args); const file_path = try std.fs.path.resolve(alloc, &.{ diff --git a/src/core/Air.Dump.zig b/src/core/Air.Dump.zig index 4afd6e208c74fd17600da1a282a7b792a5688253..03f29ece0026eeec8b67714a9531b6b2ed612b33 100644 --- a/src/core/Air.Dump.zig +++ b/src/core/Air.Dump.zig @@ -130,6 +130,16 @@ pub const State = struct { try tty.setColor(w, .reset); try w.writeAll(")"); }, + .field_tuple => { + const field: Air.FieldStruct = air.instData(.field_tuple, inst); + try w.writeAll("("); + try s.dumpInstRef(w, field.base); + try w.writeAll(", "); + try tty.setColor(w, .magenta); + try w.print("{d}", .{field.field_index}); + try tty.setColor(w, .reset); + try w.writeAll(")"); + }, inline .call, .js_new => |t| { const call: Air.Call = air.instData(t, inst); const call_data = call.call_data.get(air); @@ -182,6 +192,15 @@ pub const State = struct { try s.dumpRefList(w, array_init.list.get(air).slice(air), '[', ']'); try w.writeAll(")"); }, + .tuple_init => { + // const array_init: Air.ArrayInit = air.instData(.array_init, inst); + // const array_type = array_init.ty.toArrayType(s.ip).?; + + // try w.writeAll("("); + // try s.dumpValue(w, array_type.item_type.value()); + // try s.dumpRefList(w, array_init.list.get(air).slice(air), '[', ']'); + // try w.writeAll(")"); + }, } } @@ -233,7 +252,10 @@ pub const State = struct { try tty.setColor(w, .blue); try w.writeAll(") -> "); try tty.setColor(w, .reset); - try s.dumpValue(w, ty.return_type.value()); + if (ty.return_type) |return_type| + try s.dumpValue(w, return_type.value()) + else + try w.writeAll("[inferred]"); }, inline .float, .int32, .comptime_float => |data, tag| { try tty.setColor(w, .bright_black); diff --git a/src/core/Air.zig b/src/core/Air.zig index 53e7c894ba144b612322ae306eb80c2d99fd608d..35f72e7cbbeb7e7802307d9935ba5488259f9c48 100644 --- a/src/core/Air.zig +++ b/src/core/Air.zig @@ -25,9 +25,11 @@ pub const Inst = struct { struct_init, array_init, + tuple_init, field_opaque, field_struct, + field_tuple, field_array, array_len, @@ -58,7 +60,8 @@ pub const Inst = struct { .store => Store, .struct_init => StructInit, .array_init => ArrayInit, - .field_struct => FieldStruct, + .tuple_init => TupleInit, + .field_struct, .field_tuple => FieldStruct, .field_opaque, .field_array, .add, @@ -286,6 +289,18 @@ pub const ArrayInit = struct { list: ExtraIndex(RefList), }; +pub const TupleInit = struct { + ty: Type, + field_values: u32, + + pub fn valuesPtr( + init: *const TupleInit, + air: *const Air, + ) [*]const Air.Inst.Ref { + return @ptrCast(air.extra.list.items[init.field_values..].ptr); + } +}; + pub const FieldStruct = struct { base: Inst.Index, field_index: u32, @@ -342,6 +357,7 @@ pub fn typeOfIndex(air: *const Air, index: Inst.Index, ip: *const InternPool) In .@"if" => air.instData(.@"if", index).data.get(air).union_ty, .struct_init => air.instData(.struct_init, index).ty, .array_init => air.instData(.array_init, index).ty, + .tuple_init => air.instData(.tuple_init, index).ty, .arg => air.instData(.arg, index).type, // A handful of binary operators are lowered to a form where both sides @@ -372,7 +388,9 @@ pub fn typeOfIndex(air: *const Air, index: Inst.Index, ip: *const InternPool) In .call => { const base_type = air.typeOf(air.instData(.call, index).base, ip); if (base_type == .@"opaque") return .@"opaque"; - return base_type.toFunctionPrototype(ip).?.return_type; + return base_type.toFunctionPrototype(ip).?.return_type orelse + // unreachable // return type was not resolved + @panic("TODO: resolve return type"); }, // Field accesses on structs are special-cased by storing the field @@ -391,6 +409,14 @@ pub fn typeOfIndex(air: *const Air, index: Inst.Index, ip: *const InternPool) In .toArrayType(ip) orelse unreachable; // field_array demands array type return base_type.item_type; }, + + // Tuple accesses use the tuple they are accessing into. + .field_tuple => { + const inst = air.instData(.field_tuple, index); + const base_type = air.typeOfIndex(inst.base, ip) + .toTupleType(ip) orelse unreachable; // field_tuple demands tuple type + return base_type.items[inst.field_index]; + }, }; } diff --git a/src/core/InternPool.zig b/src/core/InternPool.zig index 28e33988dbaa1134e7149166be34c4438560573b..a52dd2e72de5d1981a818216355cad6d8bc8846b 100644 --- a/src/core/InternPool.zig +++ b/src/core/InternPool.zig @@ -158,7 +158,7 @@ pub const Tag = enum(u8) { => unreachable, // does not have a payload .float, .comptime_float => Float, .func => FuncDecl, - .function_type => TypeFunction, + .function_type => FunctionType, .string => String.WithLen, .struct_value => StructValue, .struct_type => StructType, @@ -180,9 +180,9 @@ pub const Tag = enum(u8) { /// Trailing: /// 0. param_type: Index to param type. Repeated .params_len - pub const TypeFunction = struct { + pub const FunctionType = struct { params_len: u32, - return_type: Type, + return_type: Type.Optional, }; /// Trailing: @@ -578,7 +578,10 @@ pub const Type = enum(u32) { try writer.print("{}", .{param.displayName(ip)}); } try writer.writeAll(") -> "); - try writer.print("{}", .{function_type.return_type.displayName(ip)}); + if (function_type.return_type) |return_type| + try writer.print("{}", .{return_type.displayName(ip)}) + else + try writer.writeAll("[inferred]"); }, .struct_type => { try writer.writeAll("[struct]"); @@ -598,6 +601,19 @@ pub const Type = enum(u32) { }, } } + + pub const Optional = enum(u32) { + none = std.math.maxInt(u32), + _, + + pub fn init(opt: ?Type) Optional { + return if (opt) |o| @enumFromInt(@intFromEnum(o)) else .none; + } + + pub fn unwrap(opt: Optional) ?Type { + return if (opt != .none) @enumFromInt(@intFromEnum(opt)) else null; + } + }; }; pub const Item = struct { @@ -956,7 +972,10 @@ pub const Key = union(enum) { pub const FunctionType = struct { param_types: []Type, - return_type: Type, + /// If a return type is missing, it is inferred and not resolved yet. + /// When analysis figures out a return type, a new `FunctionType` with + /// the resolved return type. + return_type: ?Type, pub fn hash(self: FunctionType, hasher: *Hash) void { const t = tracer.trace(@src()); @@ -1491,9 +1510,9 @@ pub fn get(ip: *InternPool, gpa: Allocator, key: Key) !Value { .function_type => |f| brk: { for (f.param_types) |ty| ty.assertValid(ip); - const extra, var trail = try ip.extra.appendTrailing(gpa, Tag.TypeFunction, .{ + const extra, var trail = try ip.extra.appendTrailing(gpa, Tag.FunctionType, .{ .params_len = @intCast(f.param_types.len), - .return_type = f.return_type, + .return_type = .init(f.return_type), }, f.param_types.len); defer trail.end(); trail.writeSlice(f.param_types); @@ -1566,12 +1585,12 @@ pub fn get(ip: *InternPool, gpa: Allocator, key: Key) !Value { fn extraTypeFunction( ip: *const InternPool, - extra_index: ExtraIndex(Tag.TypeFunction), + extra_index: ExtraIndex(Tag.FunctionType), ) Key.FunctionType { const function_type, var trail = extra_index.getTrail(ip); const param_types = trail.readTypeSlice(function_type.params_len); const result: Key.FunctionType = .{ - .return_type = function_type.return_type, + .return_type = function_type.return_type.unwrap(), .param_types = param_types.get(ip), }; return result; diff --git a/src/core/Sema.zig b/src/core/Sema.zig index a3bade6fede8c25c716f12b5b9414de64c8a4117..8a578ffdd2a2d1cb68474ac078a1d4f9081e0c7b 100644 --- a/src/core/Sema.zig +++ b/src/core/Sema.zig @@ -1,10 +1,13 @@ -//! Semantic Analysis +//! Semantic Analysis converts `Teg` //! -//! Designed as a nonrecursive pause/resumable state machine, Sema converts -//! declarations to Intern Pool `Value`s and function bodies to `Air`. +//! Designed as a nonrecursive state machine which can be paused when an +//! unfinished dependency is encountered, allowing the thread to work +//! on other analysis jobs. //! -//! - Compile time resolution of values -//! - Type checking +//! Sema's responsibilities include +//! - Operation validation and reporting of most compile errors. +//! - Evaluation of `comptime` code. +//! - Declaring dependencies on other analysis units. comp: *Comp, ip: *InternPool, @@ -175,6 +178,7 @@ const Frame = struct { undefined: void, conditional: Conditional, + func: Func, }; const Conditional = enum { @@ -183,7 +187,16 @@ const Frame = struct { runtime_no, }; + const Func = union(enum) { + return_type, + argument: u16, + }; + fn initFromBlock(s: *Sema, block_index: Teg.Inst.Index, mode: AnalysisMode, data: Data) Frame { + switch (s.teg.instTag(block_index)) { + else => unreachable, // not a block + .block, .@"comptime" => {}, + } const list = s.teg.instEncodedData(block_index) .decodeUnsafe(Teg.PlNode(Teg.IndexList), s.teg.instTag(block_index)) .extra_index.get(&s.teg) @@ -476,7 +489,8 @@ const InstructionKind = enum { .@"comptime", .@"if", => .control_flow, - // these pop frames, which may end the compilation + // break is used to pop a frame, which has the + // possibility to end analysis .@"break", => .pop_control_flow, // these are statements which do not evaluate to anything (void) @@ -501,6 +515,12 @@ const PopControlFlowState = enum { stop, @"continue" }; fn stepInner(s: *Sema, comptime tag: Teg.Tag) Error!InstructionKind.fromTag(tag).Result() { switch (tag) { + .block => unreachable, // frame must already be pushed + .declaration, + .variable_declaration, + .test_declaration, + => unreachable, // declarations are not analzed by Sema + // TODO: pre-evaluate `.param` nodes, since they are always at the start of a function. // this way we can avoid repeating the non-trivial call into InternPool .param => { @@ -666,6 +686,8 @@ fn stepInner(s: *Sema, comptime tag: Teg.Tag) Error!InstructionKind.fromTag(tag) return try s.fieldAccessArray(lhs, lhs_type, field.rhs); } else if (lhs_type.toStructType(s.ip)) |struct_type| { return try s.fieldAccessStruct(lhs, lhs_type, struct_type, field.rhs); + } else if (lhs_type.toTupleType(s.ip)) |tuple_type| { + return try s.fieldAccessTuple(lhs, lhs_type, tuple_type, field.rhs); } else if (lhs.value()) |value| { if (value.valueTag(s.ip) == .external_value) { const rhs = try s.getCoercedValue(s.lazyNode(.rhs), field.rhs, .string); @@ -877,8 +899,6 @@ fn stepInner(s: *Sema, comptime tag: Teg.Tag) Error!InstructionKind.fromTag(tag) // return Value.void.toAir(); // }, - // control flow - .block => unreachable, // frame should already be pushed .@"comptime" => { const index = s.frame.currentIndex(); try s.pushFrame(); @@ -892,50 +912,66 @@ fn stepInner(s: *Sema, comptime tag: Teg.Tag) Error!InstructionKind.fromTag(tag) .{ .params = Type, .captures = Air.Inst.Ref }, .{ data.params.len, data.captures.len }, ); - defer reserve.deinit(); - if (data.captures.len > 0) @panic("captures"); + if (data.captures.len > 0) @panic("TODO: captures"); + + var handled_reserve: std.debug.SafetyLock = .{}; + handled_reserve.lock(); + defer handled_reserve.assertUnlocked(); + errdefer handled_reserve.unlock(); for (data.params.slice(&s.teg), 0..) |param, i| { const src = s.lazyNode(.argument(@intCast(i))); - const ty = try s.getType(src, param.unwrap() orelse @panic("TODO: inferred param sets")); - if (!ty.hasPossibleRepresentation(s.ip)) { - return s.fail(.param_comptime_only, .{ - .src = src, - .type = ty, - }); + + const known = param.unwrap() orelse @panic("TODO: inferred param sets"); + switch (known.decode()) { + .value => |val| { + const ty = (try s.coerceValue(src, val, .type)).toTypeUnsafe(s.ip); + if (!ty.hasPossibleRepresentation(s.ip)) { + return s.fail(.param_comptime_only, .{ + .src = src, + .type = ty, + }); + } + reserve.items(.params)[i] = ty; + }, + .index => |block| { + const mode = s.frame.mode; + try s.pushFrame(); + s.frame = Frame.initFromBlock(s, block, mode, .{ .func = .{ .argument = @intCast(i) } }); + s.frame.mark_index = reserve.index(); + handled_reserve.unlock(); + return; + }, } - reserve.items(.params)[i] = ty; } - const return_type = try s.getType( - s.lazyNode(.return_type), - data.return_type.unwrap() orelse .void, - ); + const return_type = ret: { + switch ((data.return_type.unwrap() orelse break :ret null).decode()) { + .value => |val| { + const ty = (try s.coerceValue(s.lazyNode(.return_type), val, .type)).toTypeUnsafe(s.ip); + if (!ty.hasPossibleRepresentation(s.ip)) { + return s.fail(.runtime_incompatible_type, .{ + .src = s.lazyNode(.return_type), + .type = ty, + }); + } + break :ret ty; + }, + .index => |block| { + const mode = s.frame.mode; + try s.pushFrame(); + s.frame = Frame.initFromBlock(s, block, mode, .{ .func = .return_type }); + s.frame.mark_index = reserve.index(); + handled_reserve.unlock(); + return; + }, + } + }; - const tracked_inst = try s.ip.trackInst(s.gpa, s.file, s.frame.currentIndex()); - const func_type = try s.makeType(.{ - .function_type = .{ - .param_types = reserve.items(.params), - .return_type = return_type, - }, - }); - const func_value = try s.makeValue(.{ - .func = .{ - .ty = func_type, - .owner_decl = s.techDebtDeclIndex(), // TODO: ooh, this is a fun one! - .teg_body_inst = tracked_inst, - }, - }); - - s.inst_map.add(s.frame.currentIndex(), func_value.toAir()); - - s.frame.advance(); - - // TODO: move this queue once we perform one of these - // - depend on the return type of an inferred-return-type function - // - use the value in any runtime context () - try s.comp.jobs.writeItem(.{ .analysis = .{ .au = .wrap(.{ .func = func_value }) } }); + try s.postVisitFunctionPrototype(reserve, return_type); + handled_reserve.unlock(); }, + .@"break" => { const brk: Teg.Break = s.frame.currentPayload(.@"break"); const operand = s.getRef(brk.operand, .marked); @@ -978,12 +1014,14 @@ fn stepInner(s: *Sema, comptime tag: Teg.Tag) Error!InstructionKind.fromTag(tag) switch (data.conditional) { .comptime_condition => { + assert(operand.kind == .value); // must be comptime s.inst_map.add(s.frame.currentIndex(), operand); s.frame.advance(); return .@"continue"; }, .runtime_yes => { try s.emitRuntimeBlock(if_data.yes.decode().index, operand, mark); + mark.deinit(); switch (if_data.no.decode()) { .value => { @@ -1025,6 +1063,79 @@ fn stepInner(s: *Sema, comptime tag: Teg.Tag) Error!InstructionKind.fromTag(tag) })); s.frame.advance(); }, + .func => { + const func_data: Teg.Func = s.frame.currentPayload(.func); + + const reserve: FunctionReserve = .fromIndex( + &s.scratch, + mark.index, + .{ func_data.params.len, func_data.captures.len }, + ); + + const return_type = switch (data.func) { + .argument => |arg_num| ret: { + for ( + func_data.params.slice(&s.teg)[arg_num + 1 ..], + reserve.items(.params)[arg_num + 1 ..], + arg_num + 1.., + ) |param, *dst, i| { + const known = param.unwrap() orelse @panic("TODO: inferred param sets"); + switch (known.decode()) { + .value => |val| { + const ty = (try s.coerceValue(s.lazyNode(.argument(@intCast(i))), val, .type)).toTypeUnsafe(s.ip); + if (!ty.hasPossibleRepresentation(s.ip)) { + return s.fail(.param_comptime_only, .{ + .src = s.lazyNode(.argument(@intCast(i))), + .type = ty, + }); + } + dst.* = ty; + }, + .index => |block| { + const mode = s.frame.mode; + try s.pushFrame(); + s.frame = Frame.initFromBlock(s, block, mode, .{ .func = .{ .argument = @intCast(i) } }); + return .@"continue"; + }, + } + } + + switch ((func_data.return_type.unwrap() orelse break :ret null).decode()) { + .value => |val| { + const ty = (try s.coerceValue(s.lazyNode(.return_type), val, .type)).toTypeUnsafe(s.ip); + if (!ty.hasPossibleRepresentation(s.ip)) { + return s.fail(.runtime_incompatible_type, .{ + .src = s.lazyNode(.return_type), + .type = ty, + }); + } + break :ret ty; + }, + .index => |block| { + const mode = s.frame.mode; + try s.pushFrame(); + s.frame = Frame.initFromBlock(s, block, mode, .{ .func = .return_type }); + s.frame.mark_index = reserve.index(); + return .@"continue"; + }, + } + }, + .return_type => ret: { + const val = operand.value() orelse unreachable; // must be comptime + const ty = (try s.coerceValue(s.lazyNode(.return_type), val, .type)).toTypeUnsafe(s.ip); + if (!ty.hasPossibleRepresentation(s.ip)) { + return s.fail(.runtime_incompatible_type, .{ + .src = s.lazyNode(.return_type), + .type = ty, + }); + } + break :ret ty; + }, + }; + + try s.postVisitFunctionPrototype(reserve, return_type); + return .@"continue"; + }, else => |t| std.debug.panic("TODO: post-visit .{s}", .{@tagName(t)}), } @@ -1141,7 +1252,6 @@ fn stepInner(s: *Sema, comptime tag: Teg.Tag) Error!InstructionKind.fromTag(tag) var has_errors = false; var is_constant = true; - // Allocate scratch space to store the values as they are being prepared. const scratch = try s.scratch.reserve( s.arena(), Air.Inst.Ref.Optional, @@ -1150,11 +1260,10 @@ fn stepInner(s: *Sema, comptime tag: Teg.Tag) Error!InstructionKind.fromTag(tag) defer scratch.deinit(); @memset(scratch.slice(), .none); + // The ordering of `storage` is defined by the struct's type, which + // may be different than the instantiation. The fields are visited in + // order of the instantiation. for (node.keys(&s.teg), node.inits(&s.teg), 0..) |init_key, init_value, i| { - // The order of `storage` is as ordered by the type, but this - // can be in a different ordering than the initialization. We - // guarantee the ordering the initializers are visited is in the - // order they are declared. const found_index = index: for (struct_type.field_keys, 0..) |decl_key, j| { if (decl_key.eqlSlice(init_key.slice(s.teg.string_bytes), s.ip)) break :index j; @@ -1181,7 +1290,7 @@ fn stepInner(s: *Sema, comptime tag: Teg.Tag) Error!InstructionKind.fromTag(tag) .marked, ) catch |e| switch (e) { error.AnalysisFail => { - // Instead of bailing out with `try`, we will continue the loop. + // Instead of bailing out with `try`, all fields will be analyzed has_errors = true; // Mark something non-null so that duplicate field errors are emitted. scratch.at(found_index).* = comptime Value.void.toAir().toOptional(); @@ -1193,29 +1302,29 @@ fn stepInner(s: *Sema, comptime tag: Teg.Tag) Error!InstructionKind.fromTag(tag) scratch.at(found_index).* = coerced.toOptional(); } - // Check for missing fields - { - const storage = scratch.slice(); - var i: usize = 0; - while (mem.indexOfScalarPos(Air.Inst.Ref.Optional, storage, i, .none)) |index| { - try s.addError(.struct_init_missing_field, .{ - .ty = ty, - .field = @intCast(index), - .src = s.lazyNode(.main_node), - }); - i = index + 1; - } - if (i != 0) has_errors = true; + const storage = scratch.slice(); + var i: usize = 0; + while (mem.indexOfScalarPos(Air.Inst.Ref.Optional, storage, i, .none)) |index| { + try s.addError(.struct_init_missing_field, .{ + .ty = ty, + .field = @intCast(index), + .src = s.lazyNode(.main_node), + }); + i = index + 1; } + if (i != 0) has_errors = true; if (has_errors) return Error.AnalysisFail; + // Now that there are definitely no missing fields, it is safe to + // interpret `scratch` as `Air.Inst.Ref` + if (is_constant) { - // Convert all items in storage into a Value type const result_storage = scratch.sliceReinterpret(Value); - for (scratch.slice(), scratch.sliceReinterpret(Value)) |ref, *value| { - value.* = ref.unwrap().?.decode().value; + for (scratch.slice(), result_storage) |ref, *value| { + // `ref` and `value` alias; this coercion is performed in-place + value.* = (ref.unwrap() orelse unreachable).decode().value; } return try s.makeValueAir(.{ .struct_value = .{ .ty = ty, @@ -1224,7 +1333,6 @@ fn stepInner(s: *Sema, comptime tag: Teg.Tag) Error!InstructionKind.fromTag(tag) } else { try s.ensureAtRuntime(s.lazyNode(.main_node)); - // Create a runtime struct instantiation const value_index = try s.func.?.extra.appendSlice(s.gpa, scratch.slice()); return try s.addRef(.struct_init, .{ .ty = ty, @@ -1239,6 +1347,73 @@ fn stepInner(s: *Sema, comptime tag: Teg.Tag) Error!InstructionKind.fromTag(tag) return s.makeValueAir(.{ .array_type = .{ .item_type = operand } }); }, + .tuple_type => { + const data: Teg.TupleType = s.frame.currentPayload(.tuple_type); + + const scratch = try s.scratch.reserve(s.arena(), Type, data.items.len); + defer scratch.deinit(); + + for (data.items.slice(&s.teg), scratch.slice(), 0..) |src, *dst, i| { + dst.* = try s.getType(s.lazyNode(.argument(@intCast(i))), src); + } + + return s.makeValueAir(.{ .tuple_type = .{ .items = scratch.slice() } }); + }, + + .tuple_init => { + const data: Teg.TupleInit = s.frame.currentPayload(.tuple_init); + + _ = data.type_hint; // TODO: use type hint + + const scratch = try s.scratch.reserve(s.arena(), Air.Inst.Ref, data.items.len); + defer scratch.deinit(); + + var is_const = true; + for (data.items.slice(&s.teg), scratch.slice()) |src, *dst| { + const result = s.getRef(src, .marked); + dst.* = result; + if (result.kind == .index) is_const = false; + } + + if (!is_const) { + try s.ensureAtRuntime(s.lazyNode(.main_node)); + + for (scratch.slice(), 0..) |*ref, i| { + ref.* = try s.ensureValueRuntime(s.lazyNode(.argument(@intCast(i))), ref.*); + } + } + + const ty = ty: { + const type_scratch = try s.scratch.reserve(s.arena(), Type, data.items.len); + defer type_scratch.deinit(); + + for (scratch.slice(), type_scratch.slice()) |val, *ty| { + ty.* = s.typeOfRef(val); + } + + break :ty try s.makeType(.{ .tuple_type = .{ .items = type_scratch.slice() } }); + }; + + if (is_const) { + const result_storage = scratch.sliceReinterpret(Value); + for (scratch.slice(), result_storage) |ref, *value| { + // `ref` and `value` alias; this coercion is performed in-place + value.* = ref.decode().value; + } + + return try s.makeValueAir(.{ .tuple_value = .{ + .items = result_storage, + .type = ty, + } }); + } else { + const value_index = try s.func.?.extra.appendSlice(s.gpa, scratch.slice()); + return try s.addRef(.struct_init, .{ + .field_values = value_index, + .ty = ty, + }); + } + }, + .result_type_argument => { const result: Teg.ResultTypeArgument = s.frame.currentData(.result_type_argument); @@ -1605,6 +1780,11 @@ fn coerceValue(s: *Sema, src: LazyNode, value: Value, to: Type) Error!Value { return s.coerceArrayValue(src, value, to_array_type.item_type); } } + if (to.toTupleType(s.ip)) |to_tuple_type| { + if (value.valueTag(s.ip) == .tuple_value) { + return s.coerceTupleValue(src, value, from, to, to_tuple_type); + } + } }, } return s.coercionError(src, from, to); @@ -1729,6 +1909,44 @@ fn coerceArrayValue(s: *Sema, src: LazyNode, value: Value, item_type: Type) !Val } }); } +fn coerceTupleValue( + s: *Sema, + src: LazyNode, + value: Value, + value_type: Type, + target_type: Type, + target_key: InternPool.Key.TupleType, +) !Value { + const tuple_value = value.toKey(s.ip).tuple_value; + if (tuple_value.items.len != target_key.items.len) + return s.coercionError(src, value_type, target_type); + + const len = tuple_value.items.len; + + const scratch = try s.scratch.reserveMulti( + s.arena(), + .{ .values = Value, .types = Type }, + .{ @intCast(len), @intCast(len) }, + ); + defer scratch.deinit(); + + // This function will append new items to the intern pool, invalidating + // existing ones. Copy all data into a stable slice first. + @memcpy(scratch.items(.values), tuple_value.items); + @memcpy(scratch.items(.types), target_key.items); + + for (0..len) |i| { + // `s.scratch` can be invalidated by having a tuple of tuples and coercing it. + const coerced = try s.coerceValue(src, scratch.at(.values, i).*, scratch.at(.types, i).*); + scratch.at(.values, i).* = coerced; + } + + return try s.makeValue(.{ .tuple_value = .{ + .type = target_type, + .items = scratch.items(.values), + } }); +} + fn lookupDecl(s: *Sema, name: NullTerminatedString) InternPool.DeclIndex { var nsi = s.ns_index; while (true) { @@ -1862,6 +2080,68 @@ fn fieldAccessArray(s: *Sema, lhs: Air.Inst.Ref, lhs_type: Type, rhs_teg: Teg.In } } +fn fieldAccessTuple( + s: *Sema, + lhs: Air.Inst.Ref, + lhs_type: Type, + tuple_type: InternPool.Key.TupleType, + field: Teg.Inst.Ref, +) !Air.Inst.Ref { + const index_value = try s.getCoercedValue(s.lazyNode(.rhs), field, .int32); + const index_signed = index_value.toInt32(s.ip) orelse unreachable; + if (index_signed < 0 or index_signed >= tuple_type.items.len) { + return s.fail(.field_lookup_failed, .{ + .ty = lhs_type, + .src = s.lazyNode(.lhs), + .field = index_value, + }); + } + const index: u32 = @intCast(index_signed); + + switch (lhs.decode()) { + .value => |value| { + return value.toKey(s.ip).tuple_value.items[index].toAir(); + }, + .index => |lhs_index| { + try s.ensureAtRuntime(s.lazyNode(.lhs)); + return s.addRef(.field_tuple, .{ + .base = lhs_index, + .field_index = index, + }); + }, + } +} + +const FunctionReserve = core.U32Stack.ReserveMulti(.{ .params = Type, .captures = Air.Inst.Ref }); + +fn postVisitFunctionPrototype(s: *Sema, reserve: FunctionReserve, return_type: ?Type) !void { + const tracked_inst = try s.ip.trackInst(s.gpa, s.file, s.frame.currentIndex()); + const func_type = try s.makeType(.{ + .function_type = .{ + .param_types = reserve.items(.params), + .return_type = return_type, + }, + }); + const func_value = try s.makeValue(.{ + .func = .{ + .ty = func_type, + .owner_decl = s.techDebtDeclIndex(), // TODO: ooh, this is a fun one! + .teg_body_inst = tracked_inst, + }, + }); + + s.inst_map.add(s.frame.currentIndex(), func_value.toAir()); + + s.frame.advance(); + + reserve.deinit(); + + // TODO: move this queue once we perform one of these + // - depend on the return type of an inferred-return-type function + // - use the value in any runtime context () + try s.comp.jobs.writeItem(.{ .analysis = .{ .au = .wrap(.{ .func = func_value }) } }); +} + // fn tempAir(s: *const Sema) Air { diff --git a/src/core/Teg.zig b/src/core/Teg.zig index f54ec2cf2e3d2b5fd86f38ac2cae4811d56e3508..d13225a790ad79472320df30366cc39e8ad4f597 100644 --- a/src/core/Teg.zig +++ b/src/core/Teg.zig @@ -126,6 +126,7 @@ pub const Tag = enum(u8) { .variable_declaration, .test_declaration, => PlNode(Decl), + .coercion => PlNode(Coercion), .@"break" => PlNode(Break), .number => PlNode(Float), @@ -151,7 +152,7 @@ pub const Tag = enum(u8) { .result_type_tuple_field => ResultTypeTupleField, .result_type_array_value => ResultTypeArrayValue, - // Single instruction operand + // One operand instructions .memory, .discard, .ensure_void, @@ -167,7 +168,7 @@ pub const Tag = enum(u8) { .assert, => RefNode, - // Two instruction operands + // Two operand instructions .field, .index, .add, diff --git a/src/core/Tokenizer.zig b/src/core/Tokenizer.zig index 6ddb2db422c8e5e09ee001c738797336d0539233..f14b847ebe7fa4f7f511f737fc50f4468295aed4 100644 --- a/src/core/Tokenizer.zig +++ b/src/core/Tokenizer.zig @@ -135,7 +135,7 @@ pub const Token = struct { /// Represents a tokenization error. Instead of reporting an error during /// tokenization, parsing will begin, and an error will be reported when - /// this token is hit. That way, the error can tell you what it expected. + /// this token is hit. That way, the error . invalid, invalid_number, invalid_string, @@ -276,8 +276,8 @@ pub const Token = struct { /// Instead of storing the length of a token, we can recompute this /// information on demand, as it is not often needed. /// - /// If you are computing the length of something that is knowable - /// without the source code, use token.tag.staticLen() instead. + /// When computing the length of something that can be known without + /// the source code, use token.tag.staticLen() instead. pub fn len(token: Token, utf8_source: [:0]const u8) u32 { return token.tag.staticLen() orelse switch (token.tag) { diff --git a/src/core/Transform.zig b/src/core/Transform.zig index 785be5697d6eea5a478ffbcf822c8e8fe7818190..4db0d59b9feab0f6da84f31d2a72b20038ada801 100644 --- a/src/core/Transform.zig +++ b/src/core/Transform.zig @@ -137,10 +137,9 @@ const ResultType = union(enum) { .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. + // Result types do not have associated nodes which is okay as `type_of` cannot + // fail in the context it is used in. A predictable poison value is used instead + // so that an attempt to print the following will cause a crash. .src_node = std.math.maxInt(i32), .operand = r, }), @@ -281,6 +280,7 @@ const Symbol = struct { value: union { mutable: Inst.Index, immutable: Inst.Ref, + param_index: u32, none: void, }, result_type_ref: Inst.Ref, diff --git a/src/core/js/Link.zig b/src/core/js/Link.zig index 1fbb16b6fb7618dd551f650de75c2c7eeabd5d59..a60404f119611e2fd5e5823044f36caebf92c403 100644 --- a/src/core/js/Link.zig +++ b/src/core/js/Link.zig @@ -309,7 +309,18 @@ fn printValue(link: *Link, value: InternPool.Value, level: Level, writer: anytyp if (i != 0) try writer.writeAll(","); - // TODO: handle special identifiers + try link.printValue(inner_value, .assignment, writer); + } + try writer.writeAll("]"); + }, + + .tuple_value => |tuple_value| { + try writer.writeAll("["); + + for (tuple_value.items, 0..) |inner_value, i| { + if (i != 0) + try writer.writeAll(","); + try link.printValue(inner_value, .assignment, writer); } try writer.writeAll("]"); diff --git a/src/core/js/NodeGen.zig b/src/core/js/NodeGen.zig index 14d2d9ce4b57ac5fc3c48b1da6438e19ec86c2c1..72a3411dab72b76f9ddc6d5398e4cbefe36dff0d 100644 --- a/src/core/js/NodeGen.zig +++ b/src/core/js/NodeGen.zig @@ -283,6 +283,15 @@ fn codeGenInst(c: *NodeGen, inst: Air.Inst.Index) Error!void { try c.markExpression(inst, node); }, + .field_tuple => { + const data: Air.FieldStruct = c.air.instData(.field_tuple, inst); + const node = try c.add(.index, .{ + .lhs = try c.fetchInstExpression(data.base), + .rhs = try c.makeValueInst(.{ .int32 = @intCast(data.field_index) }), + }); + try c.markExpression(inst, node); + }, + .array_len => { const data: Air.Un = c.air.instData(.array_len, inst); const node = try c.add(.field, .{ @@ -459,9 +468,9 @@ fn codeGenInst(c: *NodeGen, inst: Air.Inst.Index) Error!void { }, .array_init => { - const struct_init: Air.ArrayInit = c.air.instData(.array_init, inst); + const array_init: Air.ArrayInit = c.air.instData(.array_init, inst); - const items = struct_init.list.get(&c.air).slice(&c.air); + const items = array_init.list.get(&c.air).slice(&c.air); const store_offset = try c.extra.alloc(c.gpa, items.len); for ( @@ -478,6 +487,28 @@ fn codeGenInst(c: *NodeGen, inst: Air.Inst.Index) Error!void { }); try c.markExpression(inst, node); }, + + .tuple_init => { + const tuple_init: Air.TupleInit = c.air.instData(.tuple_init, inst); + + const field_count = tuple_init.ty.toTupleType(c.ip).?.items.len; + const items = tuple_init.field_values; + const store_offset = try c.extra.alloc(c.gpa, field_count); + + for ( + items..items + field_count, + store_offset.., + ) |in_i, expr_i| { + const in = try c.fetchExpression(@bitCast(c.air.extra.list.items[in_i])); + c.extra.list.items[expr_i] = @intFromEnum(in); + } + + const node = try c.add(.array, .{ + .start = store_offset, + .len = @intCast(field_count), + }); + try c.markExpression(inst, node); + }, } } @@ -587,6 +618,13 @@ fn valueToExpression(c: *NodeGen, v: Value) !Node.Index { } } +fn makeValueInst(c: *NodeGen, k: InternPool.Key) !Node.Index { + return try c.valueToExpression(c.ip.get(c.gpa, k) catch |err| switch (err) { + error.TooManyValues => @panic("TODO: handle TooManyValues"), + else => |e| return e, + }); +} + fn addTemporary(c: *NodeGen) !Node.Index { const t = tracer.trace(@src()); defer t.end(); @@ -727,6 +765,7 @@ fn canLowerToExpression(c: *const NodeGen, inst: Air.Inst.Ref) bool { .arg, .field_opaque, .field_struct, + .field_tuple, .field_array, .add, .sub, @@ -741,6 +780,7 @@ fn canLowerToExpression(c: *const NodeGen, inst: Air.Inst.Ref) bool { .struct_init, .array_init, .array_len, + .tuple_init, => true, // JavaScript variable declarations require statements. diff --git a/src/core/stack.zig b/src/core/stack.zig index 6ce630a36106d8fdb64b70b2bf836b71c9e8cf58..3e8b00dc1d26c7e2f93a1fe985abf20eee83f88b 100644 --- a/src/core/stack.zig +++ b/src/core/stack.zig @@ -15,7 +15,7 @@ pub fn Stack(comptime Base: type) type { pub const Mark = struct { stack: *Self, - index: usize, + index: u32, pub fn deinit(m: Mark) void { if (core.runtime_safety) @@ -23,8 +23,8 @@ pub fn Stack(comptime Base: type) type { m.stack.list.shrinkRetainingCapacity(m.index); } - pub fn len(m: Mark) usize { - return m.stack.list.items.len - m.index; + pub fn len(m: Mark) u32 { + return @intCast(m.stack.list.items.len - m.index); } pub fn slice(m: Mark, comptime T: type) []T { @@ -39,7 +39,7 @@ pub fn Stack(comptime Base: type) type { pub fn mark(stack: *Self) Mark { return .{ .stack = stack, - .index = stack.list.items.len, + .index = @intCast(stack.list.items.len), }; } @@ -101,8 +101,8 @@ pub fn Stack(comptime Base: type) type { return struct { stack: *Self, - starts: [fields.len]usize, - lengths: [fields.len]usize, + starts: [fields.len]u32, + lengths: [fields.len]u32, fn Type(comptime field: FieldEnum) type { return @field(types, @tagName(field)); @@ -128,6 +128,25 @@ pub fn Stack(comptime Base: type) type { std.mem.copyForwards(u32, dest, from); r.stack.list.shrinkRetainingCapacity(start + from.len); } + + pub fn index(r: @This()) u32 { + return r.starts[0]; + } + + pub fn fromIndex( + stack: *Self, + start: u32, + lengths: [std.meta.fields(@TypeOf(types)).len]u32, + ) @This() { + var all_lengths: u32 = 0; + var starts: [std.meta.fields(@TypeOf(types)).len]u32 = undefined; + inline for (lengths, &starts) |len, *slice_start| { + slice_start.* = start + all_lengths; + all_lengths += len; + } + stack.list.items.len += all_lengths; + return .{ .stack = stack, .starts = starts, .lengths = lengths }; + } }; } @@ -137,13 +156,13 @@ pub fn Stack(comptime Base: type) type { stack: *Self, allocator: Allocator, comptime types: anytype, - lengths: [std.meta.fields(@TypeOf(types)).len]usize, + lengths: [std.meta.fields(@TypeOf(types)).len]u32, ) !ReserveMulti(types) { var all_lengths: usize = 0; - var starts: [std.meta.fields(@TypeOf(types)).len]usize = undefined; + var starts: [std.meta.fields(@TypeOf(types)).len]u32 = undefined; const start = stack.list.items.len; inline for (lengths, &starts) |len, *slice_start| { - slice_start.* = start + all_lengths; + slice_start.* = @intCast(start + all_lengths); all_lengths += len; } try stack.list.ensureUnusedCapacity(allocator, all_lengths); diff --git a/test/behavior/tuple b/test/behavior/tuple index 5130f76840e7af806599dc98fd2aea19657b10c5..088915e7301707580c30f035b94ae81f2a5dd980 100644 --- a/test/behavior/tuple +++ b/test/behavior/tuple @@ -1,13 +1,14 @@ let assertEqual = @extern("assertEqual"); -fn hello(x: type) -> y(x, float) { - (5, 2.5) +fn hello() -> tuple (int, float) { + let x: tuple (int, float) = (5, 2.5); + x } fn main { let x = hello(); - assertEqual(x.0, 5); - assertEqual(x.1, 2.5); + assertEqual(x.0, i(5)); + assertEqual(x.1, f(2.5)); } fn f(a: float) -> float { a } -- 2.54.0