From 15d77cb85855a5a5fedbfc51ac3a76b6f937db30 Mon Sep 17 00:00:00 2001 From: dave caruso Date: Mon, 16 Sep 2024 03:14:19 -0400 Subject: [PATCH] feat: implement tuple types (#109) this regresses return types by making analysis of a function's return type when it has not been inferred yet panic. it does not provide any code to prevent the panic --- CHANGELOG.md | 3 +- src/cli.zig | 9 +- src/cli_compiler.zig | 146 +++++++++++ src/cli_transform.zig | 3 +- src/core/Air.Dump.zig | 40 ++- src/core/Air.zig | 30 ++- src/core/Ast.Dump.zig | 2 + src/core/InternPool.zig | 158 +++++++++++- src/core/Parse.zig | 554 +++++++++++++++++++++------------------- src/core/Sema.zig | 429 +++++++++++++++++++++++++------ src/core/Teg.Dump.zig | 18 ++ src/core/Teg.zig | 44 +++- src/core/Tokenizer.zig | 7 +- src/core/Transform.zig | 40 ++- src/core/js/Js.zig | 2 +- src/core/js/Link.zig | 13 +- src/core/js/NodeGen.zig | 46 +++- src/core/stack.zig | 37 ++- src/main.zig | 121 ++------- test/behavior/tuple | 15 ++ 20 files changed, 1245 insertions(+), 472 deletions(-) create mode 100644 src/cli_compiler.zig create mode 100644 test/behavior/tuple diff --git a/CHANGELOG.md b/CHANGELOG.md index e4d92933b3ffc2773110675cbe61c502e2359734..4fd3a46010f6e891a8b671348cce1db15e4084c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,8 @@ - feat: add array index syntax via `x[1]` and `x.1` - feat: add `len` property to arrays - feat: rename `noreturn` to `never` -- feat: allow instantiating `@extern` +- feat: allow instantiating `@extern` at compile time +- feat: implement tuples - fix: crash when printing unused struct declaration error - fix: provide array result type to sema - fix: crash when analyzing an empty array without a result type diff --git a/src/cli.zig b/src/cli.zig index fd981159f46ab157af9a83a7a403f460203ea21c..39ee68fcf7bf3361b25ee74cb1c27923ad37e1df 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -8,7 +8,7 @@ options: []const Option = &.{}, positionals: Positionals = .none, help: HelpConfig = .{}, -const Option = struct { +pub const Option = struct { /// Should be formatted as the flag name: '--dump-ir' name: []const u8, /// Displayed in generated help menu @@ -32,6 +32,10 @@ const Option = struct { } }; + pub fn optional(comptime cond: bool, opts: []const Option) []const Option { + return if (cond) opts else &.{}; + } + const help: Option = .{ .name = "--help", .short = 'h', @@ -631,8 +635,7 @@ pub fn main() !void { const program_name, const args = try Cli.programArgsAlloc(alloc); defer Cli.programArgsFree(alloc, args); - const parsed = config.parse(args[1..]) catch - config.showHelpAndExit(program_name); + const parsed = config.parse(program_name, args[1..]); std.debug.print("--flag: {any}\n", .{parsed.flag}); std.debug.print("--str: {?s}\n", .{parsed.str}); diff --git a/src/cli_compiler.zig b/src/cli_compiler.zig new file mode 100644 index 0000000000000000000000000000000000000000..4e1a6a189acbea0aa9b2d29bd0f9e23f5845872e --- /dev/null +++ b/src/cli_compiler.zig @@ -0,0 +1,146 @@ +//! For testing the core compiler with minimal feature and +//! additional debugging parameters. + +const config = @import("config"); + +const arg_config: Cli = .{ + .options = &.{ + .{ .short = 'i', .name = "--dump-ir", .desc = "Dump intermediate representations" }, + .{ .name = "--dump-state", .desc = "Dump compiler state to JSON", .kind = .string }, + .{ .short = 'r', .name = "--report-trace", .desc = "Print stack traces to errors" }, + .{ .short = 'L', .name = "--no-leak-checks", .desc = "" }, + }, + .positionals = .single, + .help = .{ + .usage = "[options...] [entry file]", + }, +}; + +pub fn main() !u8 { + try tracer.init(); + defer tracer.deinit(); + try tracer.init_thread(null); + defer tracer.deinit_thread(); + + // unicode props is asserted to be initialized before core.Tokenizer is used + // this will result in a static page allocation, which is never cleaned up. + try core.unicode_props.init(); + + const use_gpa = builtin.mode == .Debug; + var gpa = if (use_gpa) + std.heap.GeneralPurposeAllocator(.{ + .stack_trace_frames = 100, + }){} + else + std.heap.ArenaAllocator.init(std.heap.page_allocator); + var no_leak_checks = false; + defer _ = if (use_gpa and !no_leak_checks) gpa.deinit(); + const alloc = gpa.allocator(); + + // Current working directory is required for CLI resolution. + var cwd_buf: [std.fs.max_path_bytes]u8 = undefined; + const cwd = try std.posix.getcwd(&cwd_buf); + + // 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, &.{ + cwd, + opts.positionals orelse + arg_config.showHelpAndExit(program_name), + }); + defer alloc.free(file_path); + + no_leak_checks = opts.no_leak_checks orelse false; + + // Execute compiler + { + var js_backend = core.Js{}; + + var comp = try core.Comp.init(.{ + .gpa = tracer.tracingAllocator(&alloc), + .options = .{ + .dump_ir = opts.dump_ir orelse false, + }, + .backend = .{ .js = &js_backend }, + }); + defer comp.deinit(); + + defer if (opts.dump_state) |path| { + dumpState(&comp, path) catch |err| { + std.debug.print("Failed to dump compilation state: {}", .{err}); + }; + }; + + const main_file = try comp.getFile(file_path); + + var fail_report: Report = undefined; + const teg = main_file.getTeg(&comp, &fail_report) catch |err| switch (err) { + error.TransformFail => { + std.debug.print("Transform failed:\n", .{}); + try fail_report.dump(.initComp(&comp, main_file), comp.out); + fail_report.deinit(comp.gpa); + return 1; + }, + error.ParseFail => { + // Already printed. + return 1; + }, + else => return err, + }; + + if (comp.options.dump_ir) { + const stderr = std.io.getStdErr().writer(); + try teg.dumpToWriter(stderr, null); + } + + const entry_point = try comp.enqueueEntryModule(main_file); + comp.runAllJobs() catch |err| switch (err) { + error.AnalysisFail => { + // error is already printed + return 1; + }, + else => return err, + }; + + { + // Output to standard out + const output = std.io.getStdOut().writer(); + var bufw = std.io.bufferedWriter(output); + const writer = bufw.writer(); + try js_backend.link(&comp, entry_point, writer.any()); + try bufw.flush(); + } + } + + std.process.cleanExit(); + return 0; +} + +const core = @import("core"); +const Comp = core.Comp; +const Report = core.Report; + +const tracer = @import("tracer"); +pub const tracer_impl = @field(tracer, @tagName(@import("config").tracing_mode)); + +const std = @import("std"); +const builtin = @import("builtin"); +const Cli = @import("Cli.zig"); + +fn dumpState(comp: *const Comp, dump_path: []const u8) !void { + const file = try std.fs.cwd().createFile(dump_path, .{}); + defer file.close(); + + var bufw = std.io.bufferedWriter(file.writer()); + var jw = std.json.writeStream(bufw.writer(), .{ + .whitespace = .indent_2, + }); + + try comp.jsonStringify(&jw); + try bufw.writer().writeAll("\n"); + try bufw.flush(); +} diff --git a/src/cli_transform.zig b/src/cli_transform.zig index 44920293e9b2d00e4fbde53476d3b694c332b957..76200d6457a22fefd34076def4e44cfe3c58c97e 100644 --- a/src/cli_transform.zig +++ b/src/cli_transform.zig @@ -25,8 +25,7 @@ pub fn main() !u8 { const program_name, const raw_args = try Cli.programArgsAlloc(alloc, "transform"); defer Cli.programArgsFree(alloc, raw_args); - const args = arg_config.parse(raw_args) catch - arg_config.showHelpAndExit(program_name); + const args = arg_config.parse(program_name, raw_args); const file_path = args.positionals orelse arg_config.fatalWithHelp(program_name, "missing filepath argument\n", .{}); diff --git a/src/core/Air.Dump.zig b/src/core/Air.Dump.zig index 79eb0df165eace069097f479434403224e853b77..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); @@ -379,6 +401,22 @@ pub const State = struct { } try w.writeAll("'"); }, + .tuple_type => |tuple_type| { + try w.writeAll("("); + for (tuple_type.items, 0..) |item, i| { + if (i != 0) try w.writeAll(", "); + try s.dumpValue(w, item.value()); + } + try w.writeAll(")"); + }, + .tuple_value => |tuple_value| { + try w.writeAll("("); + for (tuple_value.items, 0..) |item, i| { + if (i != 0) try w.writeAll(", "); + try s.dumpValue(w, item); + } + try w.writeAll(")"); + }, } } 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/Ast.Dump.zig b/src/core/Ast.Dump.zig index 6eebfd5f2230937fb9089326de97c82562e07a88..820004af937a755181a10d82406db9e852f8d755 100644 --- a/src/core/Ast.Dump.zig +++ b/src/core/Ast.Dump.zig @@ -126,6 +126,8 @@ fn dumpNode(writer: anytype, state: *State, node: NodeIndex) @TypeOf(writer).Err .struct_decl, => try dumpNodeList(writer, state, list, '{', '}'), .builtin_call, + .tuple_init, + .tuple_type, => try dumpNodeList(writer, state, list, '(', ')'), .array_init, => try dumpNodeList(writer, state, list, '[', ']'), diff --git a/src/core/InternPool.zig b/src/core/InternPool.zig index 679e01f3ad3122666d05b6eedda99389fd0b6f6c..a52dd2e72de5d1981a818216355cad6d8bc8846b 100644 --- a/src/core/InternPool.zig +++ b/src/core/InternPool.zig @@ -101,6 +101,8 @@ pub const Tag = enum(u8) { struct_type, /// An array type array_type, + /// A tuple type + tuple_type, /// A type that can be represented with only an enum tag. simple_type, @@ -129,6 +131,8 @@ pub const Tag = enum(u8) { external_value, /// A value masked as "opaque" type opaque_value, + /// A tuple value + tuple_value, pub fn Data(comptime tag: Tag) type { return switch (tag) { @@ -154,13 +158,15 @@ 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, .array_type => ArrayType, .array_value => ArrayValue, .external_value => ExternalValue, + .tuple_type => TupleType, + .tuple_value => TupleValue, }; } @@ -174,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: @@ -199,6 +205,20 @@ pub const Tag = enum(u8) { items_len: u32, }; + /// Trailing: + /// 0. item: Type. Repeated per .items_len + pub const TupleType = struct { + items_len: u32, + }; + + /// Trailing: + /// 0. value: Value. Repeated per .items_len + pub const TupleValue = struct { + /// This is the tuple type, not the item type + type: Type, + items_len: u32, + }; + /// Trailing: /// 0. value: Value. Repeated .ty.structInfo().field_len pub const StructValue = struct { @@ -216,9 +236,9 @@ pub const Tag = enum(u8) { /// This is an index into .items pub const Value = enum(u32) { pub const first_type: Value = .opaque_type; - pub const last_type: Value = .numeric_literal_array_type; + pub const last_type: Value = .empty_tuple_type; pub const first_value: Value = .void; - pub const last_value: Value = .empty_array; + pub const last_value: Value = .empty_tuple; opaque_type, bool_type, @@ -230,6 +250,7 @@ pub const Value = enum(u32) { numeric_literal_type, numeric_literal_array_type, never_array_type, + empty_tuple_type, void, bool_true, @@ -239,6 +260,7 @@ pub const Value = enum(u32) { nan, empty_string, empty_array, + empty_tuple, none = std.math.maxInt(u32), _, @@ -316,6 +338,7 @@ pub const Value = enum(u32) { .simple_type, .struct_type, .array_type, + .tuple_type, => .type, .simple_value => switch (value.valueData(ip, .simple_value)) { @@ -332,6 +355,7 @@ pub const Value = enum(u32) { .struct_value => value.toKey(ip).struct_value.ty, .array_value => value.toKey(ip).array_value.type, .external_value, .opaque_value => .@"opaque", + .tuple_value => value.toKey(ip).tuple_value.type, }; } @@ -345,6 +369,7 @@ pub const Value = enum(u32) { .struct_type, .simple_type, .array_type, + .tuple_type, => @enumFromInt(@intFromEnum(value)), // exception: void is a value AND a type @@ -395,6 +420,7 @@ pub const Type = enum(u32) { numeric_literal = @intFromEnum(Value.numeric_literal_type), numeric_literal_array = @intFromEnum(Value.numeric_literal_array_type), never_array = @intFromEnum(Value.never_array_type), + empty_tuple = @intFromEnum(Value.empty_tuple_type), void = @intFromEnum(Value.void), _, @@ -404,6 +430,7 @@ pub const Type = enum(u32) { struct_type = @intFromEnum(InternPool.Tag.struct_type), array_type = @intFromEnum(InternPool.Tag.array_type), simple_type = @intFromEnum(InternPool.Tag.simple_type), + tuple_type = @intFromEnum(InternPool.Tag.tuple_type), }; /// An array of `Index` existing within the `extra` array. @@ -446,6 +473,11 @@ pub const Type = enum(u32) { return ip.valueToKey(ty.value()).array_type; } + pub fn toTupleType(ty: Type, ip: *const InternPool) ?Key.TupleType { + if (ty.tag(ip) != .tuple_type) return null; + return ip.valueToKey(ty.value()).tuple_type; + } + /// If it is possible to represent this type at runtime. Some types do not /// have a defined representation, such as structs (to allow field renaming, unwrapping). pub fn hasPossibleRepresentation(ty: Type, ip: *const InternPool) bool { @@ -466,6 +498,15 @@ pub const Type = enum(u32) { .struct_type, .function_type, => true, + .tuple_type => { + const tuple_type = ty.toTupleType(ip).?; + for (tuple_type.items) |item| { + if (!item.hasPossibleRepresentation(ip)) { + return false; + } + } + return true; + }, .array_type => ty.toArrayType(ip).?.item_type.hasPossibleRepresentation(ip), }, }; @@ -490,6 +531,7 @@ pub const Type = enum(u32) { .simple_type => unreachable, // all simple types are already printed .struct_type, .function_type, + .tuple_type, => false, .array_type => ty.toArrayType(ip).?.item_type.hasDefinedRepresentation(ip), }, @@ -536,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]"); @@ -546,8 +591,29 @@ pub const Type = enum(u32) { const array_type = ty.toArrayType(ip).?; try writer.print("{}", .{array_type.item_type.displayName(ip)}); }, + .tuple_type => { + try writer.writeAll("("); + for (ty.toTupleType(ip).?.items, 0..) |item, i| { + if (i != 0) try writer.writeAll(", "); + try writer.print("{}", .{item.displayName(ip)}); + } + try writer.writeAll(")"); + }, } } + + 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 { @@ -584,6 +650,7 @@ pub const static_keys = [_]Key{ SimpleType.numeric_literal.key(), .{ .array_type = .{ .item_type = Type.numeric_literal } }, .{ .array_type = .{ .item_type = Type.never } }, + .{ .tuple_type = .{ .items = &.{} } }, SimpleValue.void.key(), SimpleValue.true.key(), @@ -596,6 +663,10 @@ pub const static_keys = [_]Key{ .items = &.{}, } }, .{ .string = "" }, + .{ .tuple_value = .{ + .type = .empty_tuple, + .items = &.{}, + } }, }; /// State that is mutable during semantic analysis. This data is not used for @@ -876,11 +947,13 @@ pub const Key = union(enum) { func: Func, array_value: ArrayValue, opaque_value: Value, + tuple_value: TupleValue, // Types simple_type: SimpleType, function_type: FunctionType, struct_type: StructType, array_type: ArrayType, + tuple_type: TupleType, external_value: ExternalValue, const Adapter = struct { @@ -899,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()); @@ -946,6 +1022,14 @@ pub const Key = union(enum) { } }; + pub const TupleType = struct { + items: []const Type, + + pub fn hash(self: TupleType, hasher: *Hash) void { + std.hash.autoHashStrat(hasher, self.items, .Deep); + } + }; + pub const ExternalValue = struct { /// A list of strings to generate the property access /// so `console.log` would be .{ "console", "log" } @@ -967,6 +1051,17 @@ pub const Key = union(enum) { } }; + pub const TupleValue = struct { + /// This is the tuple type, not the item type + type: Type, + items: []const Value, + + pub fn hash(self: TupleValue, hasher: *Hash) void { + std.hash.autoHash(hasher, self.type); + std.hash.autoHashStrat(hasher, self.items, .Deep); + } + }; + pub const Func = struct { ty: Type, owner_decl: DeclIndex, @@ -1008,6 +1103,8 @@ pub const Key = union(enum) { .array_value, .array_type, .external_value, + .tuple_type, + .tuple_value, => |x| { var hasher = Hash.init(seed); x.hash(&hasher); @@ -1047,7 +1144,7 @@ pub const Key = union(enum) { return false; }, .struct_type => { - return false; // structs are nomial + return false; // structs are nominal }, .struct_value => |a_info| { const b_info = b.struct_value; @@ -1066,6 +1163,15 @@ pub const Key = union(enum) { const b_info = b.external_value; return std.mem.eql(NullTerminatedString, a_info.key, b_info.key); }, + .tuple_type => |a_info| { + const b_info = b.tuple_type; + return a_info.items.len == b_info.items.len and std.mem.eql(Type, a_info.items, b_info.items); + }, + .tuple_value => |a_info| { + const b_info = b.tuple_value; + return a_info.type == b_info.type and + std.mem.eql(Value, a_info.items, b_info.items); + }, }; } }; @@ -1336,6 +1442,19 @@ pub fn valueToKey(ip: *const InternPool, index: Value) Key { .key = trail.readNullTerminatedStringSlice(payload.key_count).get(ip), } }; }, + .tuple_type => { + const payload: Tag.TupleType, var trail = data.decode(.tuple_type, item.tag).getTrail(ip); + return .{ .tuple_type = .{ + .items = trail.readTypeSlice(payload.items_len).get(ip), + } }; + }, + .tuple_value => { + const payload: Tag.TupleValue, var trail = data.decode(.tuple_value, item.tag).getTrail(ip); + return .{ .tuple_value = .{ + .type = payload.type, + .items = trail.readValueSlice(payload.items_len).get(ip), + } }; + }, }; } @@ -1391,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); @@ -1439,6 +1558,21 @@ pub fn get(ip: *InternPool, gpa: Allocator, key: Key) !Value { trail.writeSlice(s.key); break :brk Item.encode(.external_value, extra); }, + .tuple_type => |s| brk: { + const extra, var trail = try ip.extra.appendTrailing(gpa, Tag.TupleType, .{ + .items_len = @intCast(s.items.len), + }, s.items.len); + trail.writeSlice(s.items); + break :brk Item.encode(.tuple_type, extra); + }, + .tuple_value => |s| brk: { + const extra, var trail = try ip.extra.appendTrailing(gpa, Tag.TupleValue, .{ + .type = s.type, + .items_len = @intCast(s.items.len), + }, s.items.len); + trail.writeSlice(s.items); + break :brk Item.encode(.tuple_value, extra); + }, }; ip.items.appendAssumeCapacity(item); const index: Value = @enumFromInt(ip.items.len - 1); @@ -1451,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/Parse.zig b/src/core/Parse.zig index f933f3e58c3aaaf1e5610b2909b8ff3543cd9797..120626eac16464a5bea13eff869cf72a4757c3d0 100644 --- a/src/core/Parse.zig +++ b/src/core/Parse.zig @@ -104,15 +104,216 @@ pub const ExpectedContext = enum { } }; +const Operator = enum(u8) { + add = @intFromEnum(Token.Tag.plus), + sub = @intFromEnum(Token.Tag.minus), + mul = @intFromEnum(Token.Tag.star), + div = @intFromEnum(Token.Tag.slash), + rem = @intFromEnum(Token.Tag.percent), + pow = @intFromEnum(Token.Tag.star_star), + @"and" = @intFromEnum(Token.Tag.and_and), + @"or" = @intFromEnum(Token.Tag.pipe_pipe), + bit_and = @intFromEnum(Token.Tag.@"and"), + bit_or = @intFromEnum(Token.Tag.pipe), + bit_xor = @intFromEnum(Token.Tag.caret), + shl = @intFromEnum(Token.Tag.lt_lt), + shr = @intFromEnum(Token.Tag.gt_gt), + eq = @intFromEnum(Token.Tag.equal_equal), + ne = @intFromEnum(Token.Tag.bang_equal), + lt = @intFromEnum(Token.Tag.lt), + le = @intFromEnum(Token.Tag.lt_equal), + gt = @intFromEnum(Token.Tag.gt), + ge = @intFromEnum(Token.Tag.gt_equal), + // Following operators are assignment operators + assign = @intFromEnum(Token.Tag.equal), + add_assign = @intFromEnum(Token.Tag.plus_equal), + sub_assign = @intFromEnum(Token.Tag.minus_equal), + mul_assign = @intFromEnum(Token.Tag.star_equal), + div_assign = @intFromEnum(Token.Tag.slash_equal), + rem_assign = @intFromEnum(Token.Tag.percent_equal), + pow_assign = @intFromEnum(Token.Tag.star_star_equal), + and_assign = @intFromEnum(Token.Tag.and_and_equal), + or_assign = @intFromEnum(Token.Tag.pipe_pipe_equal), + bit_and_assign = @intFromEnum(Token.Tag.and_equal), + bit_or_assign = @intFromEnum(Token.Tag.pipe_equal), + bit_xor_assign = @intFromEnum(Token.Tag.caret_equal), + shl_assign = @intFromEnum(Token.Tag.lt_lt_equal), + shr_assign = @intFromEnum(Token.Tag.gt_gt_equal), + + const Direction = enum { ltr, rtl }; + + /// Only the binary operator levels are actually used, + /// but all are listed for reference. + const Level = enum { + /// e. e[] e() + accessor, + /// new + new, + /// ++e, --e, !e, ~e, +e, -e, typeof, void, delete + prefix, + /// ** + exponentiation, + /// *, /, % + multiplicative, + /// +, - + additive, + /// <<, >>, >>> + bitwise_shift, + /// <, <=, >, >= + relational, + /// ==, != + equality, + /// & + bit_and, + /// ^ + bit_xor, + /// | + bit_or, + /// && + logical_and, + /// ||, ?? + logical_or, + /// =, +=, -=, *=, /=, %=, / <<=, >>=, >>>=, &=, ^=, |= + assign, + }; + + pub fn direction(op: Operator) Direction { + return switch (op) { + .pow => .rtl, + + .assign, + .add_assign, + .sub_assign, + .mul_assign, + .div_assign, + .rem_assign, + .pow_assign, + .and_assign, + .or_assign, + .bit_and_assign, + .bit_or_assign, + .bit_xor_assign, + .shl_assign, + .shr_assign, + => .rtl, + + else => .ltr, + }; + } + + pub fn level(op: Operator) Level { + return switch (op) { + .pow => .exponentiation, + .mul, .div, .rem => .multiplicative, + .add, .sub => .additive, + .shl, .shr => .bitwise_shift, + .lt, .le, .gt, .ge => .relational, + .eq, .ne => .equality, + .@"and" => .logical_and, + .@"or" => .logical_or, + .bit_and => .bit_and, + .bit_xor => .bit_xor, + .bit_or => .bit_or, + .assign, + .add_assign, + .sub_assign, + .mul_assign, + .div_assign, + .rem_assign, + .pow_assign, + .and_assign, + .or_assign, + .bit_and_assign, + .bit_or_assign, + .bit_xor_assign, + .shl_assign, + .shr_assign, + => .assign, + }; + } + + const lut = brk: { + const token_tags = std.enums.values(Token.Tag); + const op_tags = std.enums.values(Operator); + + var is_operator_lut: [token_tags.len]bool = .{false} ** token_tags.len; + for (op_tags) |op| { + is_operator_lut[@intFromEnum(op)] = true; + } + + var ast_tag_lut: [token_tags.len]Node.Tag = .{.root} ** token_tags.len; + for (op_tags) |op| { + ast_tag_lut[@intFromEnum(op)] = @field(Node.Tag, @tagName(op)); + } + + break :brk struct { + const is_operator = is_operator_lut; + const ast_tag = ast_tag_lut; + }; + }; + + pub fn fromToken(tag: Token.Tag) ?Operator { + return if (lut.is_operator[@intFromEnum(tag)]) + @enumFromInt(@intFromEnum(tag)) + else + null; + } + + pub fn astTag(op: Operator) Node.Tag { + const tag = lut.ast_tag[@intFromEnum(op)]; + assert(tag != .root); // invalid operator or lut construction + return tag; + } + + pub fn isAssign(op: Operator) bool { + return switch (op) { + .assign, + .add_assign, + .sub_assign, + .mul_assign, + .div_assign, + .rem_assign, + .pow_assign, + .and_assign, + .or_assign, + .bit_and_assign, + .bit_or_assign, + .bit_xor_assign, + .shl_assign, + .shr_assign, + => true, + else => false, + }; + } +}; + +const ParseExprMode = enum { + /// Standard parsing + expression, + /// Statements allow assignment binary operators in addition to other expressions. + statement, + /// This bans struct initializations so that parsing stops at a '{' + /// Example: "if hello {" must not be parsed as a struct initialization. + preceding_block, +}; + +/// Some constructs are valid expressions as well as valid top-level +/// declarations. Top level declarations must have associated names, but the +/// anonymous versions of these must not have names. +const ExprParseContext = enum { + declaration, + expression, +}; + +const TypePresence = enum { + types_optional, + types_required, +}; + /// When error.SyntaxError is returned, `p.err` is set to this as well as `p.pos` /// When printing these, token list and Ast is not actually available, so full tokens /// are stored in the errors. pub const AstError = union(enum) { - // /// Failure during tokenization - // tokenization: struct { - // err: Tokenizer.TokenizeError, - // byte: ByteOffset, - // }, /// Unexpected data, expected a token. unexpected_token: struct { expected: Token.Tag, @@ -134,23 +335,6 @@ pub const AstError = union(enum) { pub fn build(err: AstError, ctx: *Report.MsgContext) !void { switch (err) { - // .tokenization => |data| { - // // TODO: consider making tokenizer emit AstError more directly, - // // so that it can just annotate start and ends - // try ctx.msg(switch (data.err) { - // error.UnterminatedString => .{ .codepoint_eol = data.byte }, - // else => .{ .codepoint = data.byte }, - // }, "{s}", .{switch (data.err) { - // error.OutOfMemory => unreachable, - // error.InvalidSyntax => "unexpected token", - // error.TrailingNumericSeparator => "trailing numeric separator", - // error.UnterminatedString => "unterminated string literal", - // error.InvalidEscapeSequence => "invalid escape sequence", - // error.InvalidEscapeCodepoint => "invalid escape codepoint", - // error.InvalidUtf8 => "file is not valid utf8", - // error.GitConflict => "git merge conflict encountered", - // }}); - // }, .unexpected_token => |data| { try ctx.msg(.{ .token_stored = data.got }, "expected {s}, but found {s}", .{ data.expected.description(), @@ -230,7 +414,8 @@ fn parseModuleMembers(p: *Parse) Error!SubList { .kw_pub, => try p.scratch.append(p.gpa, try p.parseModuleDecl()), - .kw_struct => try p.scratch.append(p.gpa, try p.parseStructDecl()), + .kw_struct => try p.scratch.append(p.gpa, try p.parseStructDecl(.declaration)), + .kw_tuple => try p.scratch.append(p.gpa, try p.parseStructDecl(.declaration)), .id => if (p.isContextualKeyword("test")) { try p.scratch.append(p.gpa, try p.parseTestDecl()); @@ -292,14 +477,6 @@ fn parseModuleDecl(p: *Parse) Error!NodeIndex { }; } -/// Some constructs are valid expressions as well as valid top-level -/// declarations. Top level declarations must have associated names, but the -/// anonymous versions of these must not have names. -const ExprParseContext = enum { - declaration, - expression, -}; - fn parseFnDecl(p: *Parse) Error!NodeIndex { const trace = t.trace(@src()); defer trace.end(); @@ -339,8 +516,6 @@ fn parseFnDecl(p: *Parse) Error!NodeIndex { }); } -const TypePresence = enum { types_optional, types_required }; - fn parseParamList(p: *Parse, types: TypePresence) Error!SubList { const trace = t.trace(@src()); defer trace.end(); @@ -390,13 +565,16 @@ fn parseParamDecl(p: *Parse, types: TypePresence) Error!NodeIndex { }); } -fn parseStructDecl(p: *Parse) Error!NodeIndex { +fn parseStructDecl(p: *Parse, ctx: ExprParseContext) Error!NodeIndex { const trace = t.trace(@src()); defer trace.end(); const main_token = p.assertToken(.kw_struct); - _ = try p.expect(.id); + switch (ctx) { + .declaration => _ = try p.expect(.id), + else => {}, + } _ = try p.expect(.start_brace); @@ -409,6 +587,28 @@ fn parseStructDecl(p: *Parse) Error!NodeIndex { }); } +fn parseTupleDecl(p: *Parse, ctx: ExprParseContext) Error!NodeIndex { + const trace = t.trace(@src()); + defer trace.end(); + + const main_token = p.assertToken(.kw_tuple); + + switch (ctx) { + .declaration => _ = try p.expect(.id), + else => {}, + } + + _ = try p.expect(.start_paren); + + const fields = try p.parseExpressionList(.end_paren, .expression); + + return try p.addNode(.{ + .tag = .tuple_type, + .main_token = main_token, + .data = .{ .list = fields }, + }); +} + fn parseTestDecl(p: *Parse) Error!NodeIndex { const trace = t.trace(@src()); defer trace.end(); @@ -447,9 +647,7 @@ fn parseStructFieldList(p: *Parse) Error!SubList { try p.scratch.append(p.gpa, try p.parseStructField()); switch (p.currentTag()) { - .comma => { - _ = p.nextToken(); - }, + .comma => _ = p.nextToken(), .end_brace => break, else => return p.failUnexpectedToken(.struct_field), } @@ -578,13 +776,13 @@ fn parseVarDecl(p: *Parse) Error!NodeIndex { _ = try p.expect(.id); const data_type: OptionalNodeIndex = if (p.eatToken(.colon)) |_| - optionalNodeIndex(try p.tryParseExpression(.expression)) + optionalNodeIndex(try p.parseExpressionOrFail(.expression)) else .none; _ = try p.expect(.equal); - const data_init = try p.tryParseExpression(.expression); + const data_init = try p.parseExpressionOrFail(.expression); _ = try p.expect(.semi); @@ -619,204 +817,13 @@ fn isExpressionStart(p: *Parse) bool { .string, .tilde, .kw_fn, + .kw_struct, + .kw_tuple, => true, else => false, }; } -const Operator = enum(u8) { - add = @intFromEnum(Token.Tag.plus), - sub = @intFromEnum(Token.Tag.minus), - mul = @intFromEnum(Token.Tag.star), - div = @intFromEnum(Token.Tag.slash), - rem = @intFromEnum(Token.Tag.percent), - pow = @intFromEnum(Token.Tag.star_star), - @"and" = @intFromEnum(Token.Tag.and_and), - @"or" = @intFromEnum(Token.Tag.pipe_pipe), - bit_and = @intFromEnum(Token.Tag.@"and"), - bit_or = @intFromEnum(Token.Tag.pipe), - bit_xor = @intFromEnum(Token.Tag.caret), - shl = @intFromEnum(Token.Tag.lt_lt), - shr = @intFromEnum(Token.Tag.gt_gt), - eq = @intFromEnum(Token.Tag.equal_equal), - ne = @intFromEnum(Token.Tag.bang_equal), - lt = @intFromEnum(Token.Tag.lt), - le = @intFromEnum(Token.Tag.lt_equal), - gt = @intFromEnum(Token.Tag.gt), - ge = @intFromEnum(Token.Tag.gt_equal), - // Following operators are assignment operators - assign = @intFromEnum(Token.Tag.equal), - add_assign = @intFromEnum(Token.Tag.plus_equal), - sub_assign = @intFromEnum(Token.Tag.minus_equal), - mul_assign = @intFromEnum(Token.Tag.star_equal), - div_assign = @intFromEnum(Token.Tag.slash_equal), - rem_assign = @intFromEnum(Token.Tag.percent_equal), - pow_assign = @intFromEnum(Token.Tag.star_star_equal), - and_assign = @intFromEnum(Token.Tag.and_and_equal), - or_assign = @intFromEnum(Token.Tag.pipe_pipe_equal), - bit_and_assign = @intFromEnum(Token.Tag.and_equal), - bit_or_assign = @intFromEnum(Token.Tag.pipe_equal), - bit_xor_assign = @intFromEnum(Token.Tag.caret_equal), - shl_assign = @intFromEnum(Token.Tag.lt_lt_equal), - shr_assign = @intFromEnum(Token.Tag.gt_gt_equal), - - const Direction = enum { ltr, rtl }; - - /// Only the binary operator levels are actually used, - /// but all are listed for reference. - const Level = enum { - /// e. e[] e() - accessor, - /// new - new, - /// ++e, --e, !e, ~e, +e, -e, typeof, void, delete - prefix, - /// ** - exponentiation, - /// *, /, % - multiplicative, - /// +, - - additive, - /// <<, >>, >>> - bitwise_shift, - /// <, <=, >, >= - relational, - /// ==, != - equality, - /// & - bit_and, - /// ^ - bit_xor, - /// | - bit_or, - /// && - logical_and, - /// ||, ?? - logical_or, - /// =, +=, -=, *=, /=, %=, / <<=, >>=, >>>=, &=, ^=, |= - assign, - }; - - pub fn direction(op: Operator) Direction { - return switch (op) { - .pow => .rtl, - - .assign, - .add_assign, - .sub_assign, - .mul_assign, - .div_assign, - .rem_assign, - .pow_assign, - .and_assign, - .or_assign, - .bit_and_assign, - .bit_or_assign, - .bit_xor_assign, - .shl_assign, - .shr_assign, - => .rtl, - - else => .ltr, - }; - } - - pub fn level(op: Operator) Level { - return switch (op) { - .pow => .exponentiation, - .mul, .div, .rem => .multiplicative, - .add, .sub => .additive, - .shl, .shr => .bitwise_shift, - .lt, .le, .gt, .ge => .relational, - .eq, .ne => .equality, - .@"and" => .logical_and, - .@"or" => .logical_or, - .bit_and => .bit_and, - .bit_xor => .bit_xor, - .bit_or => .bit_or, - .assign, - .add_assign, - .sub_assign, - .mul_assign, - .div_assign, - .rem_assign, - .pow_assign, - .and_assign, - .or_assign, - .bit_and_assign, - .bit_or_assign, - .bit_xor_assign, - .shl_assign, - .shr_assign, - => .assign, - }; - } - - const lut = brk: { - const token_tags = std.enums.values(Token.Tag); - const op_tags = std.enums.values(Operator); - - var is_operator_lut: [token_tags.len]bool = .{false} ** token_tags.len; - for (op_tags) |op| { - is_operator_lut[@intFromEnum(op)] = true; - } - - var ast_tag_lut: [token_tags.len]Node.Tag = .{.root} ** token_tags.len; - for (op_tags) |op| { - ast_tag_lut[@intFromEnum(op)] = @field(Node.Tag, @tagName(op)); - } - - break :brk struct { - const is_operator = is_operator_lut; - const ast_tag = ast_tag_lut; - }; - }; - - pub fn fromToken(tag: Token.Tag) ?Operator { - return if (lut.is_operator[@intFromEnum(tag)]) - @enumFromInt(@intFromEnum(tag)) - else - null; - } - - pub fn astTag(op: Operator) Node.Tag { - const tag = lut.ast_tag[@intFromEnum(op)]; - assert(tag != .root); // invalid operator or lut construction - return tag; - } - - pub fn isAssign(op: Operator) bool { - return switch (op) { - .assign, - .add_assign, - .sub_assign, - .mul_assign, - .div_assign, - .rem_assign, - .pow_assign, - .and_assign, - .or_assign, - .bit_and_assign, - .bit_or_assign, - .bit_xor_assign, - .shl_assign, - .shr_assign, - => true, - else => false, - }; - } -}; - -const ParseExprMode = enum { - /// Standard parsing - expression, - /// Statements allow assignment binary operators in addition to other expressions. - statement, - /// This bans struct initializations so that parsing stops at a '{' - /// Example: "if hello {" must not be parsed as a struct initialization. - preceding_block, -}; - fn parseExpression(p: *Parse, mode: ParseExprMode) Error!NodeIndex { const trace = t.trace(@src()); defer trace.end(); @@ -831,8 +838,13 @@ fn parseExpression(p: *Parse, mode: ParseExprMode) Error!NodeIndex { // These unary operators are greedy, so handle them first. switch (p.currentTag()) { - .kw_return => return p.parseReturn(), - .kw_break => return p.parseBreak(), + .kw_return => return p.parseReturn(mode), + .kw_break => return p.parseBreak(mode), + + .kw_assert => return p.parseGreedyUnary(.assert, mode), + .kw_comptime => return p.parseGreedyUnary(.@"comptime", mode), + .kw_pure => return p.parseGreedyUnary(.pure, mode), + else => {}, } @@ -846,7 +858,7 @@ fn parseExpression(p: *Parse, mode: ParseExprMode) Error!NodeIndex { return expr; } -fn tryParseExpression(p: *Parse, mode: ParseExprMode) Error!NodeIndex { +fn parseExpressionOrFail(p: *Parse, mode: ParseExprMode) Error!NodeIndex { return if (p.isExpressionStart()) p.parseExpression(mode) else @@ -858,7 +870,6 @@ fn parseExpressionPart(p: *Parse, mode: ParseExprMode) Error!NodeIndex { defer trace.end(); var expr = switch (p.currentTag()) { - // Literals .id => if (p.nextTag() == .colon) try p.parseLabelExpression() else @@ -867,24 +878,27 @@ fn parseExpressionPart(p: *Parse, mode: ParseExprMode) Error!NodeIndex { .number_float => try p.blankNode(.number_float), .string => try p.blankNode(.string), - // Complex expressions .builtin_id => try p.parseBuiltinCall(), - .dot => try p.parseAnonInstantiation(), - .kw_break => try p.parseBreak(), + .dot => try p.parseAnonStructInit(), .kw_fn => try p.parseAnonFunction(), .kw_if => try p.parseIf(), - .kw_return => try p.parseReturn(), + .kw_tuple => try p.parseTupleType(), + .start_brace => try p.parseBlock(), .start_bracket => try p.parseArrayValueOrType(mode), - .start_paren => try p.parseTupleType(), + .start_paren => try p.parseTupleInit(), + + .kw_struct => try p.parseStructDecl(.expression), + + .kw_return => return p.parseReturn(mode), + .kw_break => return p.parseBreak(mode), + .kw_assert => return p.parseGreedyUnary(.assert, mode), + .kw_comptime => return p.parseGreedyUnary(.@"comptime", mode), + .kw_pure => return p.parseGreedyUnary(.pure, mode), - // Unary operators inline .bang, .tilde, .minus, - .kw_comptime, - .kw_pure, - .kw_assert, => |tag| unary: { const main_token = p.nextToken(); const expr = try p.parseExpressionPart(mode); @@ -955,7 +969,7 @@ fn parseExpressionPart(p: *Parse, mode: ParseExprMode) Error!NodeIndex { return p.fail(.{ .array_type_flipped_hint = p.fullToken(p.pos) }); } - const index = try p.tryParseExpression(.expression); + const index = try p.parseExpressionOrFail(.expression); _ = try p.expect(.end_bracket); @@ -1020,14 +1034,14 @@ fn parseBinarySuffix(p: *Parse, lhs: NodeIndex, op: Operator, mode: ParseExprMod }); } -fn parseReturn(p: *Parse) Error!NodeIndex { +fn parseReturn(p: *Parse, mode: ParseExprMode) Error!NodeIndex { const trace = t.trace(@src()); defer trace.end(); const main_token = p.assertToken(.kw_return); const expr = if (p.isExpressionStart()) - try p.parseExpression(.expression) + try p.parseExpression(mode) else null; @@ -1040,7 +1054,7 @@ fn parseReturn(p: *Parse) Error!NodeIndex { }); } -fn parseBreak(p: *Parse) Error!NodeIndex { +fn parseBreak(p: *Parse, mode: ParseExprMode) Error!NodeIndex { const trace = t.trace(@src()); defer trace.end(); @@ -1052,7 +1066,7 @@ fn parseBreak(p: *Parse) Error!NodeIndex { null; const expr = if (p.isExpressionStart()) - try p.parseExpression(.expression) + try p.parseExpression(mode) else null; @@ -1066,6 +1080,23 @@ fn parseBreak(p: *Parse) Error!NodeIndex { }); } +fn parseGreedyUnary(p: *Parse, tag: Node.Tag, mode: ParseExprMode) Error!NodeIndex { + const trace = t.trace(@src()); + defer trace.end(); + + const main_token = p.nextToken(); + + const expr = try p.parseExpressionOrFail(mode); + + return try p.addNode(.{ + .tag = tag, + .main_token = main_token, + .data = .{ .un = .{ + .expr = expr, + } }, + }); +} + fn parseBuiltinCall(p: *Parse) Error!NodeIndex { const trace = t.trace(@src()); defer trace.end(); @@ -1083,11 +1114,7 @@ fn parseBuiltinCall(p: *Parse) Error!NodeIndex { }); } -fn parseExpressionList( - p: *Parse, - comptime end_token: Token.Tag, - comptime fail_context: ExpectedContext, -) Error!SubList { +fn parseExpressionList(p: *Parse, comptime end_token: Token.Tag, fail_context: ExpectedContext) Error!SubList { const trace = t.trace(@src()); defer trace.end(); @@ -1161,7 +1188,7 @@ fn parseElseValue(p: *Parse) Error!NodeIndex { }; } -fn parseAnonInstantiation(p: *Parse) Error!NodeIndex { +fn parseAnonStructInit(p: *Parse) Error!NodeIndex { const trace = t.trace(@src()); defer trace.end(); @@ -1272,9 +1299,20 @@ fn parseArrayValueOrType(p: *Parse, mode: ParseExprMode) Error!NodeIndex { }); } -fn parseTupleType(p: *Parse) Error!NodeIndex { +fn parseTupleInit(p: *Parse) Error!NodeIndex { const main_token = p.assertToken(.start_paren); const list = try p.parseExpressionList(.end_paren, .expression); + return p.addNode(.{ + .tag = .tuple_init, + .main_token = main_token, + .data = .{ .list = list }, + }); +} + +fn parseTupleType(p: *Parse) Error!NodeIndex { + const main_token = p.assertToken(.kw_tuple); + _ = try p.expect(.start_paren); + const list = try p.parseExpressionList(.end_paren, .expression); return p.addNode(.{ .tag = .tuple_type, .main_token = main_token, diff --git a/src/core/Sema.zig b/src/core/Sema.zig index cd5881ac5810b77ffc6fb5b0da7d2f552fd074e0..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); @@ -1615,6 +1795,14 @@ fn ensureValueRuntime(s: *Sema, src: LazyNode, ref: Air.Inst.Ref) Error!Air.Inst .value => |v| { const ty = v.typeOf(s.ip); switch (ty) { + // Allowed at runtime + .bool, + .float, + .int32, + .string, + .void, + .empty_tuple, + => {}, .numeric_literal => { const float = v.toFloat(s.ip) orelse unreachable; @@ -1649,13 +1837,6 @@ fn ensureValueRuntime(s: *Sema, src: LazyNode, ref: Air.Inst.Ref) Error!Air.Inst .type = ty, }), .@"opaque" => @panic("TODO: represent comptime-known opaques better"), - // Allowed at runtime - .bool, - .float, - .int32, - .string, - .void, - => {}, _ => if (!ty.hasPossibleRepresentation(s.ip)) { return s.fail(.runtime_incompatible_type, .{ .src = src, @@ -1728,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) { @@ -1861,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.Dump.zig b/src/core/Teg.Dump.zig index 222c7f1beed361a53fc35f22e250a0335b73a4a0..71bccf7dbebad6757c530ff9bf2a823f2b64467d 100644 --- a/src/core/Teg.Dump.zig +++ b/src/core/Teg.Dump.zig @@ -319,6 +319,24 @@ const State = struct { if (data.type_hint != .none) try writer.writeAll(")"); }, + .tuple_type => { + const data: Teg.TupleType = teg.instPayload(.tuple_type, inst); + + try writer.writeAll("("); + try s.dumpRefMarkerList(writer, data.items.slice(teg), '[', ']'); + try writer.writeAll(")"); + }, + .tuple_init => { + const data: Teg.TupleInit = teg.instPayload(.tuple_init, inst); + + try writer.writeAll("("); + if (data.type_hint.unwrap()) |type_hint| { + try s.dumpRef(writer, type_hint); + try writer.writeAll(", "); + } + try s.dumpRefMarkerList(writer, data.items.slice(teg), '[', ']'); + try writer.writeAll(")"); + }, .result_type_argument => { const data: Teg.ResultTypeArgument = teg.instData(.result_type_argument, inst); diff --git a/src/core/Teg.zig b/src/core/Teg.zig index e6e2b340e3386a9622c72ddf865abcfaa1c75c94..d13225a790ad79472320df30366cc39e8ad4f597 100644 --- a/src/core/Teg.zig +++ b/src/core/Teg.zig @@ -75,6 +75,9 @@ pub const Tag = enum(u8) { struct_init, array_init, + tuple_init, + tuple_type, + add, sub, mul, @@ -123,6 +126,7 @@ pub const Tag = enum(u8) { .variable_declaration, .test_declaration, => PlNode(Decl), + .coercion => PlNode(Coercion), .@"break" => PlNode(Break), .number => PlNode(Float), @@ -136,6 +140,8 @@ pub const Tag = enum(u8) { .struct_decl => PlNode(StructDecl), .struct_init => PlNode(StructInit), .array_init => PlNode(ArrayInit), + .tuple_type => PlNode(TupleType), + .tuple_init => PlNode(TupleInit), .func => PlNode(Func), .load_decl => StrNode, @@ -146,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, @@ -162,7 +168,7 @@ pub const Tag = enum(u8) { .assert, => RefNode, - // Two instruction operands + // Two operand instructions .field, .index, .add, @@ -230,7 +236,7 @@ pub const Inst = struct { main_module_inst = 0, _, - pub const static_len = @intFromEnum(Ref.empty_array) + 1; + pub const static_len = @intFromEnum(Ref.empty_tuple) + 1; pub fn toRef(i: Index) Inst.Ref { return @enumFromInt(static_len + @intFromEnum(i)); @@ -269,6 +275,7 @@ pub const Inst = struct { numeric_literal_type, numeric_literal_array_type, never_array_type, + empty_tuple_type, void, bool_true, @@ -278,7 +285,7 @@ pub const Inst = struct { nan, empty_string, empty_array, - + empty_tuple, _, pub const Decoded = union(enum) { @@ -423,6 +430,20 @@ pub const ArrayInit = struct { list: RefList, }; +pub const TupleType = struct { + /// The items in the tuple + items: RefList, +}; + +pub const TupleInit = 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 tuple + items: RefList, +}; + pub const Decl = struct { name: NullTerminatedString, hash: Cache.BinDigest, @@ -697,6 +718,8 @@ pub fn writeToHasher( .struct_decl, .struct_init, .array_init, + .tuple_init, + .tuple_type, .func, .store_decl, .store_field, @@ -902,6 +925,19 @@ fn writePlNodeInnerToHasher( hasher.add(isolatedIndexValue(decl_index, data.body)); hasher.add(data.instruction_count); }, + TupleType => { + hasher.add(data.items.len); + for (data.items.slice(teg)) |item| { + hasher.add(isolatedRefValue(decl_index, item)); + } + }, + TupleInit => { + if (data.type_hint.unwrap()) |type_hint| + hasher.add(isolatedRefValue(decl_index, type_hint)) + else + hasher.add(@as(i32, std.math.maxInt(i32))); + writeRefListToHasher(hasher, decl_index, data.items.slice(teg)); + }, else => @compileError("hash not implemented for " ++ @typeName(T)), } } diff --git a/src/core/Tokenizer.zig b/src/core/Tokenizer.zig index 2ed112a4e62969157e0190d5fcb6f0da3e1520b3..f14b847ebe7fa4f7f511f737fc50f4468295aed4 100644 --- a/src/core/Tokenizer.zig +++ b/src/core/Tokenizer.zig @@ -75,6 +75,7 @@ pub const Token = struct { kw_return, kw_struct, kw_trait, + kw_tuple, kw_var, kw_while, // sort-lines: end @@ -134,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, @@ -275,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 32509ffc2eba9d367bf18111af3fbe6ec57ee987..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, @@ -903,6 +903,7 @@ fn transformExpression( switch (node_tag) { .root, .param_decl, + .struct_field, => unreachable, // not an expression .id => { @@ -1520,6 +1521,37 @@ fn transformExpression( }); }, + .tuple_init => { + const tuple_init = t.ast.nodeData(node).list; + + const items = try t.transformExpressionList(ctx, .none, tuple_init.slice(&t.ast)) orelse return null; + + if (items.len == 0) { + return .empty_tuple; + } + + return try t.addRef(.tuple_init, .{ + .src_node = ctx.nodeOffset(node), + .extra_index = try t.extra.append(t.gpa, Teg.TupleInit, .{ + .type_hint = .none, + .items = items, + }), + }); + }, + + .tuple_type => { + const tuple_type = t.ast.nodeData(node).list; + + const items = try t.transformExpressionList(ctx, .none, tuple_type.slice(&t.ast)) orelse return null; + + return try t.addRef(.tuple_type, .{ + .src_node = ctx.nodeOffset(node), + .extra_index = try t.extra.append(t.gpa, Teg.TupleType, .{ + .items = items, + }), + }); + }, + inline .fn_decl, .fn_expr => |tag| { const data = switch (tag) { .fn_decl => t.ast.fullFnDecl(node), diff --git a/src/core/js/Js.zig b/src/core/js/Js.zig index a4e4d0e445471e24baf6af562a47c835af89923d..71c01f168567a2b5d06203c1fd067d0b2a898666 100644 --- a/src/core/js/Js.zig +++ b/src/core/js/Js.zig @@ -43,7 +43,7 @@ pub fn link( entry_point: Comp.Decl.Index, writer: std.io.AnyWriter, ) !void { - var state = Link{ + var state: Link = .{ .comp = comp, .backend = backend, }; 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 5ed8f7017f896b336b79b901ef3c3c3b721e508f..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); + }, } } @@ -557,6 +588,7 @@ fn valueToExpression(c: *NodeGen, v: Value) !Node.Index { .simple_type, .struct_type, .array_type, + .tuple_type, => unreachable, // Type values are not allowed here .int32, @@ -566,6 +598,7 @@ fn valueToExpression(c: *NodeGen, v: Value) !Node.Index { .simple_value, .struct_value, .array_value, + .tuple_value, => return try c.add(.plain_value, .{ .value = v }), .external_value => { @@ -585,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(); @@ -725,6 +765,7 @@ fn canLowerToExpression(c: *const NodeGen, inst: Air.Inst.Ref) bool { .arg, .field_opaque, .field_struct, + .field_tuple, .field_array, .add, .sub, @@ -739,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/src/main.zig b/src/main.zig index ebbe1e8800327ce7b1a243bec9b8b4780c79dd1d..b94445681e3b7951c67522860be9ecd2d134e635 100644 --- a/src/main.zig +++ b/src/main.zig @@ -4,9 +4,18 @@ //! for example, test the `core` module's compilation features without //! needing to compile the formatter. -const have_debug_features = builtin.mode == .Debug; const config = @import("config"); +const arg_config: Cli = .{ + .options = .{ + .{ .short = 'i', .name = "--dump-ir", .desc = "Dump intermediate representations" }, + .{ .name = "--dump-state", .desc = "Dump compiler state to JSON", .kind = .string }, + } ++ Cli.Option.optional(config.have_embedded_quickjs, &.{ + .{ .name = "--run", .desc = "Execute with QuickJS" }, + }), + .positionals = .single, +}; + pub fn main() !u8 { try tracer.init(); defer tracer.deinit(); @@ -24,8 +33,7 @@ pub fn main() !u8 { }){} else std.heap.ArenaAllocator.init(std.heap.page_allocator); - var no_leak_checks = false; - defer _ = if (use_gpa and !no_leak_checks) gpa.deinit(); + defer _ = if (use_gpa) gpa.deinit(); const alloc = gpa.allocator(); // Current working directory is required for CLI resolution. @@ -33,95 +41,17 @@ pub fn main() !u8 { const cwd = try std.posix.getcwd(&cwd_buf); // Command line argument parsing. - var maybe_file_path: ?[]const u8 = null; - defer if (maybe_file_path) |path| alloc.free(path); - var exe_name: ?[]const u8 = null; - defer if (exe_name) |path| alloc.free(path); - var dump_ir = false; - var dump_state: ?[]const u8 = null; - var run = false; - var has_errors = false; - { - var args = try std.process.argsWithAllocator(alloc); - defer args.deinit(); - - exe_name = try alloc.dupe(u8, std.fs.path.basename(args.next() orelse "cli")); - - while (args.next()) |arg| { - if (std.mem.startsWith(u8, arg, "-")) { - if (have_debug_features) { - if (std.mem.eql(u8, arg, "--report-trace")) { - core.Report.debug_show_report_trace = true; - continue; - } - if (std.mem.eql(u8, arg, "--no-leak-checks")) { - no_leak_checks = true; - continue; - } - } - - if (std.mem.eql(u8, arg, "--dump-ir")) { - dump_ir = true; - continue; - } - - if (std.mem.startsWith(u8, arg, "--dump-state=")) { - dump_state = arg["--dump-state=".len..]; - continue; - } - - if (config.have_embedded_quickjs and std.mem.eql(u8, arg, "--run")) { - run = true; - continue; - } - - std.debug.print("error: unknown flag '{s}'\n", .{arg}); - has_errors = true; - break; - } - - if (maybe_file_path == null) { - maybe_file_path = try std.fs.path.resolve(alloc, &.{ cwd, arg }); - } else { - std.debug.print("error: multiple files specified\n", .{}); - maybe_file_path = null; - break; - } - } - } - - if (maybe_file_path == null) has_errors = true; - if (has_errors) { - std.debug.print("Usage: {s} [flags] \n", .{exe_name.?}); - { - std.debug.print( - // | 60 chars wrap point| - \\ - \\Flags: - \\ --dump-ir Dump intermediate representations - \\ --dump-state= Dump compiler state to JSON - \\ - , .{}); - } - if (config.have_embedded_quickjs) { - std.debug.print( - \\ --run Run program using QuickJS - \\ - , .{}); - } - if (have_debug_features) { - std.debug.print( - \\ - \\Debug Mode Flags: - \\ --report-trace Print stack traces per error - \\ --no-leak-checks - \\ - , .{}); - } - std.debug.print("\nLanguage SDK v" ++ config.version_string ++ "\n", .{}); - return 1; - } - const file_path = maybe_file_path.?; + 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, &.{ + cwd, + opts.positionals orelse + arg_config.showHelpAndExit(program_name), + }); + defer alloc.free(file_path); // Execute compiler { @@ -130,13 +60,13 @@ pub fn main() !u8 { var comp = try core.Comp.init(.{ .gpa = tracer.tracingAllocator(&alloc), .options = .{ - .dump_ir = dump_ir, + .dump_ir = opts.dump_ir orelse false, }, .backend = .{ .js = &js_backend }, }); defer comp.deinit(); - defer if (dump_state) |path| { + defer if (opts.dump_state) |path| { dumpState(&comp, path) catch |err| { std.debug.print("Failed to dump compilation state: {}", .{err}); }; @@ -173,7 +103,7 @@ pub fn main() !u8 { else => return err, }; - if (config.have_embedded_quickjs and run) { + if (config.have_embedded_quickjs and (opts.run orelse false)) { // Execute with QuickJS var out: std.ArrayListUnmanaged(u8) = .{}; defer out.deinit(alloc); @@ -244,6 +174,7 @@ pub const tracer_impl = @field(tracer, @tagName(@import("config").tracing_mode)) const std = @import("std"); const builtin = @import("builtin"); +const Cli = @import("Cli.zig"); fn dumpState(comp: *const Comp, dump_path: []const u8) !void { const file = try std.fs.cwd().createFile(dump_path, .{}); diff --git a/test/behavior/tuple b/test/behavior/tuple new file mode 100644 index 0000000000000000000000000000000000000000..088915e7301707580c30f035b94ae81f2a5dd980 --- /dev/null +++ b/test/behavior/tuple @@ -0,0 +1,15 @@ +let assertEqual = @extern("assertEqual"); + +fn hello() -> tuple (int, float) { + let x: tuple (int, float) = (5, 2.5); + x +} + +fn main { + let x = hello(); + assertEqual(x.0, i(5)); + assertEqual(x.1, f(2.5)); +} + +fn f(a: float) -> float { a } +fn i(a: int) -> int { a } -- 2.54.0