diff --git a/CHANGELOG.md b/CHANGELOG.md index 31b28cbbafd4fdcb5d3f7a67eddce6d279a8d0b4..e274cb7de0b7170ca347e7439a3d2e78e987c3f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,12 @@ ## 0.1.4 +- feat: implement string identifiers +- feat: implement basic struct types +- feat: include a `README.html` alongside compiler with documentation and code samples. - fix(sema): miscompilation using left side of all binary expression for both sides - fix(sema): handle recursive function calls - fix(sema): quit block analysis when hitting `noreturn` +- fix(sema): binary type resolution with opaque and numeric literals no longer crash ## 0.1.3 diff --git a/README.md b/README.md index c20ab9899951fb38287813d80615e9aa8c9b67f1..f2420e064ea4a352c3a67741939a3cdd17bafcad 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,20 @@ # New Language Project -This is a high-level, general purpose programming language. It is a mix of Rust, Zig, and JavaScript's great points. +This is a high-level, general purpose programming language. It is a mix of Rust, Zig, and +JavaScript's great points. -For a guide on how to use the language, please see the Language Reference document. +For a guide on how to use the language, please see the Language Manual included in the binary +distribution. (TODO: host a rendered version) ## Building -The primary dependency of the language SDK is a Zig compiler. If one is not available, the `./build` and `.\build.cmd` scripts can substitute for running `zig build` on POSIX and Windows systems, respectively. With a Zig compiler, a release build can be produced in a single command: +The primary dependency of the language SDK is a Zig compiler. If one is not available, the +`./build` and `.\build.cmd` scripts can substitute for running `zig build` on POSIX and Windows +systems, respectively. With a Zig compiler, a release build can be produced in a single command: zig build --release To run the standard packaging procedure, which builds the SDK for all officially supported targets: zig build package + diff --git a/build.zig b/build.zig index a06d4d03ca4118a959f2e2ab4f4b8eafffa66ed0..7c5e8a2baf4491cea83bfe526666369a27f0ef39 100644 --- a/build.zig +++ b/build.zig @@ -34,8 +34,14 @@ pub fn build(b: *std.Build) void { "Enable tracing", ) orelse .none; - const native_debug = buildWithTarget(b, native_target, .Debug, .none); - const main = buildWithTarget(b, target, optimize, tracing); + const no_llvm = b.option( + bool, + "no_llvm", + "Experiment with the Zig self-hosted backends", + ) orelse false; + + const native_debug = buildWithTarget(b, native_target, .Debug, .none, no_llvm); + const main = buildWithTarget(b, target, optimize, tracing, no_llvm); b.installArtifact(main.compiler); @@ -46,7 +52,6 @@ pub fn build(b: *std.Build) void { const artifact, const name = entry; const run_cmd = b.addRunArtifact(artifact); - run_cmd.step.dependOn(b.getInstallStep()); if (b.args) |args| { run_cmd.addArgs(args); } @@ -128,7 +133,7 @@ pub fn build(b: *std.Build) void { }, ) catch @panic("OOM"); - const artifacts = buildWithTarget(b, cross_target, .ReleaseSafe, .none); + const artifacts = buildWithTarget(b, cross_target, .ReleaseSafe, .none, no_llvm); inline for (.{ .{ artifacts.compiler, "compiler" }, @@ -168,8 +173,11 @@ fn buildWithTarget( target: Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, tracing: TracingMode, + no_llvm: bool, ) Artifacts { const dep_opts = .{ .target = target, .optimize = optimize }; + const use_llvm = if (no_llvm) false else null; + const use_lld = if (no_llvm) false else null; // Dependencies const zg = b.dependency("zg", dep_opts); @@ -195,6 +203,8 @@ fn buildWithTarget( .root_source_file = b.path("src/core/mod.zig"), .target = target, .optimize = optimize, + .use_lld = use_lld, + .use_llvm = use_llvm, }); core_tests.root_module.addImport("build_options", opts_mod); core_tests.root_module.addImport("tracer", tracer.module("tracer")); @@ -206,7 +216,8 @@ fn buildWithTarget( .root_source_file = b.path("src/core/main.zig"), .target = target, .optimize = optimize, - .link_libc = true, + .use_lld = use_lld, + .use_llvm = use_llvm, }); compiler_exe.root_module.addImport("build_options", opts_mod); compiler_exe.root_module.addImport("core", core); @@ -218,7 +229,8 @@ fn buildWithTarget( .root_source_file = b.path("src/lsp/lsp.zig"), .target = target, .optimize = optimize, - .link_libc = true, + .use_lld = use_lld, + .use_llvm = use_llvm, }); lsp_exe.root_module.addImport("build_options", opts_mod); lsp_exe.root_module.addImport("core", core); diff --git a/doc/architecture.md b/doc/architecture.md index ab7b3b8b903e7351bfd972ba0ec3804ad29c9256..1b76ec34d5a4442f4c1cb3a549fe2a7df6afafc3 100644 --- a/doc/architecture.md +++ b/doc/architecture.md @@ -1,7 +1,13 @@ -# compiler architecture +# Architecture This document will outline the code structure of the language's SDK as well as the compilation pipeline architecture. The major components of the language are: -- [**Core**][core] -- [**Standard Library**][std] -- [**Language Server**][lsp] +- [**Core**][core] (`src/core`) +- [**SDK Manual Generator**][manual] (`src/manual`) + +Keep in mind that code is the best documentation for specific details, and this document aims to give an overall explanation of the high level process. The entry point to each component is `mod.zig`. + +General code style is to sort the file's contents by importance. Most files will start with an overview comment, followed by the root struct fields, and then important companion types and functions, then helpers. Imports and tests are not as important to a reader, so those are placed at the end of the file. + +An example of this organization is `src/core/Tokenizer.zig`, which opens with the tokenizer state, and then `pub fn tokenize` to bring attention to the flow of tokenization. The `Token` struct is directly after this, since it is important, but not as important as visualizing *how* the tokenizer works. + diff --git a/doc/language-manual.md b/doc/language-manual.md index 6004e2c0b4a59004c45413fd53d7b7da5123e981..7ba4ed14cb4f7d4c96bb6823c8507c5867504919 100644 --- a/doc/language-manual.md +++ b/doc/language-manual.md @@ -158,3 +158,43 @@ fn currentYear() -> int { print(currentYear()); ``` + +## Structs + +To group data together, a `struct` can be used. A struct contains zero or one fields of runtime-compatible types. + +```lang:run +let print = @js("console").log; + +struct Position { + x: int, + y: int, +} + +let center = Position{ x: 0, y: -2 }; +print(center.x, center.y); + +fn addPositions(a: Position, b: Position) -> Position { + // To allow less verbose code, the struct type can be inferred with `.` + // TODO: that is not implemented ;-; + Position{ x: a.x + b.x, y: a.y + b.y } +} + +let added = addPositions( + Position{ x: 1, y: -4 }, + Position{ x: 5, y: 8 }, +); +print("Add result:", added.x, added.y); +``` + +To allow optimizations, `struct`s by default do not have a defined representation in JavaScript. This means passing the struct to an opaque function is not allowed. + +```lang:build +struct Person { + name: string, + age: int, +} + +let print = @js("console").log; +print(Person{ name: "hello", age: 42 }); +``` diff --git a/src/core/Air.Dump.zig b/src/core/Air.Dump.zig index cc8751aa6a80d2a362508a53d13c1ab497ef6a89..1823157e86248fd0f45d05a3ae95e20bf84059c5 100644 --- a/src/core/Air.Dump.zig +++ b/src/core/Air.Dump.zig @@ -116,8 +116,8 @@ pub const State = struct { try s.dumpValue(w, memory.ty.value()); try w.writeAll(")"); }, - .field => { - const field: Air.Field = air.instData(.field, inst); + .field_opaque => { + const field: Air.FieldOpaque = air.instData(.field_opaque, inst); try w.writeAll("("); try s.dumpRef(w, field.base); try w.writeAll(", "); @@ -126,6 +126,22 @@ pub const State = struct { try tty.setColor(w, .reset); try w.writeAll(")"); }, + .field_struct => { + const field: Air.FieldStruct = air.instData(.field_struct, inst); + try w.writeAll("("); + try s.dumpInstRef(w, field.base); + try w.writeAll(", "); + try tty.setColor(w, .magenta); + try w.print("{s}", .{ + air + .typeOfIndex(field.base, s.ip) + .toStructType(s.ip).? + .field_keys[field.field_index] + .toSlice(s.ip), + }); + try tty.setColor(w, .reset); + try w.writeAll(")"); + }, inline .call, .new => |t| { const call: Air.Call = air.instData(t, inst); const call_data = call.call_data.get(air); @@ -152,6 +168,23 @@ pub const State = struct { try s.newline(w); try w.writeAll("}"); }, + .struct_init => { + const struct_init: Air.StructInit = air.instData(.struct_init, inst); + const struct_type = struct_init.ty.toStructType(s.ip).?; + + try w.writeAll("("); + try s.dumpValue(w, struct_init.ty.value()); + try w.writeAll(", {"); + s.indent += 1; + for (struct_type.field_keys, struct_init.valuesPtr(air)) |key, ref| { + try s.newline(w); + try w.print("{}: ", .{key.fmt(s.ip)}); + try s.dumpRef(w, ref); + } + s.indent -= 1; + try s.newline(w); + try w.writeAll("})"); + }, } } @@ -188,14 +221,14 @@ pub const State = struct { try w.print("{s}", .{@tagName(t)}); try tty.setColor(w, .reset); }, - .type_function => |ty| { + .function_type => |ty| { try tty.setColor(w, .blue); try w.writeAll("fn("); try tty.setColor(w, .reset); for (ty.param_types, 0..) |param, i| { if (i != 0) { try tty.setColor(w, .blue); - try w.writeAll(","); + try w.writeAll(", "); try tty.setColor(w, .reset); } try s.dumpValue(w, param.value()); @@ -245,6 +278,65 @@ pub const State = struct { try w.writeAll(">"); try tty.setColor(w, .reset); }, + .struct_type => |struct_type| { + try tty.setColor(w, .bright_magenta); + try w.writeAll("struct"); // TODO: struct name + try tty.setColor(w, .reset); + try w.writeAll(" {"); + + if (struct_type.field_keys.len > 0) + try w.writeAll(" "); + + for ( + struct_type.field_keys, + struct_type.field_types, + 0.., + ) |key, field_type, i| { + if (i != 0) + try w.writeAll(", "); + try w.writeAll(key.toSlice(s.ip)); + try w.writeAll(": "); + try s.dumpValue(w, field_type.value()); + } + + if (struct_type.field_keys.len > 0) + try w.writeAll(" "); + + try w.writeAll("}"); + }, + .struct_value => |struct_value| { + try tty.setColor(w, .bright_black); + try w.writeAll("<"); + try tty.setColor(w, .bright_magenta); + try w.writeAll("struct"); // TODO: struct name + try tty.setColor(w, .bright_black); + try w.writeAll(", "); + try tty.setColor(w, .reset); + try w.writeAll("{"); + + if (struct_value.field_values.len > 0) + try w.writeAll(" "); + + for ( + struct_value.ty.toStructType(s.ip).?.field_keys, + struct_value.field_values, + 0.., + ) |key, value, i| { + if (i != 0) + try w.writeAll(", "); + try w.writeAll(key.toSlice(s.ip)); + try w.writeAll(": "); + try s.dumpValue(w, value); + } + + if (struct_value.field_values.len > 0) + try w.writeAll(" "); + + try w.writeAll("}"); + try tty.setColor(w, .bright_black); + try w.writeAll(">"); + try tty.setColor(w, .reset); + }, } } diff --git a/src/core/Air.zig b/src/core/Air.zig index 89aaef766e0b02ff44d61e332553be2ae3ec9ea6..ac000eecad53e091fb348022e3657e17282b537c 100644 --- a/src/core/Air.zig +++ b/src/core/Air.zig @@ -20,11 +20,15 @@ pub const Inst = struct { @"break", @"if", arg, - field, memory, store, + struct_init, + + field_opaque, + field_struct, + // bin add, sub, @@ -48,9 +52,11 @@ pub const Inst = struct { .@"if" => If, .call, .new => Call, .arg => Arg, - .field => Field, .memory => Memory, .store => Store, + .struct_init => StructInit, + .field_opaque => FieldOpaque, + .field_struct => FieldStruct, .add, .sub, .mul, @@ -83,14 +89,26 @@ pub const Inst = struct { }; } - pub fn toOptional(ref: ?Ref) Optional { - return if (ref) |r| @enumFromInt(@as(u32, @bitCast(r))) else .none; + pub fn value(ref: Ref) ?Value { + return if (ref.kind == .value) return @enumFromInt(ref.data) else null; + } + + pub fn index(ref: Ref) ?Value { + return if (ref.kind == .value) return @enumFromInt(ref.data) else null; + } + + pub fn toOptional(ref: Ref) Optional { + return @enumFromInt(@as(u32, @bitCast(ref))); } pub const Optional = enum(u32) { none = std.math.maxInt(u32), _, + pub fn init(ref: ?Ref) Optional { + return if (ref) |r| @enumFromInt(@as(u32, @bitCast(r))) else .none; + } + pub fn unwrap(opt: Optional) ?Ref { return if (opt != .none) @bitCast(@intFromEnum(opt)) else null; } @@ -104,6 +122,24 @@ pub const Inst = struct { pub fn eql(ref: Ref, other: Ref) bool { return @as(u32, @bitCast(ref)) == @as(u32, @bitCast(other)); } + + pub fn dump(ref: Ref, ip: *const InternPool) std.fmt.Formatter(dumpImpl) { + return .{ .data = .{ ref, ip } }; + } + + fn dumpImpl( + data: struct { Ref, *const InternPool }, + comptime fmt: []const u8, + _: std.fmt.FormatOptions, + w: anytype, + ) !void { + comptime assert(fmt.len == 0); + const ref, const ip = data; + switch (ref.decode()) { + .value => |v| try w.print("{}", .{v.dump(ip)}), + .index => |i| try w.print("#{d}", .{@intFromEnum(i)}), + } + } }; /// reference to another instruction in this air @@ -189,7 +225,7 @@ pub const Un = struct { unused: Unused = .zero, }; -pub const Field = struct { +pub const FieldOpaque = struct { base: Inst.Ref, field: InternPool.NullTerminatedString, }; @@ -224,6 +260,23 @@ pub const Memory = struct { ty: Type, }; +pub const StructInit = struct { + ty: Type, + field_values: u32, + + pub fn valuesPtr( + init: *const StructInit, + 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, +}; + pub fn deinit(air: *Air, gpa: std.mem.Allocator) void { defer air.* = undefined; air.insts.deinit(gpa); @@ -250,46 +303,70 @@ pub fn instPayload(air: *const Air, comptime tag: Inst.Tag, index: Inst.Index) t return air.instData(tag, index).extra_index.get(air); } -pub fn typeOf(air: *const Air, ref: Inst.Ref, ip: *InternPool) InternPool.Type { +pub fn typeOf(air: *const Air, ref: Inst.Ref, ip: *const InternPool) InternPool.Type { return switch (ref.decode()) { .value => |v| v.typeOf(ip), .index => |i| air.typeOfIndex(i, ip), }; } -pub fn typeOfIndex(air: *const Air, index: Inst.Index, ip: *InternPool) InternPool.Type { +pub fn typeOfIndex(air: *const Air, index: Inst.Index, ip: *const InternPool) InternPool.Type { return switch (air.instTag(index)) { + // These values are by definition, opaque .js_global_ref, .new => .@"opaque", - .block => { - const items = air.instData(.block, index).slice(air); - return air.typeOfIndex(items[items.len - 1], ip); - }, - - inline .add, .sub, .mul, .div => |t| air.typeOf(air.instData(t, index).lhs, ip), - + // These operations always produce known types .lt, .gt, .eq, .bool_not => .bool, + .@"break" => .noreturn, + .field_opaque => .@"opaque", + // For many instructions, the type is stored on it's data directly. This + // is avoided when possible to reduce the size of Air. + .memory => air.instData(.memory, index).ty, + .store => air.instData(.memory, air.instData(.store, index).memory).ty, + .@"if" => air.instData(.@"if", index).data.get(air).union_ty, + .struct_init => air.instData(.struct_init, index).ty, .arg => air.instData(.arg, index).type, + // A handful of binary operators are lowered to a form where both sides + // are the same type, so the type of the left side can be used. + inline .add, + .sub, + .mul, + .div, + => |t| { + const left = air.typeOf(air.instData(t, index).lhs, ip); + if (std.debug.runtime_safety) { + assert(left == air.typeOf(air.instData(t, index).rhs, ip)); + } + return left; + }, + // Negate follows a similar pattern to the above .negate => air.typeOf(air.instData(.negate, index).operand, ip), - .@"break" => .noreturn, - - // todo: - .field => .@"opaque", + // TODO: taking the last instruction is likely wrong. + // The last instruction is going to be a .break and be of type .noreturn + .block => { + const items = air.instData(.block, index).slice(air); + assert(items.len > 0); // empty blocks should always be `Value.void` + return air.typeOfIndex(items[items.len - 1], ip); + }, + // Calls return the return type of the operand. .call => { const base_type = air.typeOf(air.instData(.call, index).base, ip); if (base_type == .@"opaque") return .@"opaque"; - return base_type.value().toKey(ip).type_function.return_type; + return base_type.toFunctionPrototype(ip).?.return_type; }, - .memory => air.instData(.memory, index).ty, - - .store => air.instData(.memory, air.instData(.store, index).memory).ty, - - .@"if" => air.instData(.@"if", index).data.get(air).union_ty, + // Field accesses on structs are special-cased by storing the field + // index instead of the name string. This avoids creating hashmaps. + .field_struct => { + const inst = air.instData(.field_struct, index); + const base_type = air.typeOfIndex(inst.base, ip) + .toStructType(ip) orelse unreachable; // not a struct + return base_type.field_types[inst.field_index]; + }, }; } diff --git a/src/core/Ast.Dump.zig b/src/core/Ast.Dump.zig index 33186d4e05d9b4ffaa86a9542af1b0698d9ef82c..22bfacf4e744225ba2538eca3cb58468971310b9 100644 --- a/src/core/Ast.Dump.zig +++ b/src/core/Ast.Dump.zig @@ -81,33 +81,41 @@ fn dumpNode(writer: anytype, state: *State, node: NodeIndex) @TypeOf(writer).Err } try tty.setColor(writer, .reset); }, - .fn_decl => { - try writer.writeAll(" "); - const fn_decl = state.ast.fullFnDecl(node); - if (fn_decl.public) { - try writer.writeAll("[pub] "); - } - try tty.setColor(writer, .magenta); - try writer.writeAll(fn_decl.id); - try tty.setColor(writer, .reset); - try writer.writeAll(" {"); - { - state.indent += 1; - defer state.indent -= 1; - try state.newline(writer); - try writer.writeAll("params:"); - try dumpNodeList(writer, state, fn_decl.params, '[', ']'); - if (fn_decl.return_type) |return_type| { + .function => switch (tag) { + else => unreachable, + inline .fn_decl, .fn_expr => |comptime_tag| { + const fn_decl = if (comptime_tag == .fn_decl) + state.ast.fullFnDecl(node) + else + state.ast.fullFnExpr(node); + if (comptime_tag == .fn_decl) { + try writer.writeAll(" "); + if (fn_decl.public) { + try writer.writeAll("[pub] "); + } + try tty.setColor(writer, .magenta); + try writer.writeAll(fn_decl.id); + try tty.setColor(writer, .reset); + } + try writer.writeAll(" {"); + { + state.indent += 1; + defer state.indent -= 1; + try state.newline(writer); + try writer.writeAll("params:"); + try dumpNodeList(writer, state, fn_decl.params, '[', ']'); + if (fn_decl.return_type) |return_type| { + try state.newline(writer); + try writer.writeAll("return_type: "); + try dumpNode(writer, state, return_type); + } try state.newline(writer); - try writer.writeAll("return_type: "); - try dumpNode(writer, state, return_type); + try writer.writeAll("body: "); + try dumpNode(writer, state, fn_decl.body); } try state.newline(writer); - try writer.writeAll("body: "); - try dumpNode(writer, state, fn_decl.body); - } - try state.newline(writer); - try writer.writeAll("}"); + try writer.writeAll("}"); + }, }, .list => { const list = data.list.slice(state.ast); @@ -115,6 +123,7 @@ fn dumpNode(writer: anytype, state: *State, node: NodeIndex) @TypeOf(writer).Err .root, .block_void, .block_implicit_return, + .struct_decl, => try dumpNodeList(writer, state, list, '{', '}'), .builtin_call, => try dumpNodeList(writer, state, list, '(', ')'), @@ -155,8 +164,10 @@ fn dumpNode(writer: anytype, state: *State, node: NodeIndex) @TypeOf(writer).Err try tty.setColor(writer, .magenta); try writer.writeAll(param_decl.id); try tty.setColor(writer, .reset); - try writer.writeAll(": "); - try dumpNode(writer, state, param_decl.type); + if (param_decl.type) |ty| { + try writer.writeAll(": "); + try dumpNode(writer, state, ty); + } }, .bin => { try writer.writeAll(" ("); @@ -244,6 +255,25 @@ fn dumpNode(writer: anytype, state: *State, node: NodeIndex) @TypeOf(writer).Err try dumpNodeList(writer, state, arg_list, '[', ']'); try writer.writeAll(")"); }, + .struct_init => { + const base = data.struct_init.base; + const extra = state.ast.extraData(data.struct_init.extra, Ast.SubList); + const fields = extra.slice(state.ast); + + try writer.writeAll("("); + try writer.writeAll("base: "); + if (base.unwrap()) |b| { + try dumpNode(writer, state, b); + } else { + try tty.setColor(writer, .blue); + try writer.writeAll(""); + try tty.setColor(writer, .reset); + } + try writer.writeAll(", "); + try writer.writeAll("fields:"); + try dumpNodeList(writer, state, fields, '[', ']'); + try writer.writeAll(")"); + }, .@"if" => { const branch = data.@"if"; const if_data = state.ast.extraData(branch.extra, Ast.If); diff --git a/src/core/Ast.zig b/src/core/Ast.zig index 730ba554c5c6e3355e3a83353b9511b9bf77adf8..7a7116ca29b77ad1ba337e8468e6195454883ce6 100644 --- a/src/core/Ast.zig +++ b/src/core/Ast.zig @@ -75,6 +75,10 @@ pub const Node = struct { /// `fn foo(x: i32) -> i32 { rhs }` /// Params and return type are stored in extra data fn_decl, + /// Function expression + /// `fn (x, y) { x + y }` + /// Params and return type are stored in extra data + fn_expr, /// 'name: type' declaration /// Main token is the identifier param_decl, @@ -208,6 +212,12 @@ pub const Node = struct { pure, /// `assert expr`. main_token is the operator. assert, + /// Struct Declaration. main_token is the id if set + struct_decl, + /// Struct Field. + struct_field, + /// Struct Init + struct_init, pub fn isAssignBin(op: Tag) bool { return switch (op) { @@ -264,71 +274,77 @@ pub const Node = struct { }; pub const data_tags = std.enums.directEnumArray(Tag, Data.FieldEnum, 0, .{ - .root = .list, - .block_void = .list, - .block_implicit_return = .list, - .var_decl = .var_decl, - .fn_decl = .fn_decl, - .param_decl = .param_decl, - .id = .none, - .number_int = .none, - .number_float = .none, - .string = .none, - .multiline_string = .list, - .call = .call, - .builtin_call = .list, + // sort-lines:start + .@"and" = .bin, + .@"break" = .@"break", + .@"comptime" = .un, .@"if" = .@"if", - .field = .field, - .index = .bin, + .@"or" = .bin, .@"return" = .@"return", - .@"break" = .@"break", - .group = .un, .add = .bin, - .sub = .bin, - .mul = .bin, - .div = .bin, - .rem = .bin, - .pow = .bin, - .@"and" = .bin, - .@"or" = .bin, + .add_assign = .bin, + .and_assign = .bin, + .assert = .un, + .assign = .bin, .bit_and = .bin, + .bit_and_assign = .bin, + .bit_not = .un, .bit_or = .bin, + .bit_or_assign = .bin, .bit_xor = .bin, - .shl = .bin, - .shr = .bin, + .bit_xor_assign = .bin, + .block_implicit_return = .list, + .block_void = .list, + .bool_not = .un, + .builtin_call = .list, + .call = .call, + .div = .bin, + .div_assign = .bin, .eq = .bin, - .ne = .bin, - .lt = .bin, - .le = .bin, - .gt = .bin, + .field = .field, + .fn_decl = .function, + .fn_expr = .function, .ge = .bin, - .assign = .bin, - .add_assign = .bin, - .sub_assign = .bin, + .group = .un, + .gt = .bin, + .id = .none, + .index = .bin, + .label = .label, + .le = .bin, + .lt = .bin, + .mul = .bin, .mul_assign = .bin, - .div_assign = .bin, - .rem_assign = .bin, - .pow_assign = .bin, - .and_assign = .bin, + .multiline_string = .list, + .ne = .bin, + .negate = .un, + .number_float = .none, + .number_int = .none, .or_assign = .bin, - .bit_and_assign = .bin, - .bit_or_assign = .bin, - .bit_xor_assign = .bin, + .param_decl = .param_decl, + .pow = .bin, + .pow_assign = .bin, + .pure = .un, + .rem = .bin, + .rem_assign = .bin, + .root = .list, + .shl = .bin, .shl_assign = .bin, + .shr = .bin, .shr_assign = .bin, - .bool_not = .un, - .negate = .un, - .bit_not = .un, - .@"comptime" = .un, - .pure = .un, - .assert = .un, - .label = .label, + .string = .none, + .struct_decl = .list, + .struct_field = .un, + .struct_init = .struct_init, + .sub = .bin, + .sub_assign = .bin, + .var_decl = .var_decl, + // sort-lines:end }); pub const Data = union { none: struct { Unused = .zero, Unused = .zero }, - fn_decl: struct { - // A `FnDecl` + function: struct { + /// A `Fn` extra: ExtraIndex, body: NodeIndex, }, @@ -339,7 +355,7 @@ pub const Node = struct { init: NodeIndex, }, param_decl: struct { - type: NodeIndex, + type: OptionalNodeIndex, unused: Unused = .zero, }, bin: struct { @@ -371,6 +387,13 @@ pub const Node = struct { /// A `Call` extra: ExtraIndex, }, + struct_init: struct { + /// .none => '.{ }' + /// else => 'base{ ... }' + base: OptionalNodeIndex, + /// A `SubList` + extra: ExtraIndex, + }, @"if": struct { cond: NodeIndex, /// A `If` @@ -457,12 +480,14 @@ pub const SubList = struct { start: u32, len: u32, + pub const empty: SubList = .{ .len = 0, .start = 0 }; + pub fn slice(self: SubList, ast: *const Ast) []const u32 { return ast.extra[self.start..][0..self.len]; } }; -pub const FnDecl = struct { +pub const Fn = struct { params: SubList, return_type: OptionalNodeIndex, }; @@ -489,6 +514,13 @@ pub const full = struct { body: NodeIndex, }; + pub const FnExpr = struct { + start_token: TokenIndex, + params: []const NodeIndex, + return_type: ?NodeIndex, + body: NodeIndex, + }; + pub const VarDecl = struct { start_token: TokenIndex, public: bool, @@ -498,11 +530,29 @@ pub const full = struct { init: NodeIndex, }; - pub const ParamDecl = struct { + pub const StructDecl = struct { + start_token: TokenIndex, + public: bool, + id: []const u8, + fields: []const NodeIndex, + }; + + pub const StructField = struct { start_token: TokenIndex, id: []const u8, type: NodeIndex, }; + + pub const StructInit = struct { + type: ?NodeIndex, + fields: []const NodeIndex, + }; + + pub const ParamDecl = struct { + start_token: TokenIndex, + id: []const u8, + type: ?NodeIndex, + }; }; pub fn deinit(ast: *Ast, gpa: std.mem.Allocator) void { @@ -531,10 +581,11 @@ pub fn fullFnDecl(ast: Ast, node: NodeIndex) full.FnDecl { const public = tok_tags[main_token -| 1] == .kw_pub; assert(tok_tags[main_token] == .kw_fn); assert(tok_tags[main_token + 1] == .id); + assert(tok_tags[main_token + 2] == .start_paren); const id = ast.tokenSlice(main_token + 1); - const data = ast.nodes.items(.data)[node].fn_decl; - const extra = ast.extraData(data.extra, FnDecl); + const data = ast.nodes.items(.data)[node].function; + const extra = ast.extraData(data.extra, Fn); return .{ .start_token = if (public) main_token - 1 else main_token, @@ -546,6 +597,46 @@ pub fn fullFnDecl(ast: Ast, node: NodeIndex) full.FnDecl { }; } +pub fn fullFnExpr(ast: Ast, node: NodeIndex) full.FnDecl { + const tok_tags = ast.tokens.items(.tag); + + const main_token = ast.nodes.items(.main_token)[node]; + const public = tok_tags[main_token -| 1] == .kw_pub; + assert(tok_tags[main_token] == .kw_fn); + const id = ast.tokenSlice(main_token + 1); + + const data = ast.nodes.items(.data)[node].function; + const extra = ast.extraData(data.extra, Fn); + + return .{ + .start_token = if (public) main_token - 1 else main_token, + .public = public, + .id = id, + .body = data.body, + .params = extra.params.slice(&ast), + .return_type = extra.return_type.unwrap(), + }; +} + +pub fn fullStructDecl(ast: Ast, node: NodeIndex) full.StructDecl { + const tok_tags = ast.tokens.items(.tag); + + const main_token = ast.nodes.items(.main_token)[node]; + const public = tok_tags[main_token -| 1] == .kw_pub; + assert(tok_tags[main_token] == .kw_struct); + assert(tok_tags[main_token + 1] == .id); + const id = ast.tokenSlice(main_token + 1); + + const data = ast.nodes.items(.data)[node].list; + + return .{ + .start_token = if (public) main_token - 1 else main_token, + .public = public, + .id = id, + .fields = data.slice(&ast), + }; +} + pub fn fullVarDecl(ast: Ast, node: NodeIndex) full.VarDecl { const tok_tags = ast.tokens.items(.tag); @@ -584,7 +675,30 @@ pub fn fullParamDecl(ast: Ast, node: NodeIndex) full.ParamDecl { return .{ .start_token = main_token, .id = id, - .type = data.type, + .type = data.type.unwrap(), + }; +} + +pub fn fullStructField(ast: Ast, node: NodeIndex) full.StructField { + const tok_tags = ast.tokens.items(.tag); + + const main_token = ast.nodes.items(.main_token)[node]; + assert(tok_tags[main_token] == .id); + const id = ast.tokenSlice(main_token); + const data = ast.nodes.items(.data)[node].un; + return .{ + .start_token = main_token, + .id = id, + .type = data.expr, + }; +} + +pub fn fullStructInit(ast: Ast, node: NodeIndex) full.StructInit { + assert(ast.nodeTag(node) == .struct_init); + const data = ast.nodeData(node).struct_init; + return .{ + .type = data.base.unwrap(), + .fields = ast.extraData(data.extra, SubList).slice(&ast), }; } diff --git a/src/core/Comp.zig b/src/core/Comp.zig index 4091fa32abe46a9fa7a7db8b91f7d3859d33ab33..edeba0893f87cf182eeab20e16122341cb64a4ff 100644 --- a/src/core/Comp.zig +++ b/src/core/Comp.zig @@ -1,4 +1,7 @@ -//! Compilation +//! Language Compilation Unit +//! +//! This is all of the state for a compilation, which starts at one or more URLs +//! (entry points), and will eventually emit one or more object files. /// General purpose allocator gpa: Allocator, @@ -7,13 +10,9 @@ intern_pool: InternPool = .{}, /// All files in the compilation files: std.StringArrayHashMapUnmanaged(*File) = .{}, -/// # of sema instructions in this compilation cycle -analyzed_instructions: usize = 0, - options: Options = .{}, -// TODO: consider computing this dynamically per-link -current_compile_decls: std.ArrayListUnmanaged(Decl.Index) = .{}, +backend: Backend, pub fn init(comp: *Comp) !void { try comp.intern_pool.init(comp.gpa); @@ -25,9 +24,47 @@ pub fn deinit(comp: *Comp) void { file.deinit(comp.gpa); } comp.files.deinit(comp.gpa); - comp.current_compile_decls.deinit(comp.gpa); + comp.backend.deinit(comp); } +/// This is the entry point for compilation. +/// Returns the Decl of the top-level code. +pub fn semaEntryPoint(comp: *Comp, file: *File) !Decl.Index { + var report: Report = undefined; + const teg = try file.getTeg(comp, &report); + + // TODO, move this elsewhere. we should print all errors and warnings at once + if (teg.report.warnings.len > 0) { + try teg.report.dumpToStdErr(comp); + } + + const ns = try comp.indexTegModule(file, .main_module_inst); + const decl_index = comp.intern_pool.namespacePtr(ns).lookupDirect(comp, .empty) orelse + unreachable; // Transform should always emit a top-level function + const decl = comp.intern_pool.declPtr(decl_index); + + var arena = std.heap.ArenaAllocator.init(comp.gpa); + defer arena.deinit(); + + var sema: Sema = .{ + .comp = comp, + .gpa = comp.gpa, + .code = teg, + .arena = arena.allocator(), + .ip = &comp.intern_pool, + .decl = decl, + .decl_index = decl_index, + .namespace_index = ns, + }; + try sema.init(); + defer sema.deinit(); + _ = try sema.analyzeDeclaration(); + try sema.finalize(); + + return decl_index; +} + +/// Add or retrieve a file to the compilation via it's file path. pub fn getFile(comp: *Comp, absolute_file_path: []const u8) !*File { assert(std.fs.path.isAbsolute(absolute_file_path)); const gop = try comp.files.getOrPut(comp.gpa, absolute_file_path); @@ -38,8 +75,8 @@ pub fn getFile(comp: *Comp, absolute_file_path: []const u8) !*File { file.* = .{ .file_path = absolute_file_path, .path_digest = null, - .root_module = .none, - .status = .never_loader, + .root_namespace = .none, + .status = .never_loaded, .source = null, .ast = null, .teg = null, @@ -53,13 +90,15 @@ pub const Source = struct { stat: Stat, }; +/// A file in the compilation and it's associated data structures. Ast and Teg +/// are not guaranteed to be loaded, even if it's `status` says `success`. pub const File = struct { /// TODO: change this to a URL file_path: []const u8, - root_module: Decl.Index.Optional, + root_namespace: Decl.Index.Optional, status: enum { - never_loader, + never_loaded, /// A failure occurred while loading the file. Retrying might succeed. retryable_failure, /// Errors during parse @@ -110,7 +149,7 @@ pub const File = struct { // used for error reporting. We need to keep the stat fields stale so that // transformFile can know to regenerate Teg. - const loaded_source = Source{ + const loaded_source: Source = .{ .code = source, .stat = .{ .size = stat.size, @@ -212,24 +251,34 @@ pub const File = struct { } }; +/// A declaration is a named value which also represents the smallest unit of +/// compilation/re-compilation. A declaration contains zero or more `Func` +/// objects, which represent the different callable values the declaration has. +/// +/// A special type of declaration is the top-level declaration, which has the +/// name "" (empty string). This is used to facilitate top-level side effects. pub const Decl = struct { name: InternPool.NullTerminatedString, - val: ?InternPool.Value, + + /// The Value that this declaration evaluates to. + raw_value: ?InternPool.Value, + /// The direct parent namespace of the Decl. In the case of the Decl /// corresponding to a file, this is the namespace of the struct, since /// there is no parent. src_namespace: Namespace.Index, - /// The AST node index of this declaration. - /// Must be recomputed when the corresponding source file is modified. + // /// The AST node index of this declaration. + // /// Must be recomputed when the corresponding source file is modified. // src_node: Ast.NodeIndex, - /// Line number corresponding to `src_node`. Stored separately so that source files - /// do not need to be loaded into memory in order to compute debug line numbers. - /// This value is absolute. + + // /// Line number corresponding to `src_node`. Stored separately so that source files + // /// do not need to be loaded into memory in order to compute debug line numbers. + // /// This value is absolute. // src_line: u32, - /// Index of the ZIR `declaration` instruction from which this `Decl` was created. - /// For the root `Decl` of a `File` and legacy anonymous decls, this is `.none`. - teg_decl_index: InternPool.TrackedInst.Index.Optional, + + /// Index of the Teg `declaration` instruction from which this `Decl` was created. + teg_decl_index: InternPool.TrackedInst.Index, /// Represents the "shallow" analysis status. For example, for decls that are functions, /// the function type is analyzed with this set to `in_progress`; however, the semantic @@ -242,20 +291,21 @@ pub const Decl = struct { /// Semantic analysis for this Decl is running right now. /// This state detects dependency loops. in_progress, - /// Semantic analysis for this Decl is running right now - /// But .value is valid - in_progress_prototype_available, + /// Semantic analysis for this Decl is running right now, and has + /// completed enough to reify the type of the result, but not the result + /// itself. + in_progress_type_available, /// The file corresponding to this Decl had a parse error or ZIR error. /// There will be a corresponding ErrorMsg in Zcu.failed_files. - file_failure, + // file_failure, /// This Decl might be OK but it depends on another one which did not /// successfully complete semantic analysis. - dependency_failure, + // dependency_failure, /// Semantic analysis failure. /// There will be a corresponding ErrorMsg in Zcu.failed_decls. - sema_failure, + // sema_failure, /// There will be a corresponding ErrorMsg in Zcu.failed_decls. - codegen_failure, + // codegen_failure, /// Semantic analysis and constant value codegen of this Decl has /// succeeded. However, the Decl may be outdated due to an in-progress /// update. Note that for a function, this does not mean codegen of the @@ -264,25 +314,35 @@ pub const Decl = struct { complete, }, - codegen: ?JsTree = null, + pub fn value(decl: *const Decl) InternPool.Value { + assert(decl.analysis == .complete); + return decl.raw_value orelse unreachable; + } - is_pub: bool, - - /// What kind of a declaration this is. - kind: Kind, + pub fn @"type"(decl: *const Decl, ip: *const InternPool) InternPool.Type { + return switch (decl.analysis) { + .unreferenced => unreachable, + .in_progress => unreachable, + .in_progress_prototype_available => decl.raw_value.?.toTypeUnsafe(ip), + .complete => decl.raw_value.?.typeOf(ip), + }; + } pub fn deinit(decl: *Decl, gpa: Allocator) void { + _ = gpa; // autofix defer decl.* = undefined; - if (decl.codegen) |*cg| cg.deinit(gpa); + // if (decl.codegen) |*cg| cg.deinit(gpa); } - pub const Kind = enum { - named, - }; - pub const Index = InternPool.DeclIndex; }; +pub const Func = struct {}; + +/// A namespace is a container for declarations. Currently the only form of +/// namespace that exists is file-scope. In the future, the `module` keyword +/// will create nested namespaces, and it is probable that `struct` and other +/// constructs will also create namespaces. pub const Namespace = struct { parent: Index.Optional, file_scope: *File, @@ -300,7 +360,12 @@ pub const Namespace = struct { return std.hash.uint32(@intFromEnum(decl.name)); } - pub fn eql(ctx: @This(), a_decl_index: Decl.Index, b_decl_index: Decl.Index, b_index: usize) bool { + pub fn eql( + ctx: @This(), + a_decl_index: Decl.Index, + b_decl_index: Decl.Index, + b_index: usize, + ) bool { _ = b_index; const a_decl = ctx.comp.intern_pool.declPtr(a_decl_index); const b_decl = ctx.comp.intern_pool.declPtr(b_decl_index); @@ -316,22 +381,30 @@ pub const Namespace = struct { return std.hash.uint32(@intFromEnum(s)); } - pub fn eql(self: @This(), a: InternPool.NullTerminatedString, b_decl_index: Decl.Index, b_index: usize) bool { + pub fn eql( + self: @This(), + a: InternPool.NullTerminatedString, + b_decl_index: Decl.Index, + b_index: usize, + ) bool { _ = b_index; return a == self.comp.intern_pool.declPtr(b_decl_index).name; } }; - pub fn lookupDirect(ns: *Namespace, comp: *Comp, name: InternPool.NullTerminatedString) ?Decl.Index { + pub fn lookupDirect( + ns: *Namespace, + comp: *Comp, + name: InternPool.NullTerminatedString, + ) ?Decl.Index { return ns.decls.getKeyAdapted(name, Namespace.DeclNameAdapter{ .comp = comp }); } pub const Index = InternPool.NamespaceIndex; }; -/// Index a module in a Teg. -/// When a File's Teg is loaded, we call this on the root module. -/// submodules are analyzed lazily. +/// Index a Teg's module scope as a namespace. This prepares `Decl` objects for +/// every declaration, but does not begin analysis of them. pub fn indexTegModule(comp: *Comp, file: *File, module_index: Teg.Inst.Index) !Namespace.Index { const teg = file.teg.?; @@ -351,12 +424,10 @@ pub fn indexTegModule(comp: *Comp, file: *File, module_index: Teg.Inst.Index) !N const tracked_inst = try comp.intern_pool.trackInst(comp.gpa, file, decl_index); const decl = try comp.intern_pool.createDecl(comp.gpa, .{ .name = name, - .val = null, + .raw_value = null, .src_namespace = ns, - .teg_decl_index = tracked_inst.toOptional(), + .teg_decl_index = tracked_inst, .analysis = .unreferenced, - .is_pub = false, // todo: - .kind = .named, }); try ns_ptr.decls.putNoClobberContext(comp.gpa, decl, void{}, .{ .comp = comp }); } @@ -364,42 +435,7 @@ pub fn indexTegModule(comp: *Comp, file: *File, module_index: Teg.Inst.Index) !N return ns; } -pub fn semaEntryPoint(comp: *Comp, file: *File) !void { - var report: Report = undefined; - const teg = try file.getTeg(comp, &report); - - // TODO, move this elsewhere. we should print all errors and warnings at once - if (teg.report.warnings.len > 0) { - try teg.report.dumpToStdErr(comp); - } - - const ns = try comp.indexTegModule(file, .main_module_inst); - const decl_index = comp.intern_pool.namespacePtr(ns).lookupDirect(comp, .empty) orelse - @panic("no top level code?"); - const decl = comp.intern_pool.declPtr(decl_index); - - assert(comp.current_compile_decls.items.len == 0); - - var arena = std.heap.ArenaAllocator.init(comp.gpa); - defer arena.deinit(); - - var sema: Sema = .{ - .comp = comp, - .gpa = comp.gpa, - .code = teg, - .arena = arena.allocator(), - .ip = &comp.intern_pool, - .decl = decl, - .decl_index = decl_index, - .namespace_index = ns, - }; - try sema.init(); - defer sema.deinit(); - _ = try sema.analyzeDeclaration(); - try sema.finalize(); -} - -pub fn codeGenDecl(c: *Comp, decl: *Decl, air: *const Air) !void { +pub fn codeGenDecl(c: *Comp, value: InternPool.Value, air: *const Air) !void { const t = tracer.trace(@src()); defer t.end(); @@ -407,20 +443,48 @@ pub fn codeGenDecl(c: *Comp, decl: *Decl, air: *const Air) !void { defer arena_state.deinit(); const arena = arena_state.allocator(); - var cg = try JsNodeGen.init(c.gpa, arena, &c.intern_pool, air); - errdefer cg.deinit(); - try cg.build(); - decl.codegen = try cg.finalize(); -} - -pub fn link(c: *Comp, writer: anytype) !void { - try JsLink.linkJsCode(c, c.current_compile_decls.items, writer); + _ = try c.backend.handleFunctionAir(c, value, air, arena); } const Options = struct { dump_ir: bool = false, }; +pub const Backend = union(enum) { + js: *@import("js/Js.zig"), + + pub fn deinit(backend: Backend, comp: *Comp) void { + backend.dispatch("deinit", .{comp}); + } + + pub const HandleFunctionAirError = error{OutOfMemory}; + + /// Once semantic analysis finishes analyzing a Function value, it hands + /// `Air` to the backend for code generation. Returned is an opaque integer + /// which is stored on the Function value. + pub fn handleFunctionAir( + backend: Backend, + comp: *Comp, + value: InternPool.Value, + air: *const Air, + arena: Allocator, + ) HandleFunctionAirError!u32 { + return backend.dispatch("handleFunctionAir", .{ comp, value, air, arena }); + } + + inline fn dispatch( + backend: Backend, + comptime name: []const u8, + args: anytype, + ) @typeInfo(@TypeOf(@field(Backend, name))).Fn.return_type.? { + return switch (backend) { + inline else => |ptr| @call(.auto, @field(@TypeOf(ptr.*), name), .{ptr} ++ args), + }; + } + + pub const Tag = std.meta.FieldEnum(Backend); +}; + const Comp = @This(); const std = @import("std"); @@ -437,9 +501,6 @@ const Teg = @import("Teg.zig"); const Cache = @import("Cache.zig"); const Sema = @import("Sema.zig"); const Transform = @import("Transform.zig"); -const JsNodeGen = @import("JsNodeGen.zig"); -const JsTree = @import("JsTree.zig"); -const JsLink = @import("JsLink.zig"); const Report = @import("Report.zig"); const Parse = @import("Parse.zig"); diff --git a/src/core/InternPool.zig b/src/core/InternPool.zig index ed4fa838cf0bfb8c37952c0db0f26f1eb95b9fe3..a62f1afb2c37477e300ab052b72e83d7dc6eada6 100644 --- a/src/core/InternPool.zig +++ b/src/core/InternPool.zig @@ -1,8 +1,21 @@ //! Large parts of this file are based on the Zig compiler's InternPool. //! https://github.com/ziglang/zig/blob/0.12.0/src/InternPool.zig +//! +//! All interned objects have both a Value and a Type. Type is a subset of +//! Value, since the language allows operating on types the same as values. +//! This data structure is intended to be serializable for incremental +//! compilation efforts. +//! +//! This data structure is self-contained, with the following exceptions: +//! * Comp.Namespace has a pointer to Comp.File -/// Maps `Key` to `Index`. `Key` objects are not stored anywhere; they are -/// constructed lazily. +/// Maps `Key` to `Value`. `Key` objects are not stored anywhere; they are +/// constructed lazily. The `Value` is also stored out of line in `.items`. The +/// ArrayHashMap functions as a list of hashes to allow fast lookups, and then +/// reading it's data out of `.items` and `.extra` via the index lookup. +/// +/// The preferred way to access these this data through `.get()` and the methods +/// on `Value` and `Type`. map: std.AutoArrayHashMapUnmanaged(void, void) = .{}, items: std.MultiArrayList(Item) = .{}, extra: ExtraStore = .{}, @@ -45,29 +58,6 @@ allocated_namespaces: std.SegmentedList(Comp.Namespace, 0) = .{}, /// Same pattern as with `decls_free_list`. namespaces_free_list: std.ArrayListUnmanaged(NamespaceIndex) = .{}, -/// How many items in the InternPool are statically known. -/// This is specified with an integer literal and a corresponding comptime -/// assert below to break an unfortunate and arguably incorrect dependency loop -/// when compiling. -pub const static_len = Teg.Inst.Index.static_len; -comptime { - // @compileLog(static_len, static_keys.len); - assert(static_len == static_keys.len); -} - -pub const Item = struct { - tag: Tag, - data: EncodedData, - - pub fn encode(comptime tag: Tag, data: tag.Data()) Item { - return .{ .tag = tag, .data = EncodedData.encode(tag, data) }; - } -}; - -pub const ExtraStore = dod.ExtraStore(.intern_pool, .@"var"); -pub const EncodedData = dod.EncodedData(Tag, u32); -pub const ExtraIndex = ExtraStore.Index; - pub const Tag = enum(u8) { /// This special tag represents a value that was removed from this pool via /// `InternPool.remove`. The item remains allocated to preserve indices, but @@ -76,10 +66,10 @@ pub const Tag = enum(u8) { removed, /// A function type - /// data is an extra index into a `FuncType` - type_function, + function_type, + /// A struct type + struct_type, /// A type that can be represented with only an enum tag. - /// data is SimpleType enum value. simple_type, // --- before this are types --- @@ -90,21 +80,18 @@ pub const Tag = enum(u8) { simple_value, /// A numeric literal that can be represented as a float. /// Integers greater than 2^53 known at compile time are currently not supported. - /// data is an extra index into a `Float` comptime_float, /// An int32 value - /// data is i32 int32, /// A float value - /// data is an extra index into a `Float` float, /// A function - /// data is an extra index into a `FuncDecl` func, /// A string - /// data is an extra index into a `String` string, + struct_value, + pub fn Data(comptime tag: Tag) type { return switch (tag) { .removed, @@ -128,8 +115,10 @@ pub const Tag = enum(u8) { => unreachable, // does not have a payload .float, .comptime_float => Float, .func => FuncDecl, - .type_function => TypeFunction, + .function_type => TypeFunction, .string => String.WithLen, + .struct_value => StructValue, + .struct_type => TypeStruct, }; } @@ -147,8 +136,23 @@ pub const Tag = enum(u8) { params_len: u32, return_type: Type, }; + + /// Trailing: + /// 0. field_key: NullTerminatedString. Repeated per field_len + /// 1. field_type: Type. Repeated per field_len + pub const TypeStruct = struct { + fields_len: u32, + }; + + /// Trailing: + /// 0. value: Value. Repeated ty.structInfo().field_len + pub const StructValue = struct { + /// The type of this field + ty: Type, + }; }; +/// This is an index into .items pub const Value = enum(u32) { pub const first_type: Value = .opaque_type; pub const last_type: Value = .noreturn_type; @@ -176,25 +180,26 @@ pub const Value = enum(u32) { none = std.math.maxInt(u32), _, - /// An array of `Index` existing within the `extra` array. + /// An array of `Value` existing within the `extra` array. /// This type exists to provide a struct with lifetime that is /// not invalidated when items are added to the `InternPool`. + /// TODO: Replace with ExtraStore.Slice(Value) pub const Slice = struct { start: u32, len: u32, - pub fn get(slice: Slice, ip: *const InternPool) []Value { + pub fn get(slice: Slice, ip: *const InternPool) []const Value { return @ptrCast(ip.extra.list.items[slice.start..][0..slice.len]); } }; - /// Used for a map of `Index` values to the index within a list of `Index` values. + /// Used for a map of `Value` values to the index within a list of `Value` values. const Adapter = struct { - indexes: []const Value, + values: []const Value, pub fn eql(ctx: @This(), a: Value, b_void: void, b_map_index: usize) bool { _ = b_void; - return a == ctx.indexes[b_map_index]; + return a == ctx.values[b_map_index]; } pub fn hash(ctx: @This(), a: Value) u32 { @@ -216,7 +221,7 @@ pub const Value = enum(u32) { } pub fn toKey(value: Value, ip: *const InternPool) Key { - return ip.indexToKey(value); + return ip.valueToKey(value); } pub fn dump(value: Value, ip: *const InternPool) std.fmt.Formatter(Value.formatValue) { @@ -240,8 +245,9 @@ pub const Value = enum(u32) { return switch (value.valueTag(ip)) { .removed => unreachable, // UAF - .type_function => .type, + .function_type => .type, .simple_type => .type, + .struct_type => .type, .simple_value => switch (value.valueData(ip, .simple_value)) { .true => .bool, @@ -254,6 +260,7 @@ pub const Value = enum(u32) { .float => .float, .string => .string, .func => value.toKey(ip).func.ty, + .struct_value => value.toKey(ip).struct_value.ty, }; } @@ -263,7 +270,8 @@ pub const Value = enum(u32) { return switch (value.valueTag(ip)) { .removed => unreachable, // UAF - .type_function, + .function_type, + .struct_type, .simple_type, => @enumFromInt(@intFromEnum(value)), @@ -276,7 +284,8 @@ pub const Value = enum(u32) { pub fn toTypeUnsafe(value: Value, proof_ip: *const InternPool) Type { if (std.debug.runtime_safety) { - return value.toType(proof_ip).?; + return value.toType(proof_ip) orelse + std.debug.panic("{} is not a type", .{value.dump(proof_ip)}); } return @enumFromInt(@intFromEnum(value)); } @@ -301,6 +310,8 @@ pub const Value = enum(u32) { } }; +/// Type is subset of Value which represents the values that have typeOf() == Value.type_type +/// It is a bitcast, so going to and from are only the cost of ensuring the Value is a Type pub const Type = enum(u32) { @"opaque" = @intFromEnum(Value.opaque_type), bool = @intFromEnum(Value.bool_type), @@ -315,7 +326,8 @@ pub const Type = enum(u32) { // Restricted version of `InternPool.Tag` pub const Tag = enum(u32) { - type_function = @intFromEnum(InternPool.Tag.type_function), + function_type = @intFromEnum(InternPool.Tag.function_type), + struct_type = @intFromEnum(InternPool.Tag.struct_type), simple_type = @intFromEnum(InternPool.Tag.simple_type), }; @@ -336,12 +348,18 @@ pub const Type = enum(u32) { } pub fn tag(ty: Type, ip: *const InternPool) Type.Tag { + if (ty == .void) return .simple_type; return @enumFromInt(@intFromEnum(ty.value().valueTag(ip))); } - pub fn toFunctionPrototype(ty: Type, ip: *const InternPool) ?Key.TypeFunction { - if (ty.tag(ip) != .type_function) return null; - return ip.indexToKey(ty.value()).type_function; + pub fn toFunctionPrototype(ty: Type, ip: *const InternPool) ?Key.FunctionType { + if (ty.tag(ip) != .function_type) return null; + return ip.valueToKey(ty.value()).function_type; + } + + pub fn toStructType(ty: Type, ip: *const InternPool) ?Key.StructType { + if (ty.tag(ip) != .struct_type) return null; + return ip.valueToKey(ty.value()).struct_type; } pub fn hasRuntimeRepresentation(ty: Type, ip: *const InternPool) bool { @@ -359,7 +377,8 @@ pub const Type = enum(u32) { => false, else => switch (ty.tag(ip)) { .simple_type => unreachable, // all simple types are already printed - .type_function => true, + .function_type => true, + .struct_type => true, }, }; } @@ -396,20 +415,46 @@ pub const Type = enum(u32) { switch (ty.tag(ip)) { .simple_type => unreachable, // all simple types are already printed - .type_function => { - const type_function = ty.toFunctionPrototype(ip).?; + .function_type => { + const function_type = ty.toFunctionPrototype(ip).?; try writer.writeAll("fn("); - for (type_function.param_types, 0..) |param, i| { + for (function_type.param_types, 0..) |param, i| { if (i != 0) try writer.writeAll(", "); try writer.print("{}", .{param.displayName(ip)}); } try writer.writeAll(") -> "); - try writer.print("{}", .{type_function.return_type.displayName(ip)}); + try writer.print("{}", .{function_type.return_type.displayName(ip)}); + }, + .struct_type => { + try writer.writeAll("[struct]"); }, } } }; +pub const Item = struct { + tag: Tag, + data: EncodedData, + + pub fn encode(comptime tag: Tag, data: tag.Data()) Item { + return .{ .tag = tag, .data = EncodedData.encode(tag, data) }; + } +}; + +pub const ExtraStore = dod.ExtraStore(.intern_pool, .@"var"); +pub const EncodedData = dod.EncodedData(Tag, u32); +pub const ExtraIndex = ExtraStore.Index; + +/// How many items in the InternPool are statically known. +/// This is specified with an integer literal and a corresponding comptime +/// assert below to break an unfortunate and arguably incorrect dependency loop +/// when compiling. +pub const static_len = Teg.Inst.Index.static_len; +comptime { + // @compileLog(static_len, static_keys.len); + assert(static_len == static_keys.len); +} + pub const static_keys = [_]Key{ SimpleType.@"opaque".key(), SimpleType.bool.key(), @@ -434,29 +479,27 @@ pub const static_keys = [_]Key{ pub const FuncAnalysis = packed struct(u32) { state: State, - _: u24 = 0, - - pub const State = enum(u8) { - /// This function has not yet undergone analysis, because we have not - /// seen a potential runtime call. It may be analyzed in future. + pub const State = enum(u32) { none, - /// Analysis for this function has been queued, but not yet completed. - queued, - /// This function intentionally only has ZIR generated because it is marked - /// inline, which means no runtime version of the function will be generated. - inline_only, - in_progress, - /// There will be a corresponding ErrorMsg in Module.failed_decls - sema_failure, - /// This function might be OK but it depends on another Decl which did not - /// successfully complete semantic analysis. - dependency_failure, - /// There will be a corresponding ErrorMsg in Module.failed_decls. - /// Indicates that semantic analysis succeeded, but code generation for - /// this function failed. - codegen_failure, - /// Semantic analysis and code generation of this function succeeded. - success, + // /// This function has not yet undergone analysis, because we have not + // /// seen a potential runtime call. It may be analyzed in future. none, + // /// Analysis for this function has been queued, but not yet completed. + // queued, + // /// This function intentionally only has ZIR generated because it is marked + // /// inline, which means no runtime version of the function will be generated. + // inline_only, + // in_progress, + // /// There will be a corresponding ErrorMsg in Module.failed_decls + // sema_failure, + // /// This function might be OK but it depends on another Decl which did not + // /// successfully complete semantic analysis. + // dependency_failure, + // /// There will be a corresponding ErrorMsg in Module.failed_decls. + // /// Indicates that semantic analysis succeeded, but code generation for + // /// this function failed. + // codegen_failure, + // /// Semantic analysis and code generation of this function succeeded. + // success, }; }; @@ -557,6 +600,7 @@ pub const NullTerminatedString = enum(u32) { string: NullTerminatedString, ip: *const InternPool, }; + fn format( data: FormatData, comptime specifier: []const u8, @@ -574,6 +618,15 @@ pub const NullTerminatedString = enum(u32) { pub fn fmt(self: NullTerminatedString, ip: *const InternPool) std.fmt.Formatter(format) { return .{ .data = .{ .string = self, .ip = ip } }; } + + pub const Slice = struct { + start: u32, + len: u32, + + pub fn get(slice: Slice, ip: *const InternPool) []NullTerminatedString { + return @ptrCast(ip.extra.list.items[slice.start..][0..slice.len]); + } + }; }; pub const TrackedInst = extern struct { @@ -721,14 +774,18 @@ const SimpleType = enum { }; pub const Key = union(enum) { + // Values comptime_float: f64, int32: i32, float: f64, string: []const u8, simple_value: SimpleValue, - simple_type: SimpleType, - type_function: TypeFunction, + struct_value: StructValue, func: Func, + // Types + simple_type: SimpleType, + function_type: FunctionType, + struct_type: StructType, const Adapter = struct { intern_pool: *const InternPool, @@ -736,7 +793,7 @@ pub const Key = union(enum) { pub fn eql(ctx: @This(), a: Key, b_void: void, b_map_index: usize) bool { _ = b_void; if (ctx.intern_pool.items.items(.tag)[b_map_index] == .removed) return false; - return ctx.intern_pool.indexToKey(@enumFromInt(b_map_index)).eql(a, ctx.intern_pool); + return ctx.intern_pool.valueToKey(@enumFromInt(b_map_index)).eql(a, ctx.intern_pool); } pub fn hash(ctx: @This(), a: Key) u32 { @@ -744,11 +801,11 @@ pub const Key = union(enum) { } }; - pub const TypeFunction = struct { + pub const FunctionType = struct { param_types: []Type, return_type: Type, - pub fn hash(self: TypeFunction, hasher: *Hash) void { + pub fn hash(self: FunctionType, hasher: *Hash) void { const t = tracer.trace(@src()); defer t.end(); @@ -758,12 +815,32 @@ pub const Key = union(enum) { std.hash.autoHash(hasher, self.return_type); } - pub fn eql(a: TypeFunction, b: TypeFunction) bool { + pub fn eql(a: FunctionType, b: FunctionType) bool { return std.mem.eql(Type, a.param_types, b.param_types) and a.return_type == b.return_type; } }; + pub const StructType = struct { + field_keys: []NullTerminatedString, + field_types: []const Type, + + pub fn hash(self: StructType, hasher: *Hash) void { + // TODO: remove this function. structural typing is not used + std.hash.autoHashStrat(hasher, self, .DeepRecursive); + } + }; + + pub const StructValue = struct { + ty: Type, + field_values: []const Value, + + pub fn hash(self: StructValue, hasher: *Hash) void { + std.hash.autoHash(hasher, self.ty); + std.hash.autoHashStrat(hasher, self.field_values, .Deep); + } + }; + pub const Func = struct { ty: Type, owner_decl: DeclIndex, @@ -781,10 +858,10 @@ pub const Key = union(enum) { } pub fn hash64(key: Key, ip: *const InternPool) u64 { + _ = ip; // autofix const t = tracer.trace(@src()); defer t.end(); - _ = ip; // autofix const asBytes = std.mem.asBytes; const KeyTag = @typeInfo(Key).Union.tag_type.?; const seed = @intFromEnum(@as(KeyTag, key)); @@ -797,7 +874,7 @@ pub const Key = union(enum) { .string, => |x| Hash.hash(seed, x), .float, .comptime_float => |x| Hash.hash(seed, asBytes(&Float.assertPure(x))), - .type_function => |x| { + .function_type => |x| { var hasher = Hash.init(seed); x.hash(&hasher); return hasher.final(); @@ -807,14 +884,24 @@ pub const Key = union(enum) { x.hash(&hasher); return hasher.final(); }, + .struct_type => |x| { + var hasher = Hash.init(seed); + x.hash(&hasher); + return hasher.final(); + }, + .struct_value => |x| { + var hasher = Hash.init(seed); + x.hash(&hasher); + return hasher.final(); + }, }; } pub fn eql(a: Key, b: Key, ip: *const InternPool) bool { + _ = ip; // autofix const t = tracer.trace(@src()); defer t.end(); - _ = ip; // autofix const KeyTag = @typeInfo(Key).Union.tag_type.?; const a_tag: KeyTag = a; const b_tag: KeyTag = b; @@ -828,7 +915,7 @@ pub const Key = union(enum) { => |x| std.mem.eql(u8, x, b.string), .float => |x| Float.assertPure(x) == Float.assertPure(b.float), .comptime_float => |x| Float.assertPure(x) == Float.assertPure(b.comptime_float), - .type_function => |x| x.eql(b.type_function), + .function_type => |x| x.eql(b.function_type), .func => |a_info| { const b_info = b.func; if (a_info.owner_decl != b_info.owner_decl) @@ -839,6 +926,14 @@ pub const Key = union(enum) { // TODO: zig has something extra here for inferred error sets return false; }, + .struct_type => { + return false; // structs are nomial + }, + .struct_value => |a_info| { + const b_info = b.struct_value; + return a_info.ty == b_info.ty and + std.mem.eql(Value, a_info.field_values, b_info.field_values); + }, }; } }; @@ -890,8 +985,8 @@ pub fn init(ip: *InternPool, gpa: Allocator) !void { if (std.debug.runtime_safety) { // Sanity check. - assert(ip.indexToKey(.bool_true).simple_value == .true); - assert(ip.indexToKey(.bool_false).simple_value == .false); + assert(ip.valueToKey(.bool_true).simple_value == .true); + assert(ip.valueToKey(.bool_false).simple_value == .false); } assert(ip.items.len == static_keys.len); @@ -944,22 +1039,20 @@ pub fn deinit(ip: *InternPool, gpa: Allocator) void { ip.* = undefined; } -pub fn indexToKey(ip: *const InternPool, index: Value) Key { +pub fn valueToKey(ip: *const InternPool, index: Value) Key { assert(index != .none); const item: Item = ip.items.get(@intFromEnum(index)); const data = item.data; return switch (item.tag) { .removed => unreachable, - .type_function => .{ - .type_function = ip.extraTypeFunction(data.decode(.type_function, item.tag)), - }, - .simple_type => .{ .simple_type = data.decode(.simple_type, item.tag) }, .simple_value => .{ .simple_value = data.decode(.simple_value, item.tag) }, .int32 => .{ .int32 = data.decode(.int32, item.tag) }, .float => .{ .float = data.decode(.float, item.tag).get(ip).decode() }, - .comptime_float => .{ .comptime_float = data.decode(.comptime_float, item.tag).get(ip).decode() }, + .comptime_float => .{ + .comptime_float = data.decode(.comptime_float, item.tag).get(ip).decode(), + }, .string => .{ .string = data.decode(.string, item.tag).get(ip).toSlice(ip) }, .func => { const func = data.decode(.func, item.tag).get(ip); @@ -969,6 +1062,29 @@ pub fn indexToKey(ip: *const InternPool, index: Value) Key { .teg_body_inst = func.teg_body_inst, } }; }, + .function_type => .{ + .function_type = ip.extraTypeFunction(data.decode(.function_type, item.tag)), + }, + .struct_type => { + const payload: Tag.TypeStruct, var trail = + data.decode(.struct_type, item.tag).getTrail(ip); + + return .{ .struct_type = .{ + .field_keys = trail.readNullTerminatedStringSlice(payload.fields_len).get(ip), + .field_types = trail.readTypeSlice(payload.fields_len).get(ip), + } }; + }, + .struct_value => { + const payload: Tag.StructValue, var trail = + data.decode(.struct_value, item.tag).getTrail(ip); + + return .{ .struct_value = .{ + .ty = payload.ty, + .field_values = trail.readValueSlice( + payload.ty.value().valueData(ip, .struct_type).get(ip).fields_len, + ).get(ip), + } }; + }, }; } @@ -1020,20 +1136,39 @@ pub fn get(ip: *InternPool, gpa: Allocator, key: Key) !Value { break :brk Item.encode(.func, extra); }, - .type_function => |f| brk: { + .function_type => |f| brk: { const extra, var trail = try ip.extra.appendTrailing(gpa, Tag.TypeFunction, .{ .params_len = @intCast(f.param_types.len), .return_type = f.return_type, }, f.param_types.len); defer trail.end(); trail.writeSlice(f.param_types); - break :brk Item.encode(.type_function, extra); + break :brk Item.encode(.function_type, extra); + }, + .struct_type => |s| brk: { + assert(s.field_keys.len == s.field_types.len); + const extra, var trail = try ip.extra.appendTrailing(gpa, Tag.TypeStruct, .{ + .fields_len = @intCast(s.field_keys.len), + }, s.field_keys.len * 2); + trail.writeSlice(s.field_keys); + trail.writeSlice(s.field_types); + + break :brk Item.encode(.struct_type, extra); + }, + .struct_value => |s| brk: { + assert(s.ty.value().toKey(ip).struct_type.field_keys.len == s.field_values.len); + const extra, var trail = try ip.extra.appendTrailing(gpa, Tag.StructValue, .{ + .ty = s.ty, + }, s.field_values.len); + trail.writeSlice(s.field_values); + break :brk Item.encode(.struct_value, extra); }, }; ip.items.appendAssumeCapacity(item); const index: Value = @enumFromInt(ip.items.len - 1); if (std.debug.runtime_safety) { - assert(ip.indexToKey(index).eql(key, ip)); + const key_added = ip.valueToKey(index); + assert(key == .struct_type or key_added.eql(key, ip)); } return index; } @@ -1041,11 +1176,11 @@ pub fn get(ip: *InternPool, gpa: Allocator, key: Key) !Value { fn extraTypeFunction( ip: *const InternPool, extra_index: ExtraIndex(Tag.TypeFunction), -) Key.TypeFunction { - const type_function, var trail = extra_index.getTrail(ip); - const param_types = trail.readTypeSlice(type_function.params_len); - const result: Key.TypeFunction = .{ - .return_type = type_function.return_type, +) 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, .param_types = param_types.get(ip), }; return result; @@ -1130,6 +1265,10 @@ pub fn namespacePtr(ip: *InternPool, index: NamespaceIndex) *Comp.Namespace { return ip.allocated_namespaces.at(@intFromEnum(index)); } +pub fn namespacePtrConst(ip: *const InternPool, index: NamespaceIndex) *const Comp.Namespace { + return ip.allocated_namespaces.at(@intFromEnum(index)); +} + pub fn createDecl( ip: *InternPool, gpa: Allocator, @@ -1222,7 +1361,7 @@ test InternPool { try testing.expectEqual(str, str2); const int1 = try ip.get(a, .{ .int32 = 21483 }); - try testing.expectEqual(@as(i32, 21483), ip.indexToKey(int1).int32); + try testing.expectEqual(@as(i32, 21483), ip.valueToKey(int1).int32); const int2 = try ip.get(a, .{ .int32 = 21483 }); try testing.expectEqual(int1, int2); } diff --git a/src/core/JsLink.zig b/src/core/JsLink.zig deleted file mode 100644 index 5a33ca3535dabf2b2a04b8450925ca189b172a3f..0000000000000000000000000000000000000000 --- a/src/core/JsLink.zig +++ /dev/null @@ -1,733 +0,0 @@ -/// This is the JavaScript backend's linker. -/// It takes in a compilation and a list of `JsTree`s and creates a JavaScript -/// bundle out of them. -comp: *Comp, - -/// Value is an index into `link_strings` -decls: std.AutoHashMapUnmanaged(Decl.Index, LinkString.Index) = .{}, -link_strings: std.ArrayHashMapUnmanaged( - LinkString, - GlobalOwner, - LinkString.Context, - true, -) = .{}, -string_bytes: std.ArrayListUnmanaged(u8) = .{}, - -/// The first declaration is the function containing top-level code. -pub fn linkJsCode( - comp: *Comp, - decls: []const Decl.Index, - writer: anytype, -) !void { - var t = tracer.trace(@src()); - defer t.end(); - - // Arena allocator is used for the main flow - var link: JsLink = .{ .comp = comp }; - defer link.deinit(); - - // Temp allocator is used for extremely temporary string work. - // var temp_sfa = std.heap.stackFallback(4096, comp.gpa); - // const temp_alloc = temp_sfa.get(); - - // 1. Resolve all global identifiers. Function names must not shadow external - // names. We also need this list prepared so that local identifier names do not - // shadow functions or externals. - const root_decl = comp.intern_pool.declPtrConst(decls[0]); - assert(root_decl.kind == .named); - assert(root_decl.name == .empty); - - for (root_decl.codegen.?.extern_symbols) |extern_symbol| { - try link.reserveExternSymbol(extern_symbol); - } - - for (decls[1..]) |decl_index| { - assert(decl_index != decls[0]); - - const decl = comp.intern_pool.declPtrConst(decl_index); - - // todo: add all externs first - for (decl.codegen.?.extern_symbols) |extern_symbol| { - try link.reserveExternSymbol(extern_symbol); - } - - const name = decl.name.toSlice(&comp.intern_pool); - assert(name.len > 0); - - const gop = try link.link_strings.getOrPutContextAdapted( - comp.gpa, - name, - LinkString.SliceContext{ .link = &link }, - .{ .link = &link }, - ); - if (gop.found_existing) { - @panic("TODO: handle this shadowing case."); - } - const name_str = try link.appendLinkString(name); - gop.key_ptr.* = name_str; - gop.value_ptr.* = .decl; - try link.decls.putNoClobber(comp.gpa, decl_index, @enumFromInt(gop.index)); - } - - try writer.print("// Language Compiler v0.1.3. Alpha quality software.\n", .{}); - - if (root_decl.codegen.?.nodes.len > 1) { - // 2. Print every function decl - for (decls[1..]) |decl_index| { - try link.printDecl(decl_index, writer); - } - - // 3. Print the top-level code - try link.printDeclContents(&root_decl.codegen.?, writer); - } else { - try writer.print("// (program has no observable effects, thus an empty file)\n", .{}); - } -} - -fn deinit(link: *JsLink) void { - var t = tracer.trace(@src()); - defer t.end(); - - defer link.* = undefined; - link.decls.deinit(link.comp.gpa); - link.link_strings.deinit(link.comp.gpa); - link.string_bytes.deinit(link.comp.gpa); -} - -fn reserveExternSymbol(link: *JsLink, extern_symbol: String.WithLen) !void { - var t = tracer.trace(@src()); - defer t.end(); - - const name = extern_symbol.toSlice(&link.comp.intern_pool); - const gop = try link.link_strings.getOrPutContextAdapted( - link.comp.gpa, - name, - LinkString.SliceContext{ .link = link }, - .{ .link = link }, - ); - - if (gop.found_existing) { - if (gop.value_ptr.* != .external) { - @panic("TODO: Rename Decl"); - } - } - gop.key_ptr.* = try link.appendLinkString(name); - gop.value_ptr.* = .external; -} - -fn appendLinkString(link: *JsLink, string: []const u8) !LinkString { - var t = tracer.trace(@src()); - defer t.end(); - - const start = link.string_bytes.items.len; - try link.string_bytes.appendSlice(link.comp.gpa, string); - return .{ .start = @intCast(start), .len = @intCast(string.len) }; -} - -fn declIdentifier(link: *const JsLink, decl: Decl.Index) []const u8 { - var t = tracer.trace(@src()); - defer t.end(); - - return (link.decls.get(decl) orelse if (std.debug.runtime_safety) { - std.debug.panic("Decl {d} ({}) is not present in link decl map", .{ - @intFromEnum(decl), - link.comp.intern_pool.declPtrConst(decl).name.fmt(&link.comp.intern_pool), - }); - } else unreachable).toString(link).slice(link); -} - -fn printDecl(link: *JsLink, decl_index: Decl.Index, writer: anytype) !void { - var t = tracer.trace(@src()); - defer t.end(); - - try writer.writeAll("let "); - try writer.writeAll(link.declIdentifier(decl_index)); - try writer.writeAll(" = "); - - const decl = link.comp.intern_pool.declPtrConst(decl_index); - const tree = &decl.codegen.?; - - if (tree.param_count != 1) - try writer.writeAll("("); - for (0..tree.param_count) |id| { - try link.printLocal(@intCast(id), writer); - if (id != tree.param_count - 1) try writer.writeAll(", "); - } - if (tree.param_count != 1) - try writer.writeAll(")"); - try writer.writeAll(" => "); - - const stmts = tree.mainStatements(); - if (stmts.len == 1 and tree.nodeTag(stmts[0]) == .stmt_return) { - if (tree.nodeData(.stmt_return, stmts[0]).operand.unwrap()) |operand| { - try link.printExpr(tree, operand, writer); - } else { - try writer.writeAll("{}"); - } - } else { - try writer.writeAll("{\n"); - try link.printDeclContents(tree, writer); - try writer.writeAll("}"); - } - - try writer.writeAll(";\n"); -} - -fn printDeclContents(link: *JsLink, tree: *const JsTree, writer: anytype) !void { - var t = tracer.trace(@src()); - defer t.end(); - - for (tree.mainStatements()) |stmt| { - try link.printExpr(tree, stmt, writer); - try writer.writeAll(";\n"); - } -} - -fn printBlockContents( - link: *JsLink, - tree: *const JsTree, - list: JsTree.IndexList, - writer: anytype, -) !void { - var t = tracer.trace(@src()); - defer t.end(); - - assert(list.slice(tree).len > 0); - for (list.slice(tree)) |stmt| { - try link.printExpr(tree, stmt, writer); - switch (tree.nodeTag(stmt)) { - .stmt_block, - .stmt_if, - => {}, - else => try writer.writeAll(";"), - } - try writer.writeAll("\n"); - } -} - -fn printBlock(link: *JsLink, tree: *const JsTree, list: JsTree.IndexList, writer: anytype) !void { - var t = tracer.trace(@src()); - defer t.end(); - - if (list.len != 1) { - try writer.writeAll("{"); - try link.printBlockContents(tree, list, writer); - try writer.writeAll("}"); - } else { - try link.printBlockContents(tree, list, writer); - } -} - -fn printExpr( - link: *JsLink, - tree: *const JsTree, - node: Node.Index, - writer: anytype, -) @TypeOf(writer).Error!void { - return link.printExprLevel(tree, node, .comma, writer); -} - -fn printExprLevel( - link: *JsLink, - tree: *const JsTree, - node: Node.Index, - level: Level, - writer: anytype, -) @TypeOf(writer).Error!void { - var t = tracer.trace(@src()); - defer t.end(); - - switch (tree.nodeTag(node)) { - .root => unreachable, - - .plain_value => { - const plain_value: JsTree.PlainValue = tree.nodeData(.plain_value, node); - switch (plain_value.value.toKey(&link.comp.intern_pool)) { - // TODO: minification - .int32 => |val| try writer.print("{d}", .{val}), - // TODO: correct printing of many value types - .float => |val| try writer.print("{d}", .{val}), - .simple_value => |val| try writer.writeAll(switch (val) { - .true => "true", - .false => "false", - // TODO: make codegen decide from null and undefined - // instead of emitting void nodes. - .void => "undefined", - }), - .string => |val| try writeString(val, writer), - - .comptime_float => unreachable, // was not lowered to float - - else => unreachable, // this value is not a plain_value - } - }, - - .local => { - const ref: JsTree.LocalRef = tree.nodeData(.local, node); - try link.printLocal(ref.id, writer); - }, - - .global => { - const ref: JsTree.GlobalRef = tree.nodeData(.global, node); - try writer.writeAll(ref.name.toSlice(&link.comp.intern_pool)); - }, - - .decl => { - const ref: JsTree.DeclRef = tree.nodeData(.decl, node); - try writer.writeAll(link.declIdentifier(ref.index)); - }, - - .stmt_break => { - const stmt: JsTree.StmtBreak = tree.nodeData(.stmt_break, node); - try writer.writeAll("break"); - if (true) { - try writer.writeAll(" "); - try link.printLabelIdentifier(stmt.label, writer); - } - }, - - .stmt_return => { - const stmt: JsTree.StmtReturn = tree.nodeData(.stmt_return, node); - try writer.writeAll("return"); - if (stmt.operand.unwrap()) |operand| { - try writer.writeAll(" "); - try link.printExpr(tree, operand, writer); - } - }, - - .stmt_declare_local => { - const stmt: JsTree.StmtDeclareLocal = tree.nodeData(.stmt_declare_local, node); - try writer.writeAll("let "); - try link.printLocal(stmt.identifier_index, writer); - if (stmt.init.unwrap()) |init| { - try writer.writeAll(" = "); - try link.printExpr(tree, init, writer); - } - }, - - .stmt_if => { - const stmt: JsTree.StmtIf = tree.nodeData(.stmt_if, node); - const data = stmt.data.get(tree); - try writer.writeAll("if ("); - try link.printExpr(tree, stmt.cond, writer); - try writer.writeAll(") "); - if (data.flags.yes_label) { - try link.printLabelIdentifier(data.flags.block_label, writer); - try writer.writeAll(":"); - } - try link.printBlock(tree, data.yes, writer); - if (data.no.len > 0) { - try writer.writeAll(" else "); - if (data.flags.no_label) { - try link.printLabelIdentifier(data.flags.block_label, writer); - try writer.writeAll(":"); - } - try link.printBlock(tree, data.no, writer); - } - }, - - .stmt_block => { - const stmt: JsTree.StmtBlock = tree.nodeData(.stmt_block, node); - try link.printLabelIdentifier(stmt.label, writer); - try writer.writeAll(": {\n"); - try link.printBlockContents(tree, stmt.content.get(tree), writer); - try writer.writeAll("}"); - }, - - inline .assign, - .add, - .sub, - .mul, - .div, - .eq, - .lt, - .gt, - .assign_add, - .assign_sub, - .assign_mul, - .assign_div, - => |tag| { - const bin: JsTree.Bin = tree.nodeData(tag, node); - const op: Operator = @field(Operator, @tagName(tag)); - const inner_level = comptime op.level(); - if (inner_level.needsParens(level)) - try writer.writeAll("("); - try link.printExprLevel(tree, bin.lhs, inner_level, writer); - try writer.writeAll(" "); - try writer.writeAll(comptime op.symbol()); - try writer.writeAll(" "); - try link.printExprLevel(tree, bin.rhs, inner_level, writer); - if (inner_level.needsParens(level)) - try writer.writeAll(")"); - }, - - inline .negate, - .bool_not, - .pre_dec, - .pre_inc, - => |tag| { - const un: JsTree.Un = tree.nodeData(tag, node); - const op: Operator = @field(Operator, @tagName(tag)); - try writer.writeAll(comptime op.symbol()); - try link.printExpr(tree, un.operand, writer); - }, - - .ternary => { - const ternary: JsTree.Ternary = tree.nodeData(.ternary, node); - const data = ternary.data.get(tree); - try writer.writeAll("("); - try link.printExpr(tree, ternary.cond, writer); - try writer.writeAll(" ? "); - try link.printExpr(tree, data.yes, writer); - try writer.writeAll(" : "); - try link.printExpr(tree, data.no, writer); - try writer.writeAll(")"); - }, - - .call => { - const call: JsTree.Call = tree.nodeData(.call, node); - const args = call.args.get(tree); - - try link.printExpr(tree, call.base, writer); - try writer.writeAll("("); - - for (args.slice(tree), 0..) |arg, i| { - if (i != 0) try writer.writeAll(", "); - try link.printExpr(tree, arg, writer); - } - - try writer.writeAll(")"); - }, - .new => { - const call: JsTree.Call = tree.nodeData(.new, node); - const args = call.args.get(tree); - - if (Level.new.needsParens(level)) - try writer.writeAll("("); - try writer.writeAll("new "); - try link.printExpr(tree, call.base, writer); - - if (args.len > 0) { - try writer.writeAll("("); - - for (args.slice(tree), 0..) |arg, i| { - if (i != 0) try writer.writeAll(", "); - try link.printExpr(tree, arg, writer); - } - - try writer.writeAll(")"); - } - if (Level.new.needsParens(level)) - try writer.writeAll(")"); - }, - - .field => { - const field: JsTree.Field = tree.nodeData(.field, node); - - try link.printExpr(tree, field.base, writer); - try writer.writeAll("."); - try writer.writeAll(field.field.toSlice(&link.comp.intern_pool)); - }, - } -} - -pub fn canPrintWithoutEscape(c: u21) bool { - const first_ascii_printable = 0x20; - const last_ascii_printable = 0x7E; - const first_high_surrogate = 0xD800; - const last_low_surrogate = 0xDFFF; - - if (c <= last_ascii_printable) { - return c >= first_ascii_printable and c != '\\' and c != '"'; - } else { - return c != 0xFEFF and (c < first_high_surrogate or c > last_low_surrogate); - } -} - -pub fn indexOfNeedsEscape(slice: []const u8) ?usize { - var it = std.unicode.Wtf8Iterator{ .bytes = slice, .i = 0 }; - var i: usize = 0; - while (it.nextCodepoint()) |c| { - if (!canPrintWithoutEscape(c)) - return i; - i = it.i; - } - return null; -} - -// This function is loosely based on Bun's `writeJSONString`, which -// is a Zig port of esbuild's `printUnquotedUTF16` function. -// Both projects are licensed under MIT -fn writeString(decoded: []const u8, writer: anytype) !void { - try writer.writeByte('"'); - - var text = decoded; - while (text.len > 0) { - const width = std.unicode.utf8ByteSequenceLength(text[0]) catch - unreachable; // string is encoded incorrectly - - const c: u21 = std.unicode.wtf8Decode(text[0..width]) catch - unreachable; // string is encoded incorrectly - - if (canPrintWithoutEscape(c)) { - if (indexOfNeedsEscape(text)) |j| { - try writer.writeAll(text[0..j]); - text = text[j..]; - continue; - } else { - try writer.writeAll(text); - break; - } - } - - text = text[width..]; - - switch (c) { - // Special-case the bell character since it may cause dumping this file to - // the terminal to make a sound, which is undesirable. Note that we can't - // use an octal literal to print this shorter since octal literals are not - // allowed in strict mode (or in template strings). - 0x07 => { - try writer.writeAll("\\x07"); - }, - 0x08 => { - try writer.writeAll("\\b"); - }, - 0x0C => { - try writer.writeAll("\\f"); - }, - '\n' => { - try writer.writeAll("\\n"); - }, - std.ascii.control_code.cr => { - try writer.writeAll("\\r"); - }, - std.ascii.control_code.vt => { - try writer.writeAll("\\v"); - }, - '\\' => { - try writer.writeAll("\\\\"); - }, - '"' => { - try writer.writeAll("\\\""); - }, - '\t' => { - try writer.writeAll("\\t"); - }, - - else => { - const hex_chars = "0123456789ABCDEF"; - - if (c < 0xFFFF) { - const k = @as(usize, @intCast(c)); - - try writer.writeAll(&[_]u8{ - '\\', - 'u', - hex_chars[(k >> 12) & 0xF], - hex_chars[(k >> 8) & 0xF], - hex_chars[(k >> 4) & 0xF], - hex_chars[k & 0xF], - }); - } else { - const first_high_surrogate = 0xD800; - const first_low_surrogate = 0xDC00; - - const k = c - 0x10000; - const lo = @as(usize, @intCast(first_high_surrogate + ((k >> 10) & 0x3FF))); - const hi = @as(usize, @intCast(first_low_surrogate + (k & 0x3FF))); - - try writer.writeAll(&[_]u8{ - '\\', - 'u', - hex_chars[lo >> 12], - hex_chars[(lo >> 8) & 15], - hex_chars[(lo >> 4) & 15], - hex_chars[lo & 15], - '\\', - 'u', - hex_chars[hi >> 12], - hex_chars[(hi >> 8) & 15], - hex_chars[(hi >> 4) & 15], - hex_chars[hi & 15], - }); - } - }, - } - } - try writer.writeByte('"'); -} - -fn printLocal(link: *JsLink, local: JsTree.LocalIndex, writer: anytype) !void { - var t = tracer.trace(@src()); - defer t.end(); - - _ = link; // todo: be smarter about this - try writer.print("v{d}", .{local}); -} - -fn printLabelIdentifier(link: *JsLink, local: JsTree.LocalIndex, writer: anytype) !void { - var t = tracer.trace(@src()); - defer t.end(); - - _ = link; // todo: be smarter about this - try writer.print("label{d}", .{local}); -} - -const GlobalOwner = enum { - external, - decl, -}; - -// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_precedence -const Level = enum { - comma, - assignment, - conditional, - logical_or, - logical_and, - bitwise_or, - bitwise_xor, - bitwise_and, - equality, - relational, - bitshift, - additive, - multiplicative, - exponentiation, - prefix, - postfix, - new, - access_and_call, - member, - - pub fn needsParens(inner_level: Level, outer_level: Level) bool { - return @intFromEnum(outer_level) > @intFromEnum(inner_level); - } -}; - -const Operator = enum { - assign, - assign_add, - assign_sub, - assign_mul, - assign_div, - add, - sub, - mul, - div, - eq, - lt, - gt, - negate, - bool_not, - pre_inc, - pre_dec, - - pub fn level(op: Operator) Level { - return switch (op) { - .assign, - .assign_add, - .assign_sub, - .assign_mul, - .assign_div, - => .assignment, - .add, .sub => .additive, - .mul, .div => .multiplicative, - .eq => .equality, - .lt, .gt => .relational, - - .pre_inc, - .pre_dec, - .bool_not, - .negate, - => .prefix, - }; - } - - pub fn symbol(op: Operator) []const u8 { - return switch (op) { - .assign => "=", - .assign_add => "+=", - .assign_sub => "-=", - .assign_mul => "*=", - .assign_div => "/=", - .add => "+", - .sub => "-", - .mul => "*", - .div => "/", - .eq => "===", - .lt => "<", - .gt => ">", - - .pre_inc => "++", - .pre_dec => "++", - - .negate => "-", - .bool_not => "!", - }; - } -}; - -const LinkString = struct { - start: u32, - len: u32, - - fn slice(str: LinkString, link: *const JsLink) []const u8 { - return link.string_bytes.items[str.start..][0..str.len]; - } - - const Index = enum(u32) { - _, - - pub fn toString(index: Index, link: *const JsLink) LinkString { - return link.link_strings.keys()[@intFromEnum(index)]; - } - }; - - const Context = struct { - link: *const JsLink, - - pub fn hash(ctx: Context, str: LinkString) u32 { - // assert(str.start + str.len < ctx.link.string_bytes.items.len); - - return @truncate(Wyhash.hash(0, str.slice(ctx.link))); - } - - pub fn eql(ctx: Context, a: LinkString, b: LinkString, _: usize) bool { - assert(a.start + a.len < ctx.link.string_bytes.items.len); - assert(b.start + b.len < ctx.link.string_bytes.items.len); - return a.start == b.start; - } - }; - - const SliceContext = struct { - link: *const JsLink, - - pub fn hash(_: SliceContext, str: []const u8) u32 { - return @truncate(Wyhash.hash(0, str)); - } - - pub fn eql(ctx: SliceContext, a: []const u8, b: LinkString, _: usize) bool { - // since the given string is not in string_bytes we do comparisons - return std.mem.eql(u8, a, b.slice(ctx.link)); - } - }; -}; - -const JsLink = @This(); -const std = @import("std"); -const assert = std.debug.assert; -const Wyhash = std.hash.Wyhash; -const log = std.log.scoped(.js_link); - -const Comp = @import("Comp.zig"); -const Decl = Comp.Decl; - -const JsTree = @import("JsTree.zig"); -const Node = JsTree.Node; - -const InternPool = @import("InternPool.zig"); -const String = InternPool.String; - -const tracer = @import("tracer"); diff --git a/src/core/JsNodeGen.zig b/src/core/JsNodeGen.zig deleted file mode 100644 index cbf728a18f387014a2805fbe424ad363458b7146..0000000000000000000000000000000000000000 --- a/src/core/JsNodeGen.zig +++ /dev/null @@ -1,701 +0,0 @@ -//! Air -> JsTree -gpa: Allocator, -arena: Allocator, -air: *const Air, -ip: *const InternPool, - -nodes: Node.List, -extra: dod.ExtraStore(.tfr, .@"var"), -extern_symbols: std.AutoArrayHashMapUnmanaged(InternPool.String.WithLen, void), -decl_deps: std.AutoArrayHashMapUnmanaged(Decl.Index, void), - -param_count: u32, -air_node_map: []MapEntry, -scratch: std.ArrayListUnmanaged(u32), -next_id: u32, -next_label: u32, - -const Error = error{ - OutOfMemory, -}; - -const MapEntry = union { - none: void, - node: Node.Index, - block: packed struct { - used: bool = false, - index: u31, - }, -}; - -pub fn init(gpa: Allocator, arena: Allocator, ip: *const InternPool, air: *const Air) !JsNodeGen { - const t = tracer.trace(@src()); - defer t.end(); - - const air_node_map = try arena.alloc(MapEntry, air.insts.len); - if (std.debug.runtime_safety) { - @memset(air_node_map, .{ .none = {} }); - } - const param_count = air.paramCount(); - return .{ - .gpa = gpa, - .arena = arena, - .air = air, - .ip = ip, - .param_count = param_count, - .nodes = .{}, - .extern_symbols = .{}, - .decl_deps = .{}, - .air_node_map = air_node_map, - .extra = .{}, - .scratch = .{}, - .next_id = param_count, - .next_label = 0, - }; -} - -pub fn deinit(c: *JsNodeGen) void { - const t = tracer.trace(@src()); - defer t.end(); - - defer c.* = undefined; - c.nodes.deinit(c.gpa); - c.extra.deinit(c.gpa); - c.extern_symbols.deinit(c.gpa); - c.decl_deps.deinit(c.gpa); - - // arena can deinit the rest -} - -pub fn finalize(c: *JsNodeGen) !JsTree { - const t = tracer.trace(@src()); - defer t.end(); - - // todo: errdefer will probably leave memory in a fully invalid state - try c.nodes.setCapacity(c.gpa, c.nodes.len); - - var extra = try c.extra.toOwnedStore(c.gpa); - errdefer extra.deinit(c.gpa); - - // todo: i dont like this dupe - const extern_symbols = try c.gpa.dupe(InternPool.String.WithLen, c.extern_symbols.keys()); - errdefer c.gpa.free(extern_symbols); - - const decl_deps = try c.gpa.dupe(Decl.Index, c.decl_deps.keys()); - errdefer c.gpa.free(decl_deps); - - defer c.deinit(); - - return .{ - .param_count = c.param_count, - .nodes = c.nodes.toOwnedSlice(), - .extern_symbols = extern_symbols, - .decl_deps = decl_deps, - .extra = extra, - }; -} - -pub fn build(c: *JsNodeGen) !void { - const t = tracer.trace(@src()); - defer t.end(); - - assert(c.air.instTag(.main_block) == .block); - const statements = c.air.instData(.block, .main_block).slice(c.air); - - const scratch_top = c.scratch.items.len; - defer c.scratch.shrinkRetainingCapacity(scratch_top); - - _ = try c.add(.root, undefined); - - if (statements.len > 0) { - for (statements[0..c.param_count], 1..) |stmt, expected_id| { - assert(@intFromEnum(stmt) == expected_id); - assert(c.air.instTag(stmt) == .arg); - c.air_node_map[@intFromEnum(stmt)] = .{ - .node = try c.add(.local, .{ .id = @intFromEnum(stmt) - 1 }), - }; - } - - for (statements[c.param_count..]) |stmt| { - try c.codeGenInst(stmt); - } - } - - const items = c.scratch.items[scratch_top..]; - const top = c.extra.list.items.len; - try c.extra.list.appendSlice(c.gpa, items); - c.nodes.items(.data)[0] = EncodedData.encode(.root, .{ - .start = @intCast(top), - .len = @intCast(items.len), - }); -} - -fn codeGenInst(c: *JsNodeGen, inst: Air.Inst.Index) Error!void { - const t = tracer.traceFmt(@src(), "#{d} {s}", .{ @intFromEnum(inst), @tagName(c.air.instTag(inst)) }); - defer t.end(); - - switch (c.air.instTag(inst)) { - .arg => unreachable, // codegen must skip over these instructions. - - .block => try c.codeGenBlock(inst), - - .js_global_ref => { - const data: InternPool.String.WithLen = c.air.instData(.js_global_ref, inst); - try c.extern_symbols.put(c.gpa, data, {}); - c.air_node_map[@intFromEnum(inst)] = .{ - .node = try c.add(.global, .{ .name = data }), - }; - }, - - .memory => { - const i = @intFromEnum(inst); - const data: Air.Memory = c.air.instData(.memory, inst); - const value: JsTree.Node.Index.Optional = if (data.init.isVoid()) - .none - else - (try c.fetchExpression(data.init)).toOptional(); - - try c.addStmt(try c.add(.stmt_declare_local, .{ - .identifier_index = c.next_id, - .init = value, - })); - const id = try c.add(.local, .{ .id = c.next_id }); - c.next_id += 1; - c.air_node_map[i] = .{ .node = id }; - }, - - inline .gt, - .lt, - .eq, - .add, - .sub, - .mul, - .div, - => |tag| { - const data: Air.Bin = c.air.instData(tag, inst); - const node = try c.add(@field(JsTree.Tag, @tagName(tag)), .{ - .lhs = try c.fetchExpression(data.lhs), - .rhs = try c.fetchExpression(data.rhs), - }); - try c.markExpression(inst, node); - }, - - .store => { - const data: Air.Store = c.air.instData(.store, inst); - - const target = try c.fetchInstExpression(data.memory); - - if (data.operand.kind == .index) { - const index = data.operand.decode().index; - switch (c.air.instTag(index)) { - inline .add, - .sub, - .mul, - .div, - => |tag| { - const inner_data: Air.Bin = c.air.instData(tag, index); - if (inner_data.lhs.eql(data.memory.toRef())) { - if ((tag == .add or tag == .sub) and - inner_data.rhs.kind == .value) - { - // if (inner_data.rhs.decode().value.toFloat(c.ip)) |f| { - // if (f == 1) { - // // convert this into ++ or -- - // const node = try c.add( - // if (tag == .add) - // .pre_inc - // else - // .pre_dec, - // .{ - // .operand = target, - // }, - // ); - // try c.markExpression(inst, node); - - // return; - // } - // } - } - - // convert into a compound assignment - const node = try c.add( - @field(JsTree.Tag, "assign_" ++ @tagName(tag)), - .{ - .lhs = target, - .rhs = try c.fetchExpression(inner_data.rhs), - }, - ); - try c.markExpression(inst, node); - - return; - } - }, - else => {}, - } - } - - const node = try c.add(.assign, .{ - .lhs = target, - .rhs = try c.fetchExpression(data.operand), - }); - try c.markExpression(inst, node); - }, - - inline .negate, - .bool_not, - => |tag| { - const data: Air.Un = c.air.instData(tag, inst); - const node = try c.add(@field(JsTree.Tag, @tagName(tag)), .{ - .operand = try c.fetchExpression(data.operand), - }); - try c.markExpression(inst, node); - }, - - .field => { - const data: Air.Field = c.air.instData(.field, inst); - const node = try c.add(.field, .{ - .base = try c.fetchExpression(data.base), - .field = data.field, - }); - try c.markExpression(inst, node); - }, - - inline .call, .new => |tag| { - const data: Air.Call = c.air.instData(tag, inst); - const call_data = data.call_data.get(c.air); - - try c.extra.list.ensureUnusedCapacity(c.gpa, call_data.args.len); - const start = c.extra.list.items.len; - c.extra.list.items.len += call_data.args.len; - - const items = c.extra.list.items[start..][0..call_data.args.len]; - - for (call_data.args.slice(c.air), items) |arg, *dest| { - dest.* = @intFromEnum(try c.fetchExpression(arg)); - } - - const node = try c.add(@field(JsTree.Tag, @tagName(tag)), .{ - .base = try c.fetchExpression(data.base), - .args = try c.extra.append(c.gpa, JsTree.IndexList, .{ - .start = @intCast(start), - .len = call_data.args.len, - }), - }); - try c.markExpression(inst, node); - }, - - .@"if" => { - const data: Air.If = c.air.instData(.@"if", inst); - const extra = data.data.get(c.air); - var cond = try c.fetchExpression(data.cond); - - if (c.canLowerToExpression(extra.yes) and - c.canLowerToExpression(extra.no)) - { - const yes = try c.lowerToExpression(extra.yes); - const no = try c.lowerToExpression(extra.yes); - - const ternary = try c.add(.ternary, .{ - .cond = cond, - .data = try c.extra.append(c.gpa, JsTree.Ternary.Data, .{ - .yes = yes, - .no = no, - }), - }); - - try c.markExpression(inst, ternary); - } else { - const flags = c.air.instFlags(inst); - const uses = flags.uses; - - const target = if (uses != .none) - try c.addTemporary() - else - null; - - var yes = try c.codeGenBranch(extra.yes, target); - var no = try c.codeGenBranch(extra.no, target); - assert(!((yes == null) and (no == null))); // no "void else void" branches - - // when `yes` is void, swap the condition so that the `else` is omitted - if (yes == null) { - const temp_yes = yes; - yes = no; - no = temp_yes; - cond = try c.add(.bool_not, .{ - .operand = cond, - }); - } - - const main_expr = try c.add(.stmt_if, .{ - .cond = cond, - .data = try c.extra.append(c.gpa, JsTree.StmtIf.Data, .{ - .yes = yes.?.list, - .no = if (no) |branch| - branch.list - else - .{ .start = 0, .len = 0 }, - .flags = .{ - .block_label = @intCast(c.next_label + 1), - .yes_label = yes.?.requires_label, - .no_label = if (no) |branch| branch.requires_label else false, - }, - }), - }); - - try c.addStmt(main_expr); - - if (target) |node| { - try c.markExpression(inst, node); - } - } - }, - - .@"break" => { - const brk: Air.Break = c.air.instData(.@"break", inst); - - const operand = try c.fetchExpression(brk.value); - - // break #0 -> return - if (brk.block_ref == .main_block) { - try c.addStmt(try c.add(.stmt_return, .{ - .operand = operand.toOptional(), - })); - } else { - const block = &c.air_node_map[@intFromEnum(brk.block_ref)].block; - - const block_insts = c.air.instData(.block, brk.block_ref).slice(c.air); - if (block_insts[block_insts.len - 1] != inst) { - block.used = true; - } - - const assignment: Node.Index.Optional = @enumFromInt(c.scratch.items[block.index]); - if (assignment.unwrap()) |target| { - try c.addStmt(try c.add(.assign, .{ - .lhs = target, - .rhs = operand, - })); - } - - try c.addStmt(try c.add(.stmt_break, .{ - .label = c.scratch.items[block.index + 1], - })); - } - }, - } -} - -fn markExpression(c: *JsNodeGen, inst: Air.Inst.Index, node: Node.Index) !void { - const t = tracer.trace(@src()); - defer t.end(); - - const flags: Air.Flags = c.air.instFlags(inst); - const i = @intFromEnum(inst); - switch (flags.uses) { - .none => { - try c.addStmt(node); - }, - .single => { - // TODO: this is not aware of side effect fences - c.air_node_map[i] = .{ .node = node }; - }, - .multi => { - const decl = try c.add(.stmt_declare_local, .{ - .identifier_index = c.next_id, - .init = node.toOptional(), - }); - try c.addStmt(decl); - const id = try c.add(.local, .{ .id = c.next_id }); - c.next_id += 1; - c.air_node_map[i] = .{ .node = id }; - }, - } -} - -fn fetchExpression(c: *JsNodeGen, ref: Air.Inst.Ref) !Node.Index { - const t = tracer.trace(@src()); - defer t.end(); - - return switch (ref.decode()) { - .value => |v| c.valueToExpression(v), - .index => |index| fetchInstExpression(c, index), - }; -} - -fn fetchInstExpression(c: *JsNodeGen, index: Air.Inst.Index) !Node.Index { - const t = tracer.trace(@src()); - defer t.end(); - - const data = c.air_node_map[@intFromEnum(index)]; - if (std.debug.runtime_safety) { - const flags: Air.Flags = c.air.instFlags(index); - switch (flags.uses) { - .none => unreachable, // incorrectly marked instruction as unused, - .single => {}, - .multi => assert(c.nodes.items(.tag)[@intFromEnum(data.node)] == .local), - } - } - return data.node; -} - -fn valueToExpression(c: *JsNodeGen, v: Value) !Node.Index { - const t = tracer.trace(@src()); - defer t.end(); - - const intern_tag = v.valueTag(c.ip); - switch (intern_tag) { - .removed => unreachable, // UAF - - .type_function, - .simple_type, - => unreachable, // Type values are not allowed here - - .int32, - .float, - .comptime_float, - .string, - .simple_value, - => return try c.add(.plain_value, .{ .value = v }), - - // funcs always point to decls - .func => { - const func = v.toKey(c.ip).func; - try c.decl_deps.put(c.gpa, func.owner_decl, {}); - return try c.add(.decl, .{ .index = func.owner_decl }); - }, - } -} - -fn addTemporary(c: *JsNodeGen) !Node.Index { - const t = tracer.trace(@src()); - defer t.end(); - - try c.addStmt(try c.add(.stmt_declare_local, .{ - .identifier_index = c.next_id, - .init = .none, - })); - const id = try c.add(.local, .{ .id = c.next_id }); - c.next_id += 1; - return id; -} - -const Branch = struct { - requires_label: bool, - list: JsTree.IndexList, -}; - -fn codeGenBranch(c: *JsNodeGen, inst: Air.Inst.Ref, temporary: ?Node.Index) !?Branch { - const t = tracer.trace(@src()); - defer t.end(); - - const scratch_top = c.scratch.items.len; - defer c.scratch.shrinkRetainingCapacity(scratch_top); - - c.next_label += 1; - defer c.next_label -= 1; - const this_label = c.next_label; - - var requires_label = false; - var items = switch (inst.decode()) { - .value => |v| items: { - if (v == .void) - return null; - if (temporary) |temp| { - const node = try c.valueToExpression(v); - try c.addStmt(try c.add(.assign, .{ - .lhs = temp, - .rhs = node, - })); - } - break :items c.scratch.items[scratch_top..]; - }, - .index => |i| items: { - assert(c.air.instTag(i) == .block); - c.air_node_map[@intFromEnum(i)] = .{ - .block = .{ .index = @intCast(scratch_top) }, - }; - - try c.scratch.appendSlice(c.arena, &.{ - @intFromEnum(if (temporary) |temp| temp.toOptional() else .none), - this_label, - }); - - const block: Air.IndexList = c.air.instData(.block, i); - for (block.slice(c.air)) |item| { - try c.codeGenInst(item); - } - - requires_label = c.air_node_map[@intFromEnum(i)].block.used; - - break :items c.scratch.items[scratch_top + 2 ..]; - }, - }; - - const last_node = c.scratch.items[c.scratch.items.len - 1]; - if (c.nodes.items(.tag)[last_node] == .stmt_break and - c.nodes.items(.data)[last_node].decode(.stmt_break, .stmt_break).label == this_label) - { - items.len -= 1; - c.nodes.len -= 1; - } - - const top = c.extra.list.items.len; - try c.extra.list.appendSlice(c.gpa, items); - return .{ - .requires_label = requires_label, - .list = .{ - .start = @intCast(top), - .len = @intCast(items.len), - }, - }; -} - -fn codeGenBlock(c: *JsNodeGen, inst: Air.Inst.Index) !void { - const t = tracer.trace(@src()); - defer t.end(); - - const scratch_top = c.scratch.items.len; - errdefer c.scratch.shrinkRetainingCapacity(scratch_top); - - c.next_label += 1; - defer c.next_label -= 1; - - const next_label = c.next_label; - - try c.scratch.appendSlice(c.arena, &.{ - // @intFromEnum(if (temporary) |temp| temp.toOptional() else .none), - @intFromEnum(Node.Index.Optional.none), - next_label, - }); - - c.air_node_map[@intFromEnum(inst)] = .{ - .block = .{ .index = @intCast(scratch_top) }, - }; - const block: Air.IndexList = c.air.instData(.block, inst); - - for (block.slice(c.air)) |item| { - try c.codeGenInst(item); - } - - const items = c.scratch.items[scratch_top + 2 ..]; - const top = c.extra.list.items.len; - try c.extra.list.appendSlice(c.gpa, items); - c.scratch.shrinkRetainingCapacity(scratch_top); - try c.addStmt(try c.add(.stmt_block, .{ - .label = c.next_label, - .content = try c.extra.append(c.gpa, JsTree.IndexList, .{ - .start = @intCast(top), - .len = @intCast(items.len), - }), - })); -} - -// Ideally, we convert everything to an expression. This allows for much -// more compact code. When that isnt possible (`return`, `while`, etc), -// we detect that here and ensure to lower `if` to actual if statements, -// and so on. -fn canLowerToExpression(c: *const JsNodeGen, inst: Air.Inst.Ref) bool { - const t = tracer.trace(@src()); - defer t.end(); - - return switch (inst.decode()) { - .value => true, - .index => |index| switch (c.air.instTag(index)) { - .js_global_ref, - .call, - .new, - .arg, - .field, - .add, - .sub, - .mul, - .div, - .eq, - .lt, - .gt, - .negate, - .bool_not, - .store, - => true, - - // JavaScript variable declarations require statements. - // Later, this could be made true if we can hoist temporaries up. - .memory => false, - - // break/return must be a js statement - .@"break", - => false, - - .block => { - // TODO: blocks that are - // - all statements except last are lowerable - // - the last statement is lowerable OR a break of this block - return false; - }, - - .@"if" => { - const call = c.air.instData(.@"if", index); - const data = call.data.get(c.air); - - return c.canLowerToExpression(call.cond) and - c.canLowerToExpression(data.yes) and - c.canLowerToExpression(data.no); - }, - }, - }; -} - -fn lowerToExpression(c: *JsNodeGen, ref: Air.Inst.Ref) !Node.Index { - const t = tracer.trace(@src()); - defer t.end(); - - assert(c.canLowerToExpression(ref)); - return switch (ref.decode()) { - .value => |v| try c.valueToExpression(v), - .index => |index| { - try c.codeGenInst(index); - return c.fetchExpression(ref); - }, - }; -} - -fn add(c: *JsNodeGen, comptime tag: JsTree.Tag, data: tag.Data()) !Node.Index { - const t = tracer.trace(@src()); - defer t.end(); - - try c.nodes.append(c.gpa, .{ - .tag = tag, - .data = EncodedData.encode(tag, data), - }); - return @enumFromInt(c.nodes.len - 1); -} - -fn addStmt(c: *JsNodeGen, stmt: Node.Index) !void { - const t = tracer.trace(@src()); - defer t.end(); - - try c.scratch.append(c.arena, @intFromEnum(stmt)); -} - -const JsNodeGen = @This(); - -const std = @import("std"); -const assert = std.debug.assert; -const Allocator = std.mem.Allocator; - -const InternPool = @import("InternPool.zig"); -const Value = InternPool.Value; -const NullTerminatedString = InternPool.NullTerminatedString; - -const Comp = @import("Comp.zig"); -const Decl = Comp.Decl; - -const JsTree = @import("JsTree.zig"); -const EncodedData = JsTree.EncodedData; -const Node = JsTree.Node; -const LocalIndex = JsTree.LocalIndex; - -const Air = @import("Air.zig"); - -const dod = @import("array.zig"); -const tracer = @import("tracer"); diff --git a/src/core/JsTree.zig b/src/core/JsTree.zig deleted file mode 100644 index a22f4ab834d5cd0b11e267395ba2b01a15fbeab4..0000000000000000000000000000000000000000 --- a/src/core/JsTree.zig +++ /dev/null @@ -1,258 +0,0 @@ -//! JavaScript Function Representation is the last representation before javascript -//! source bytes are created. One notable thing here is that all local identifiers -//! are numbered, instead of being named. The first `param_count` local identifiers -//! refer to parameters, and the rest refer to the respective block that defined them. - -param_count: u32, -nodes: Node.List.Slice, -extern_symbols: []String.WithLen, -decl_deps: []Decl.Index, -extra: ExtraStore, - -pub const LocalIndex = u32; -pub const ExtraStore = dod.ExtraStore(.tfr, .@"const"); -pub const EncodedData = dod.EncodedData(Tag, u64); -pub const ExtraIndex = ExtraStore.Index; - -pub const Tag = enum { - root, - /// Only allow simple value types: int, float, string, bool - plain_value, - /// Reference to a local identifier (give an VarId) - local, - /// Reference to a global identifier (give a NullTerminatedString) - global, - /// Reference to a declaration (contains a Decl.Index) - decl, - // bin ops - assign, - assign_add, - assign_sub, - assign_mul, - assign_div, - add, - sub, - mul, - div, - eq, - lt, - gt, - // exprs - call, - new, - field, - ternary, - // prefix ops - negate, - bool_not, - pre_inc, - pre_dec, - // stmts - /// `let ... = ...` - stmt_declare_local, - /// `if (...) ... else ...` - stmt_if, - /// `break ... ...` - stmt_break, - /// `return ...` (break from block #0) - stmt_return, - /// `...: { ... }` - stmt_block, - - pub fn Data(comptime tag: Tag) type { - return switch (tag) { - .root => IndexList, - .plain_value => PlainValue, - .local => LocalRef, - .global => GlobalRef, - .decl => DeclRef, - .assign, - .assign_add, - .assign_sub, - .assign_mul, - .assign_div, - .add, - .sub, - .mul, - .div, - .eq, - .lt, - .gt, - => Bin, - .ternary => Ternary, - .negate, - .bool_not, - .pre_inc, - .pre_dec, - => Un, - .call, .new => Call, - .field => Field, - .stmt_if => StmtIf, - .stmt_break => StmtBreak, - .stmt_return => StmtReturn, - .stmt_declare_local => StmtDeclareLocal, - .stmt_block => StmtBlock, - }; - } -}; - -pub const Node = struct { - tag: Tag, - data: EncodedData, - - pub const List = std.MultiArrayList(Node); - - pub const Index = enum(u32) { - root = 0, - _, - - pub fn toOptional(i: Index) Optional { - return @enumFromInt(@intFromEnum(i)); - } - - pub const Optional = enum(u32) { - none = std.math.maxInt(u32), - _, - - pub fn unwrap(oi: Optional) ?Index { - return if (oi == .none) - null - else - @enumFromInt(@intFromEnum(oi)); - } - }; - }; -}; - -pub const PlainValue = struct { - value: Value, - unused: Unused = .zero, -}; - -pub const LocalRef = struct { - id: LocalIndex, - unused: Unused = .zero, -}; - -pub const GlobalRef = struct { - name: InternPool.String.WithLen, -}; - -pub const DeclRef = struct { - index: Decl.Index, - unused: Unused = .zero, -}; - -pub const StmtDeclareLocal = struct { - identifier_index: u32, - init: Node.Index.Optional, -}; - -pub const Bin = struct { - lhs: Node.Index, - rhs: Node.Index, -}; - -pub const Un = struct { - operand: Node.Index, - unused: Unused = .zero, -}; - -pub const Call = struct { - base: Node.Index, - args: ExtraIndex(IndexList), -}; - -pub const Field = struct { - base: Node.Index, - field: NullTerminatedString, -}; - -pub const IndexList = struct { - start: u32, - len: u32, - - pub fn slice(list: IndexList, tree: *const JsTree) []const Node.Index { - return @ptrCast(tree.extra.list.items[list.start..][0..list.len]); - } -}; - -pub const StmtIf = struct { - cond: Node.Index, - data: ExtraIndex(Data), - - pub const Data = struct { - yes: IndexList, - no: IndexList, - flags: Flags, - }; - - pub const Flags = packed struct(u32) { - block_label: u16, - yes_label: bool, - no_label: bool, - _: u14 = 0, - }; -}; - -pub const Ternary = struct { - cond: Node.Index, - data: ExtraIndex(Data), - - pub const Data = struct { - yes: Node.Index, - no: Node.Index, - }; -}; - -pub const StmtBreak = struct { - label: u32, - unused: Unused = .zero, -}; - -pub const StmtReturn = struct { - operand: Node.Index.Optional, - unused: Unused = .zero, -}; - -pub const StmtBlock = struct { - label: u32, - content: ExtraIndex(IndexList), -}; - -pub fn deinit(tree: *JsTree, gpa: std.mem.Allocator) void { - tree.nodes.deinit(gpa); - gpa.free(tree.decl_deps); - gpa.free(tree.extern_symbols); - tree.extra.deinit(gpa); -} - -pub fn nodeTag(tree: JsTree, index: Node.Index) Tag { - return tree.nodes.items(.tag)[@intFromEnum(index)]; -} - -pub fn nodeData(tree: JsTree, comptime tag: Tag, index: Node.Index) tag.Data() { - return tree.nodes.items(.data)[@intFromEnum(index)].decode(tag, tree.nodeTag(index)); -} - -pub fn mainStatements(tree: *const JsTree) []const Node.Index { - return tree.nodeData(.root, .root).slice(tree); -} - -const JsTree = @This(); - -const std = @import("std"); -const assert = std.debug.assert; - -const InternPool = @import("InternPool.zig"); -const Value = InternPool.Value; - -const NullTerminatedString = InternPool.NullTerminatedString; -const String = InternPool.String; - -const Comp = @import("Comp.zig"); -const Decl = Comp.Decl; - -const dod = @import("array.zig"); - -const Unused = @import("Ast.zig").Unused; diff --git a/src/core/Parse.zig b/src/core/Parse.zig index d8d3b3449c150436798a0eda1e51f610fc077207..dddfed2145a4df8221c666bd0d13fc5c7772f66d 100644 --- a/src/core/Parse.zig +++ b/src/core/Parse.zig @@ -103,14 +103,18 @@ pub const ExpectedContext = enum { statement, expression, else_clause, + struct_field, + param_list, pub fn description(tag: ExpectedContext) []const u8 { return switch (tag) { .param => "a parameter", + .param_list => "function parameters", .argument => "an argument", .statement => "a statement", .expression => "an expression", .else_clause => "an else clause", + .struct_field => "a field definition", }; } }; @@ -233,6 +237,8 @@ fn parseModuleMembers(p: *Parse) Error!SubList { => try p.scratch.append(p.gpa, try p.parseModuleDecl()), .kw_struct, + => try p.scratch.append(p.gpa, try p.parseStructDecl(.declaration)), + .kw_enum, .kw_trait, .kw_impl, @@ -247,7 +253,7 @@ fn parseModuleMembers(p: *Parse) Error!SubList { const first_tag = p.currentTag(); - try p.scratch.append(p.gpa, try p.parseExpression(true)); + try p.scratch.append(p.gpa, try p.parseExpression(.statement)); switch (first_tag) { // some control flow and blocks are allowed to not have @@ -280,6 +286,14 @@ 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(); @@ -290,20 +304,20 @@ fn parseFnDecl(p: *Parse) Error!NodeIndex { _ = try p.expect(.start_paren); - const params = try p.parseParamList(); + const params = try p.parseParamList(.types_required); const return_type = if (p.eatToken(.minus_lt)) |_| if (!p.isExpressionStart()) return p.failUnexpectedToken(.expression) else - try p.parseExpression(false) + try p.parseExpression(.preceding_block) else null; try p.expectWithoutAdvance(.start_brace); const body = try p.parseBlock(); - const extra = try p.addExtra(Ast.FnDecl, .{ + const extra = try p.addExtra(Ast.Fn, .{ .params = params, .return_type = optionalNodeIndex(return_type), }); @@ -311,14 +325,16 @@ fn parseFnDecl(p: *Parse) Error!NodeIndex { return try p.addNode(.{ .tag = .fn_decl, .main_token = main_token, - .data = .{ .fn_decl = .{ + .data = .{ .function = .{ .body = body, .extra = extra, } }, }); } -fn parseParamList(p: *Parse) Error!SubList { +const TypePresence = enum { types_optional, types_required }; + +fn parseParamList(p: *Parse, types: TypePresence) Error!SubList { const trace = t.trace(@src()); defer trace.end(); @@ -331,7 +347,7 @@ fn parseParamList(p: *Parse) Error!SubList { if (p.currentTag() == .end_paren) break; - try p.scratch.append(p.gpa, try p.parseParamDecl()); + try p.scratch.append(p.gpa, try p.parseParamDecl(types)); switch (p.currentTag()) { .comma => { @@ -347,25 +363,92 @@ fn parseParamList(p: *Parse) Error!SubList { return p.scratchToSubList(scratch_top); } -fn parseParamDecl(p: *Parse) Error!NodeIndex { +fn parseParamDecl(p: *Parse, types: TypePresence) Error!NodeIndex { const trace = t.trace(@src()); defer trace.end(); const main_token = p.assertToken(.id); - _ = try p.expect(.colon); - - const ty = try p.parseExpression(false); + const ty = if (types == .types_required or p.currentTag() == .colon) brk: { + _ = try p.expect(.colon); + break :brk try p.parseExpression(.expression); + } else null; return try p.addNode(.{ .tag = .param_decl, .main_token = main_token, .data = .{ .param_decl = .{ - .type = ty, + .type = optionalNodeIndex(ty), } }, }); } +fn parseStructDecl(p: *Parse, ctx: ExprParseContext) Error!NodeIndex { + const trace = t.trace(@src()); + defer trace.end(); + + const main_token = p.assertToken(.kw_struct); + + _ = ctx; // TODO: + _ = try p.expect(.id); + + _ = try p.expect(.start_brace); + + const fields = try p.parseStructFieldList(); + + return try p.addNode(.{ + .tag = .struct_decl, + .main_token = main_token, + .data = .{ .list = fields }, + }); +} + +fn parseStructFieldList(p: *Parse) Error!SubList { + const trace = t.trace(@src()); + defer trace.end(); + + const scratch_top = p.scratch.items.len; + defer p.scratch.shrinkRetainingCapacity(scratch_top); + + while (true) { + const trace_param = t.traceFmt(@src(), "Param {d}", .{p.scratch.items.len - scratch_top}); + defer trace_param.end(); + + if (p.currentTag() == .end_brace) break; + + try p.scratch.append(p.gpa, try p.parseStructField()); + + switch (p.currentTag()) { + .comma => { + _ = p.nextToken(); + }, + .end_brace => break, + else => return p.failUnexpectedToken(.struct_field), + } + } + + _ = p.assertToken(.end_brace); + + return p.scratchToSubList(scratch_top); +} + +fn parseStructField(p: *Parse) Error!NodeIndex { + const trace = t.trace(@src()); + defer trace.end(); + + const main_token = try p.expect(.id); + + _ = try p.expect(.colon); + + const expr = try p.parseExpression(.expression); + + return try p.addNode(.{ + .tag = .struct_field, + .main_token = main_token, + .data = .{ .un = .{ .expr = expr } }, + }); +} + fn parseBlock(p: *Parse) Error!NodeIndex { const trace = t.trace(@src()); defer trace.end(); @@ -390,7 +473,8 @@ fn parseLabelExpression(p: *Parse) Error!NodeIndex { const main_token = p.assertToken(.id); _ = try p.expect(.colon); - const expr = try p.parseExpression(false); + const expr = try p.parseExpression(.expression); + return try p.addNode(.{ .tag = .label, .main_token = main_token, @@ -422,7 +506,7 @@ fn parseBlockContents(p: *Parse) Error!struct { Node.Tag, SubList } { const first_tag = p.currentTag(); - try p.scratch.append(p.gpa, try p.parseExpression(true)); + try p.scratch.append(p.gpa, try p.parseExpression(.statement)); switch (first_tag) { // some control flow and blocks are allowed to not have @@ -466,13 +550,13 @@ fn parseVarDecl(p: *Parse) Error!NodeIndex { _ = try p.expect(.id); const data_type: OptionalNodeIndex = if (p.eatToken(.colon)) |_| - optionalNodeIndex(try p.tryParseExpression(false)) + optionalNodeIndex(try p.tryParseExpression(.expression)) else .none; _ = try p.expect(.equal); - const data_init = try p.tryParseExpression(true); + const data_init = try p.tryParseExpression(.expression); _ = try p.expect(.semi); @@ -490,6 +574,7 @@ fn isExpressionStart(p: *Parse) bool { return switch (p.currentTag()) { .bang, .builtin_id, + .dot, .id, .kw_assert, .kw_break, @@ -504,6 +589,7 @@ fn isExpressionStart(p: *Parse) bool { .start_paren, .string, .tilde, + .kw_fn, => true, else => false, }; @@ -692,7 +778,17 @@ const Operator = enum(u8) { } }; -fn parseExpression(p: *Parse, allow_assign_operators: bool) Error!NodeIndex { +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(); @@ -711,24 +807,24 @@ fn parseExpression(p: *Parse, allow_assign_operators: bool) Error!NodeIndex { else => {}, } - var expr = try p.parseExpressionPart(); + var expr = try p.parseExpressionPart(mode); while (Operator.fromToken(p.currentTag())) |op| { - if (!allow_assign_operators and op.isAssign()) break; - expr = try p.parseBinarySuffix(expr, op, allow_assign_operators); + if (mode != .statement and op.isAssign()) break; + expr = try p.parseBinarySuffix(expr, op, mode); } return expr; } -fn tryParseExpression(p: *Parse, allow_assign_operators: bool) Error!NodeIndex { +fn tryParseExpression(p: *Parse, mode: ParseExprMode) Error!NodeIndex { return if (p.isExpressionStart()) - p.parseExpression(allow_assign_operators) + p.parseExpression(mode) else p.failUnexpectedToken(.expression); } -fn parseExpressionPart(p: *Parse) Error!NodeIndex { +fn parseExpressionPart(p: *Parse, mode: ParseExprMode) Error!NodeIndex { const trace = t.trace(@src()); defer trace.end(); @@ -749,6 +845,8 @@ fn parseExpressionPart(p: *Parse) Error!NodeIndex { .kw_if => try p.parseIf(), .start_paren => try p.parseGroup(), .start_brace => try p.parseBlock(), + .dot => try p.parseAnonInstantiation(), + .kw_fn => try p.parseAnonFunction(), // Unary operators inline .bang, @@ -759,7 +857,7 @@ fn parseExpressionPart(p: *Parse) Error!NodeIndex { .kw_assert, => |tag| unary: { const main_token = p.nextToken(); - const expr = try p.parseExpressionPart(); + const expr = try p.parseExpressionPart(mode); break :unary try p.addNode(.{ .tag = switch (tag) { @@ -819,7 +917,7 @@ fn parseExpressionPart(p: *Parse) Error!NodeIndex { }, .start_bracket => { const main_token = p.nextToken(); - const index = try p.parseExpression(false); + const index = try p.parseExpression(.expression); expr = try p.addNode(.{ .tag = .index, @@ -830,7 +928,20 @@ fn parseExpressionPart(p: *Parse) Error!NodeIndex { } }, }); }, + .start_brace => { + if (mode == .preceding_block) break; + const main_token = p.nextToken(); + const initializers = try p.parseStructFieldList(); + expr = try p.addNode(.{ + .tag = .struct_init, + .main_token = main_token, + .data = .{ .struct_init = .{ + .base = optionalNodeIndex(expr), + .extra = try p.addExtra(Ast.SubList, initializers), + } }, + }); + }, else => break, } } @@ -838,16 +949,16 @@ fn parseExpressionPart(p: *Parse) Error!NodeIndex { return expr; } -fn parseBinarySuffix(p: *Parse, lhs: NodeIndex, op: Operator, allow_assign_operators: bool) Error!NodeIndex { +fn parseBinarySuffix(p: *Parse, lhs: NodeIndex, op: Operator, mode: ParseExprMode) Error!NodeIndex { const trace = t.trace(@src()); defer trace.end(); const main_token = p.nextToken(); const level = op.level(); - var rhs = try p.parseExpressionPart(); + var rhs = try p.parseExpressionPart(mode); while (Operator.fromToken(p.currentTag())) |next_op| { - if (!allow_assign_operators and next_op.isAssign()) break; + if (mode != .statement and next_op.isAssign()) break; const new_level = next_op.level(); const use = switch (op.direction()) { @@ -856,7 +967,7 @@ fn parseBinarySuffix(p: *Parse, lhs: NodeIndex, op: Operator, allow_assign_opera }; if (!use) break; - rhs = try p.parseBinarySuffix(rhs, next_op, allow_assign_operators); + rhs = try p.parseBinarySuffix(rhs, next_op, mode); } return try p.addNode(.{ @@ -876,7 +987,7 @@ fn parseReturn(p: *Parse) Error!NodeIndex { const main_token = p.assertToken(.kw_return); const expr = if (p.isExpressionStart()) - try p.parseExpression(false) + try p.parseExpression(.expression) else null; @@ -901,7 +1012,7 @@ fn parseBreak(p: *Parse) Error!NodeIndex { null; const expr = if (p.isExpressionStart()) - try p.parseExpression(false) + try p.parseExpression(.expression) else null; @@ -940,7 +1051,7 @@ fn parseCallArgs(p: *Parse) Error!SubList { defer p.scratch.shrinkRetainingCapacity(scratch_top); while (p.isExpressionStart()) { - try p.scratch.append(p.gpa, try p.parseExpression(false)); + try p.scratch.append(p.gpa, try p.parseExpression(.expression)); switch (p.currentTag()) { .end_paren => { @@ -966,7 +1077,7 @@ fn parseIf(p: *Parse) Error!NodeIndex { const main_token = p.assertToken(.kw_if); - const cond = try p.parseExpression(false); + const cond = try p.parseExpression(.preceding_block); _ = try p.expectWithoutAdvance(.start_brace); const yes = try p.parseBlock(); @@ -1012,7 +1123,7 @@ fn parseGroup(p: *Parse) Error!NodeIndex { const main_token = p.assertToken(.start_paren); - const expr = try p.parseExpression(false); + const expr = try p.parseExpression(.expression); _ = try p.expect(.end_paren); @@ -1025,6 +1136,74 @@ fn parseGroup(p: *Parse) Error!NodeIndex { }); } +fn parseAnonInstantiation(p: *Parse) Error!NodeIndex { + const trace = t.trace(@src()); + defer trace.end(); + + _ = p.assertToken(.dot); + + const main_token = try p.expect(.start_brace); + const initializers = try p.parseStructFieldList(); + return try p.addNode(.{ + .tag = .struct_init, + .main_token = main_token, + .data = .{ .struct_init = .{ + .base = .none, + .extra = try p.addExtra(Ast.SubList, initializers), + } }, + }); +} + +fn parseAnonFunction(p: *Parse) Error!NodeIndex { + const trace = t.trace(@src()); + defer trace.end(); + + const main_token = p.assertToken(.kw_fn); + + const params = switch (p.currentTag()) { + // fn { 1 } + .start_brace => SubList.empty, + // fn (args, here) { 2 } + .start_paren => param_list: { + _ = p.assertToken(.start_paren); + break :param_list try p.parseParamList(.types_optional); + }, + // fn arg { 3 } + .id => single_param: { + const scratch_top = p.scratch.items.len; + defer p.scratch.shrinkRetainingCapacity(scratch_top); + try p.scratch.append(p.gpa, try p.blankNode(.id)); + break :single_param try p.scratchToSubList(scratch_top); + }, + else => return p.failUnexpectedToken(.param_list), + }; + + const return_type = if (p.eatToken(.minus_lt)) |_| + if (!p.isExpressionStart()) + return p.failUnexpectedToken(.expression) + else + try p.parseExpression(.preceding_block) + else + null; + + try p.expectWithoutAdvance(.start_brace); + const body = try p.parseBlock(); + + const extra = try p.addExtra(Ast.Fn, .{ + .params = params, + .return_type = optionalNodeIndex(return_type), + }); + + return try p.addNode(.{ + .tag = .fn_expr, + .main_token = main_token, + .data = .{ .function = .{ + .extra = extra, + .body = body, + } }, + }); +} + // helpers /// Append a node and return its index. diff --git a/src/core/Report.zig b/src/core/Report.zig index d26557fd9ff49a5b8605362553efe55de478ea1a..6a7cea3a71c2c985c5b5c04676278965c3b7ccf0 100644 --- a/src/core/Report.zig +++ b/src/core/Report.zig @@ -174,8 +174,8 @@ pub const Err = union(enum) { fn build(data: @This(), ctx: *MsgContext) !void { const node = try data.src.get(ctx); try ctx.msgNode(node, "expected \"{s}\", got \"{s}\"", .{ - data.to.displayName(&ctx.comp.intern_pool), - data.from.displayName(&ctx.comp.intern_pool), + data.to.displayName(ctx.ip()), + data.from.displayName(ctx.ip()), }); } }, @@ -186,7 +186,7 @@ pub const Err = union(enum) { fn build(data: @This(), ctx: *MsgContext) !void { const node = try data.call.get(ctx); try ctx.msgNode(node, "\"{s}\" is not callable", .{ - data.ty.displayName(&ctx.comp.intern_pool), + data.ty.displayName(ctx.ip()), }); } }, @@ -210,7 +210,7 @@ pub const Err = union(enum) { fn build(data: @This(), ctx: *MsgContext) !void { const node = try data.src.get(ctx); try ctx.msgNode(node, "type \"{}\" cannot be represented as a runtime value", .{ - data.type.displayName(&ctx.comp.intern_pool), + data.type.displayName(ctx.ip()), }); } }, @@ -221,7 +221,7 @@ pub const Err = union(enum) { fn build(data: @This(), ctx: *MsgContext) !void { const node = try data.src.get(ctx); try ctx.msgNode(node, "parameter of comptime type \"{s}\" must be marked \"comptime\"", .{ - data.type.displayName(&ctx.comp.intern_pool), + data.type.displayName(ctx.ip()), }); } }, @@ -275,8 +275,24 @@ pub const Err = union(enum) { "\"{s}\" operator is not compatible with types \"{}\" and \"{}\"", .{ ctx.ast.tokenSlice(ctx.ast.nodeToken(node)), - data.lhs.displayName(&ctx.comp.intern_pool), - data.rhs.displayName(&ctx.comp.intern_pool), + data.lhs.displayName(ctx.ip()), + data.rhs.displayName(ctx.ip()), + }, + ); + } + }, + un_op_mismatch: struct { + src: LazyNode, + ty: Type, + + fn build(data: @This(), ctx: *MsgContext) !void { + const node = try data.src.get(ctx); + try ctx.msgNode( + node, + "\"{s}\" operator is not compatible with type \"{}\"", + .{ + ctx.ast.tokenSlice(ctx.ast.nodeToken(node)), + data.ty.displayName(ctx.ip()), }, ); } @@ -293,6 +309,96 @@ pub const Err = union(enum) { ); } }, + struct_init_invalid_type: struct { + ty: Type, + src: LazyNode, + + fn build(data: @This(), ctx: *MsgContext) !void { + const node = try data.src.get(ctx); + try ctx.msgNode( + node, + "\"{}\" is not constructable via struct initialization syntax", + .{data.ty.displayName(ctx.ip())}, + ); + } + }, + struct_init_unknown_field: struct { + ty: Type, + src: LazyNode, + + fn build(data: @This(), ctx: *MsgContext) !void { + const node = try data.src.get(ctx); + try ctx.msgNode( + node, + "\"{}\" does not have field {}", + .{ data.ty.displayName(ctx.ip()), formatId("field") }, + ); + } + }, + struct_init_duplicate_field: struct { + ty: Type, + src: LazyNode, + + fn build(data: @This(), ctx: *MsgContext) !void { + const node = try data.src.get(ctx); + try ctx.msgNode( + node, + "duplicate field {}", + .{formatId("field")}, + ); + } + }, + struct_init_missing_field: struct { + ty: Type, + src: LazyNode, + field: u32, + + fn build(data: @This(), ctx: *MsgContext) !void { + const node = try data.src.get(ctx); + try ctx.msgNode( + node, + "{} is missing field {}", + .{ + data.ty.displayName(ctx.ip()), + formatId( + data.ty.toStructType(ctx.ip()).? + .field_keys[data.field].toSlice(ctx.ip()), + ), + }, + ); + } + }, + + field_lookup_failed: struct { + ty: Type, + src: LazyNode, + field: Value, + + fn build(data: @This(), ctx: *MsgContext) !void { + const node = try data.src.get(ctx); + try ctx.msgNode( + node, + "{} is not a field on \"{}\"", + .{ + formatId(data.field.asString(ctx.ip()).toSlice(ctx.ip())), + data.ty.displayName(ctx.ip()), + }, + ); + } + }, + field_invalid_type: struct { + ty: Type, + src: LazyNode, + + fn build(data: @This(), ctx: *MsgContext) !void { + const node = try data.src.get(ctx); + try ctx.msgNode( + node, + "\"{}\" does not support field access", + .{data.ty.displayName(ctx.ip())}, + ); + } + }, }; pub const Warn = union(enum) { @@ -783,8 +889,8 @@ pub const Highlight = union(enum) { /// This serves as a small API to make creating messages easier. pub const MsgContext = struct { - file: *File, - comp: *Comp, + file: *const File, + comp: *const Comp, ast: *const Ast, report: *const Report, utf8_source: [:0]const u8, @@ -819,6 +925,10 @@ pub const MsgContext = struct { pub fn msgAlone(m: *MsgContext, comptime fmt: []const u8, args: anytype) !void { return m.msg(.alone, fmt, args); } + + pub fn ip(ctx: *const MsgContext) *const InternPool { + return &ctx.comp.intern_pool; + } }; pub const LazyNode = struct { @@ -828,7 +938,7 @@ pub const LazyNode = struct { pub fn get(lazy: LazyNode, ctx: *MsgContext) !NodeIndex { const decl = ctx.comp.intern_pool.declPtrConst(lazy.decl); - const ns = ctx.comp.intern_pool.namespacePtr(decl.src_namespace); + const ns = ctx.comp.intern_pool.namespacePtrConst(decl.src_namespace); // TODO: This might change if we have errors like // - function arguments not matching at @@ -845,10 +955,9 @@ pub const LazyNode = struct { // this will be required once incremental compilation hits @panic("TODO: teg was unloaded")); - const decl_inst = decl.teg_decl_index.unwrap() orelse - unreachable; // file was not analyzed, yet teg loaded? + const decl_inst = decl.teg_decl_index; - const teg_decl = teg.instData(.declaration, decl_inst.resolve(&ctx.comp.intern_pool)); + const teg_decl = teg.instData(.declaration, decl_inst.resolve(ctx.ip())); const node_offset = teg.instNodeOffset(lazy.inst); @@ -903,7 +1012,7 @@ pub const LazyNode = struct { }, .declaration => { assert(node_tag == .fn_decl); - const fn_decl = ctx.ast.extraData(data.fn_decl.extra, Ast.FnDecl); + const fn_decl = ctx.ast.extraData(data.function.extra, Ast.Fn); return switch (lazy.operand) { .return_type => fn_decl.return_type.unwrap().?, else => fn_decl.params.slice(ctx.ast)[lazy.operand.getArgId()], @@ -912,11 +1021,24 @@ pub const LazyNode = struct { .js => { return base_node; }, + .struct_init => { + assert(node_tag == .struct_init); + switch (lazy.operand) { + .operand => return data.struct_init.base.unwrap() orelse + // for anon struct initializations, this will point at the '.' + base_node, + else => { + const fields = ctx.ast + .extraData(data.struct_init.extra, Ast.SubList) + .slice(ctx.ast); + const field = fields[lazy.operand.getArgId()]; + return ctx.ast.nodeData(field).un.expr; + }, + } + }, else => {}, } - if (!std.debug.runtime_safety) unreachable; - std.debug.panic("TODO: operand lookup: .{s} + .{s} + {}", .{ @tagName(teg.instTag(lazy.inst)), @tagName(ctx.ast.nodeTag(base_node)), @@ -1020,7 +1142,7 @@ fn formatIdImpl( // differences in the string formatters. Ideally these are merged // under a common function that takes an argument of the 'mode' to // print the string in. - const JSLink = @import("JsLink.zig"); + const JSLink = @import("mod.zig").Js.Link; const indexOfNeedsEscape = JSLink.indexOfNeedsEscape; const canPrintWithoutEscape = JSLink.canPrintWithoutEscape; diff --git a/src/core/Sema.zig b/src/core/Sema.zig index c2eccbe2c08f042077b14915da4e1bb9148b3a5e..6c8bda188678d81b17f6e8ec702d2974140d6686 100644 --- a/src/core/Sema.zig +++ b/src/core/Sema.zig @@ -1,5 +1,28 @@ /// absolutely stealing zig on this one haha /// This struct is created per declaration and chills +/// +/// TODO: explain this in more depth. or maybe take this to architecture.md +/// +/// All things in semantic analysis are either Air.Inst.Index (runtime known +/// value pointing to the instruction that computed it), or Value (comptime known) +/// +/// There is a union of these two called Ref [Air.Inst.Ref]. Generally, +/// functions referring to Ref refer to runtime known values. +/// +/// Many things have two versions, one for Ref (runtime) + one for Value (comptime) +/// +/// Example is going from Teg.Inst.Index to the result of it's analysis: +/// - s.resolveRef() +/// - s.resolveValue() +/// The latter will ensure the result is a Value (comptime) and return that. +/// +/// Many things in Semantic Analysis don't have a way to represent constants +/// using Air.Inst.Index, and we also want to avoid emitting things like `1 + 2`. +/// +/// We do that by using Air.Inst.Ref.value() or .decode(), which allows +/// observing if the value is known at compile time. This makes analysis +/// lazy and forces constant folding whenever possible. (see .bool_not for a +/// simple example) // inputs: comp: *Comp, @@ -16,12 +39,14 @@ inst_map: InstMap = undefined, /// scratch memory. can be used in any place you might temporarily allocate an arraylist /// this is most notably used to store block contents before the block is finalized scratch: std.ArrayListUnmanaged(u32) = .{}, + +/// This is only set for function declarations params: []Type = undefined, // outputs report: Report.Builder = undefined, air_instructions: std.MultiArrayList(Air.Inst) = .{}, -air_extra: dod.ExtraStore(.air, .@"var") = .{}, +air_extra: store.ExtraStore(.air, .@"var") = .{}, temp_result_map: std.AutoArrayHashMapUnmanaged(Teg.Inst.Index, Type) = .{}, @@ -41,7 +66,7 @@ pub const Error = error{ OutOfMemory, } || Comp.File.GetTegError; -/// Instead of using a hash map for this, just pre-allocate a list long enough to hold everything. +/// Instead of using a Hash map for this, just pre-allocate a list long enough to hold everything. /// This is fine because all instructions are going to be adjacent in a single declaration. const InstMap = struct { items: []Air.Inst.Ref.Optional, @@ -70,10 +95,11 @@ const InstMap = struct { } pub fn add(map: *InstMap, from: Teg.Inst.Index, to: Air.Inst.Ref) !void { - if (assertWithMessage(map.get(from) == null)) { - std.debug.panic("%{d} is already mapped (to #{d})", .{ + if (assertFailed(map.get(from) == null)) { + const s: *const Sema = @fieldParentPtr("inst_map", map); + std.debug.panic("%{d} is already mapped (to {})", .{ @intFromEnum(from), - @intFromEnum(map.get(from).?.decode().index), + map.get(from).?.dump(s.ip), }); } const source_i = @intFromEnum(from); @@ -91,12 +117,10 @@ pub fn init(s: *Sema) !void { .current_file = s.ip.namespacePtr(s.namespace_index).file_scope, }; - try s.comp.current_compile_decls.append(s.gpa, s.decl_index); - s.inst_map = try InstMap.init( s.arena, &s.code, - s.ip.declPtr(s.decl_index).teg_decl_index.unwrap().?.resolve(s.ip), + s.ip.declPtr(s.decl_index).teg_decl_index.resolve(s.ip), ); } @@ -117,21 +141,55 @@ pub fn deinit(s: *Sema) void { } pub fn finalize(s: *Sema) !void { + s.decl.analysis = .complete; + const air: Air = s.tempAir(); - if (s.comp.options.dump_ir) { - std.debug.print("=" ** 20, .{}); - std.debug.print("\nAir for @ {d}\n", .{@intFromEnum(s.decl.teg_decl_index)}); - air.dumpToWriter(s.ip, std.io.getStdErr().writer()) catch {}; - } + if (s.decl.value().valueTag(s.ip) == .func) { + if (s.comp.options.dump_ir) { + std.debug.print("=" ** 20, .{}); + std.debug.print("\nAir for {}\n", .{s.decl.value().dump(s.ip)}); + air.dumpToWriter(s.ip, std.io.getStdErr().writer()) catch {}; + } - try s.comp.codeGenDecl(s.decl, &air); + try s.comp.codeGenDecl(s.decl.value(), &air); + } } // --- analyze functions --- /// This is the entrypoint to semantic analysis: It all starts at a declaration. pub fn analyzeDeclaration(s: *Sema) Error!Value { + const decl = s.code.declData(s.inst_map.start); + switch (decl.kind) { + .function => return analyzeFunctionDeclaration(s), + .immutable => {}, + } + + const root_block = decl.body; + switch (root_block.decode()) { + .value => |val| { + s.decl.raw_value = val; + s.decl.analysis = .complete; + }, + .index => |index| { + const result = try s.analyzeBlock(index); + switch (result.decode()) { + .value => |val| { + s.decl.raw_value = val; + s.decl.analysis = .complete; + }, + .index => @panic("TODO"), + } + }, + } + + return s.decl.value(); +} + +/// TODO: Delete this legacy code. This will be replaced by analyzeDeclaration and a lambda +/// function (See #45) +pub fn analyzeFunctionDeclaration(s: *Sema) Error!Value { const t = tracer.trace(@src()); defer t.end(); @@ -164,7 +222,7 @@ pub fn analyzeDeclaration(s: *Sema) Error!Value { ); const func_prototype = (try s.ip.get(s.gpa, .{ - .type_function = .{ + .function_type = .{ .param_types = s.params, .return_type = return_type, }, @@ -173,14 +231,14 @@ pub fn analyzeDeclaration(s: *Sema) Error!Value { .func = .{ .ty = func_prototype, .owner_decl = s.decl_index, - .teg_body_inst = s.decl.teg_decl_index.unwrap().?, + .teg_body_inst = s.decl.teg_decl_index, }, }); - s.decl.val = value; - s.decl.analysis = .in_progress_prototype_available; + s.decl.raw_value = value.typeOf(s.ip).value(); + s.decl.analysis = .in_progress_type_available; - const root_block = fn_decl.body; + const root_block = decl.body; switch (root_block.decode()) { .value => |val| { const coerced = try s.coerceValue( @@ -200,6 +258,9 @@ pub fn analyzeDeclaration(s: *Sema) Error!Value { }, } + s.decl.analysis = .complete; + s.decl.raw_value = value; + return value; } @@ -268,31 +329,73 @@ fn analyzeCodeInner(s: *Sema, in: Teg.Inst.Index) Error!Air.Inst.Ref { return s.makeValueAir(.{ .string = string.slice(s.code.string_bytes) }); }, .number => { - const number: dod.Float = s.code.instPayload(.number, in); + const number: store.Float = s.code.instPayload(.number, in); return s.makeValueAir(.{ .comptime_float = number.decode() }); }, .js => { const operand_ref = s.code.instData(.js, in).operand; - const operand = try s.resolveRefComptimeType(in.op(.operand), operand_ref, .string); + const operand = try s.resolveCoerceValue(in.op(.operand), operand_ref, .string); return s.addInstRef(.js_global_ref, operand.asString(s.ip)); }, .field => { const field: Teg.Inst.Bin = s.code.instPayload(.field, in); - const base = try s.resolveRefRuntime(field.lhs); + const base = try s.resolveRef(field.lhs); - const rhs = try s.resolveRefComptimeType(in.op(.rhs), field.rhs, .string); + const rhs = try s.resolveCoerceValue(in.op(.rhs), field.rhs, .string); const string = rhs.asString(s.ip); - const base_type = s.typeOfRef(base); - switch (base_type) { - .@"opaque" => { - return s.addInstRef(.field, .{ - .base = base, - .field = try string.tryToNullTerminatedString(s.ip, s.gpa), + const ty = s.typeOfRef(base); + + if (ty == .@"opaque") { + return s.addInstRef(.field_opaque, .{ + .base = base, + .field = string.tryToNullTerminatedString(s.ip, s.gpa) catch |e| switch (e) { + error.HasNulls => @panic("TODO: handle null"), + else => return e, + }, + }); + } + + if (ty.toStructType(s.ip)) |struct_type| { + const found_index: u32 = found_index: { + const lookup = string.tryToNullTerminatedString( + s.ip, + s.gpa, + ) catch |e| switch (e) { + error.HasNulls => break :found_index null, + else => return e, + }; + + break :found_index for (struct_type.field_keys, 0..) |key, i| { + if (key == lookup) + break @as(u32, @intCast(i)); + } else null; + } orelse { + return s.fail(.field_lookup_failed, .{ + .ty = ty, + .src = in.op(.rhs).lazyNode(s.decl_index), + .field = rhs, }); - }, - else => |tag| std.debug.panic("TODO: field analyze with base .{s}", .{@tagName(tag)}), + }; + + switch (base.decode()) { + .value => |value| { + _, const trail = value.valueData(s.ip, .struct_value).getTrail(s.ip); + return trail.at(&s.ip.extra, Value, found_index).toAir(); + }, + .index => |index| { + return s.addInstRef(.field_struct, .{ + .base = index, + .field_index = found_index, + }); + }, + } } + + return s.fail(.field_invalid_type, .{ + .ty = ty, + .src = in.op(.rhs).lazyNode(s.decl_index), + }); }, .load_decl => { const request: Teg.Inst.StrNode = s.code.instData(.load_decl, in); @@ -326,8 +429,8 @@ fn analyzeCodeInner(s: *Sema, in: Teg.Inst.Index) Error!Air.Inst.Ref { const bin: Teg.Inst.Bin = s.code.instPayload(tag, in); const op = @field(BinaryOp, @tagName(tag)); - const lhs = try s.resolveRefRuntime(bin.lhs); - const rhs = try s.resolveRefRuntime(bin.rhs); + const lhs = try s.resolveRef(bin.lhs); + const rhs = try s.resolveRef(bin.rhs); const left_ty = s.typeOfRef(lhs); const right_ty = s.typeOfRef(rhs); @@ -350,23 +453,47 @@ fn analyzeCodeInner(s: *Sema, in: Teg.Inst.Index) Error!Air.Inst.Ref { .rhs = right, }); }, - inline .negate, .bool_not => |tag| { - const un: Teg.Inst.RefNode = s.code.instData(tag, in); + .bool_not => { + const un: Teg.Inst.RefNode = s.code.instData(.bool_not, in); - const operand = try s.resolveRefRuntime(un.operand); + const operand = try s.resolveCoerceRef(in.op(.operand), un.operand, .bool); - const operand_type = s.typeOfRef(operand); - _ = operand_type; // autofix + // Constant propagation + if (operand.value()) |value| { + assert(value == .bool_false or value == .bool_true); + return if (value == .bool_true) + Value.bool_false.toAir() + else + Value.bool_true.toAir(); + } - // TODO: validate type for operation + return s.addInstRef(.bool_not, .{ .operand = operand }); + }, + .negate => { + const un: Teg.Inst.RefNode = s.code.instData(.negate, in); - return s.addInstRef(tag.airTag(), .{ - .operand = operand, - }); + const operand = try s.resolveRef(un.operand); + + const ty = s.typeOfRef(operand); + switch (ty) { + .numeric_literal, .int32, .float => {}, + else => return s.fail(.un_op_mismatch, .{ + .src = in.op(.operand).lazyNode(s.decl_index), + .ty = ty, + }), + } + + if (operand.value()) |value| { + return s.makeValueAir(.{ + .comptime_float = -value.toFloat(s.ip).?, + }); + } + + return s.addInstRef(.negate, .{ .operand = operand }); }, .@"break" => { const brk: Teg.Inst.Break = s.code.instPayload(.@"break", in); - const operand = try s.resolveRefRuntime(brk.operand); + const operand = try s.resolveRef(brk.operand); const block_ref = s.inst_map.get(brk.block).?.decode().index; // block ref is resolved by teg const coerced = if (s.temp_result_map.get(brk.block)) |ty| @@ -381,7 +508,7 @@ fn analyzeCodeInner(s: *Sema, in: Teg.Inst.Index) Error!Air.Inst.Ref { }, .call => { const call: Teg.Inst.Call = s.code.instPayload(.call, in); - const base = try s.resolveRefRuntime(call.base); + const base = try s.resolveRef(call.base); const base_type = s.typeOfRef(base); const scratch = try s.arena.alloc(Air.Inst.Ref, call.args.len); @@ -405,7 +532,7 @@ fn analyzeCodeInner(s: *Sema, in: Teg.Inst.Index) Error!Air.Inst.Ref { // validate function args for (proto.param_types, call.args.slice(&s.code), 0..) |param, arg, i| { - const ref = try s.resolveRefRuntime(arg); + const ref = try s.resolveRef(arg); const coerced = try s.coerceRef( in.op(Report.Operand.argument(@intCast(i))), ref, @@ -417,7 +544,7 @@ fn analyzeCodeInner(s: *Sema, in: Teg.Inst.Index) Error!Air.Inst.Ref { for (call.args.slice(&s.code), 0..) |arg, i| { const ref = try s.ensureValueRuntime( in.op(Report.Operand.argument(@intCast(i))), - try s.resolveRefRuntime(arg), + try s.resolveRef(arg), ); scratch[i] = ref; } @@ -438,14 +565,14 @@ fn analyzeCodeInner(s: *Sema, in: Teg.Inst.Index) Error!Air.Inst.Ref { }, .js_new => { const call: Teg.Inst.JsNew = s.code.instPayload(.js_new, in); - const base = try s.resolveRefRuntime(call.base); + const base = try s.resolveRef(call.base); const scratch = try s.arena.alloc(Air.Inst.Ref, call.args.len); defer s.arena.free(scratch); for (call.args.slice(&s.code), 0..) |arg, i| { const ref = try s.ensureValueRuntime( in.op(Report.Operand.argument(@intCast(i))), - try s.resolveRefRuntime(arg), + try s.resolveRef(arg), ); scratch[i] = ref; } @@ -462,7 +589,7 @@ fn analyzeCodeInner(s: *Sema, in: Teg.Inst.Index) Error!Air.Inst.Ref { }); }, .ensure_void => { - const operand = try s.resolveRefRuntimeExtra(s.code.instData(.ensure_void, in).operand, false); + const operand = try s.resolveRefExtra(s.code.instData(.ensure_void, in).operand, false); const t = s.typeOfRef(operand); _ = t; // if (!t.isEnsureVoid(s.ip)) { @@ -473,7 +600,7 @@ fn analyzeCodeInner(s: *Sema, in: Teg.Inst.Index) Error!Air.Inst.Ref { return InternPool.Value.void.toAir(); }, .discard => { - const operand = try s.resolveRefRuntimeExtra(s.code.instData(.discard, in).operand, false); + const operand = try s.resolveRefExtra(s.code.instData(.discard, in).operand, false); const t = s.typeOfRef(operand); if (t == .void) { std.debug.print("oh no, we have a useless _ = void;\n", .{}); @@ -483,7 +610,7 @@ fn analyzeCodeInner(s: *Sema, in: Teg.Inst.Index) Error!Air.Inst.Ref { .@"if" => { const data: Teg.Inst.If = s.code.instData(.@"if", in).extra_index.get(&s.code); - const cond_raw = try s.resolveRefRuntime(data.cond); + const cond_raw = try s.resolveRef(data.cond); const cond = try s.coerceRef(in.op(.operand), cond_raw, .bool); // If known at comptime, only analyze the taken path @@ -517,7 +644,7 @@ fn analyzeCodeInner(s: *Sema, in: Teg.Inst.Index) Error!Air.Inst.Ref { .memory => { const data = s.code.instData(.memory, in); - const initial_value = try s.resolveRefRuntime(data.operand); + const initial_value = try s.resolveRef(data.operand); const runtime_known = try s.ensureValueRuntime( in.op(.operand), @@ -535,7 +662,7 @@ fn analyzeCodeInner(s: *Sema, in: Teg.Inst.Index) Error!Air.Inst.Ref { const location = s.mapTeg(data.memory.toRef()).decode().index; s.markInst(location); - const operand = try s.resolveRefRuntime(data.operand); + const operand = try s.resolveRef(data.operand); const operand_coerced = try s.coerceRef( in.op(.rhs), operand, @@ -548,23 +675,23 @@ fn analyzeCodeInner(s: *Sema, in: Teg.Inst.Index) Error!Air.Inst.Ref { }, .as => { const as: Teg.Inst.As = s.code.instPayload(.as, in); - const coerce_to = try s.resolveRefComptimeType(in.op(.lhs), as.type, .type); - const operand = try s.resolveRefRuntime(as.operand); + const coerce_to = try s.resolveCoerceValue(in.op(.lhs), as.type, .type); + const operand = try s.resolveRef(as.operand); return try s.coerceRef(in.op(.rhs), operand, coerce_to.toType(s.ip).?); }, .@"comptime" => { const node: Teg.Inst.RefNode = s.code.instData(.@"comptime", in); - const value = try s.resolveRefComptimeAny(node.operand); + const value = try s.resolveValue(node.operand); return value.toAir(); }, .type_of => { const node: Teg.Inst.RefNode = s.code.instData(.type_of, in); - const value = try s.resolveRefComptimeAny(node.operand); + const value = try s.resolveValue(node.operand); return value.typeOf(s.ip).value().toAir(); }, .compile_error => { const node: Teg.Inst.RefNode = s.code.instData(.compile_error, in); - const value = try s.resolveRefComptimeType( + const value = try s.resolveCoerceValue( in.op(Report.Operand.argument(0)), node.operand, .string, @@ -575,6 +702,162 @@ fn analyzeCodeInner(s: *Sema, in: Teg.Inst.Index) Error!Air.Inst.Ref { .msg = try s.report.addString(str), }); }, + .struct_decl => { + const node: Teg.Inst.StructDecl = s.code.instPayload(.struct_decl, in); + + const scratch_top = s.scratch.items.len; + try s.scratch.ensureUnusedCapacity(s.arena, node.field_len * 2); + s.scratch.items.len += node.field_len * 2; + defer s.scratch.shrinkRetainingCapacity(scratch_top); + + for ( + node.keys(&s.code), + // Keeping the slice pointer is fine for this loop, since scratch is not resized + s.scratch.items[scratch_top..][0..node.field_len], + ) |key, *dest| { + dest.* = @intFromEnum(try s.ip.getOrPutString( + s.gpa, + key.slice(s.code.string_bytes), + .no_embedded_nulls, + )); + } + + for ( + node.types(&s.code), + // We cannot reuse the pointer from scratch between calls to sema + scratch_top + node.field_len.., + 0.., + ) |ty, i, arg_i| { + s.scratch.items[i] = @intFromEnum(try s.resolveCoerceValue( + in.op(Report.Operand.argument(@intCast(arg_i))), + ty, + .type, + )); + } + + const keys = s.scratch.items[scratch_top..][0..node.field_len]; + const types = s.scratch.items[scratch_top + node.field_len ..][0..node.field_len]; + + return try s.makeValueAir(.{ .struct_type = .{ + .field_keys = @ptrCast(keys), + .field_types = @ptrCast(types), + } }); + }, + .struct_init => { + const node: Teg.Inst.StructInit = s.code.instPayload(.struct_init, in); + + // Ensure we have a valid struct to operate on + const ty = try s.resolveType(in.op(.operand), node.base_type); + const struct_type = ty.toStructType(s.ip) orelse return s.fail( + .struct_init_invalid_type, + .{ + .src = in.op(.operand).lazyNode(s.decl_index), + .ty = ty, + }, + ); + + var has_errors = false; + var is_constant = true; + + // Allocate scratch space to store the values as they are being prepared. + // TODO: make this an abstraction + const scratch_top = s.scratch.items.len; + + try s.scratch.ensureUnusedCapacity(s.arena, struct_type.field_types.len); + const storage: []Air.Inst.Ref.Optional = + @ptrCast(s.scratch.unusedCapacitySlice()[0..struct_type.field_types.len]); + s.scratch.items.len += storage.len; + @memset(storage, .none); + + const scratch_end = s.scratch.items.len; + defer { + const from = s.scratch.items[scratch_end..]; + const dest = s.scratch.items[scratch_top..][0..from.len]; + std.mem.copyForwards(u32, dest, from); + s.scratch.shrinkRetainingCapacity(scratch_top + from.len); + } + + for (node.keys(&s.code), node.inits(&s.code), 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.code.string_bytes), s.ip)) + break :index j; + } else { + has_errors = true; + try s.addError(.struct_init_unknown_field, .{ + .ty = ty, + .src = in.op(Report.Operand.argument(@intCast(i))).lazyNode(s.decl_index), + }); + continue; + }; + if (storage[found_index] != .none) { + has_errors = true; + try s.addError(.struct_init_duplicate_field, .{ + .ty = ty, + .src = in.op(Report.Operand.argument(@intCast(i))).lazyNode(s.decl_index), + }); + continue; + } + // TODO: calling coerceRef / resolveRefRuntime will clobber the `storage` pointer + const coerced = s.resolveCoerceRef( + in.op(Report.Operand.argument(@intCast(i))), + init_value, + struct_type.field_types[found_index], + ) catch |e| switch (e) { + error.AnalysisFail => { + // Instead of bailing out with `try`, we will continue the loop. + has_errors = true; + // Mark something non-null so that duplicate field errors are emitted. + storage[found_index] = comptime Value.void.toAir().toOptional(); + continue; + }, + else => return e, + }; + if (coerced.kind == .index) is_constant = false; + storage[found_index] = coerced.toOptional(); + } + + // Check for missing fields + { + 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 = in.op(.main_node).lazyNode(s.decl_index), + }); + i = index + 1; + } + if (i != 0) has_errors = true; + } + + if (has_errors) + return Error.AnalysisFail; + + if (is_constant) { + // Convert all items in storage into a Value type + const result_storage: []Value = @ptrCast(storage); + for (storage, result_storage) |ref, *value| { + value.* = ref.unwrap().?.decode().value; + } + return try s.makeValueAir(.{ .struct_value = .{ + .ty = ty, + .field_values = result_storage, + } }); + } else { + // TODO: make this less error prone + const result_storage: []u32 = @ptrCast(storage); + const value_index = try s.air_extra.alloc(s.gpa, result_storage.len); + @memcpy(s.air_extra.list.items[value_index..].ptr, result_storage); + return try s.addInstRef(.struct_init, .{ + .ty = ty, + .field_values = value_index, + }); + } + }, else => |tag| std.debug.panic("TODO: analyze of .{s}", .{@tagName(tag)}), } } @@ -591,7 +874,7 @@ fn analyzeRefToBlock(s: *Sema, in: Teg.Inst.Ref) Error!Air.Inst.Ref { const scratch_top = s.scratch.items.len; defer s.scratch.shrinkRetainingCapacity(scratch_top); - const visited = s.resolveRefRuntime(in) catch |err| switch (err) { + const visited = s.resolveRef(in) catch |err| switch (err) { error.HitNoReturn => Value.void.toAir(), else => return err, }; @@ -614,7 +897,7 @@ fn analyzeRefToBlock(s: *Sema, in: Teg.Inst.Ref) Error!Air.Inst.Ref { return (try block.finish(s, scratch_top)).toRef(); } -fn resolveRefRuntimeExtra(s: *Sema, ref: Teg.Inst.Ref, mark: bool) Error!Air.Inst.Ref { +fn resolveRefExtra(s: *Sema, ref: Teg.Inst.Ref, mark: bool) Error!Air.Inst.Ref { const index = ref.toIndex() orelse { return ref.toValue().?.toAir(); }; @@ -632,11 +915,11 @@ fn resolveRefRuntimeExtra(s: *Sema, ref: Teg.Inst.Ref, mark: bool) Error!Air.Ins return s.analyzeCode(index); } -fn resolveRefRuntime(s: *Sema, ref: Teg.Inst.Ref) Error!Air.Inst.Ref { - return resolveRefRuntimeExtra(s, ref, true); +fn resolveRef(s: *Sema, ref: Teg.Inst.Ref) Error!Air.Inst.Ref { + return resolveRefExtra(s, ref, true); } -fn resolveRefComptimeAny(s: *Sema, ref: Teg.Inst.Ref) !Value { +fn resolveValue(s: *Sema, ref: Teg.Inst.Ref) !Value { const index = switch (ref.decode()) { .value => |value| return value, .index => |index| index, @@ -655,21 +938,69 @@ fn resolveRefComptimeAny(s: *Sema, ref: Teg.Inst.Ref) !Value { return s.makeValue(.{ .string = string.slice(s.code.string_bytes) }); }, - else => |tag| std.debug.panic("TODO: comptime analyze of .{s}", .{@tagName(tag)}), + .block => { + // Currently only one pattern is supported: + // + // %2 = block [ + // %1 = load_decl X + // %3 = break(%3, %2) + // ] + // + // This is to support function arguments with structs. + // The rest of this will be implemented in https://github.com/paperdave/wl/issues/44 + special_cased: { + const index_list: Teg.IndexList = s.code.instPayload(.block, index); + if (index_list.len != 2) break :special_cased; + const indexes = index_list.slice(&s.code); + const load_inst = indexes[0]; + const break_inst = indexes[1]; + + if (s.code.instTag(break_inst) != .@"break") break :special_cased; + const break_data: Teg.Inst.Break = s.code.instPayload(.@"break", break_inst); + if (break_data.operand != load_inst.toRef()) break :special_cased; + if (break_data.block != index) break :special_cased; + + if (s.code.instTag(load_inst) != .load_decl) break :special_cased; + + const request: Teg.Inst.StrNode = s.code.instData(.load_decl, load_inst); + const decl_name = try s.ip.getOrPutString( + s.gpa, + request.string.slice(s.code.string_bytes), + .no_embedded_nulls, + ); + const decl_index = s.lookupDecl(decl_name) orelse { + std.debug.print("did not find {s}\n", .{decl_name.toSlice(s.ip)}); + return error.AnalysisFail; + }; + return try s.resolveDecl(load_inst.op(.main_node), decl_index); + } + + std.debug.panic("TODO: advanced comptime interpreter", .{}); + }, + + else => |tag| std.debug.panic("TODO: comptime analyze of .{s} {d}", .{ @tagName(tag), @intFromEnum(index) }), } } -fn resolveRefComptimeType(s: *Sema, src: Teg.InstOp, ref: Teg.Inst.Ref, ty: Type) !Value { - const any = try s.resolveRefComptimeAny(ref); +fn resolveCoerceValue(s: *Sema, src: Teg.InstOp, ref: Teg.Inst.Ref, ty: Type) !Value { + const any = try s.resolveValue(ref); return try s.coerceValue(src, any, ty); } +fn resolveCoerceRef(s: *Sema, src: Teg.InstOp, ref: Teg.Inst.Ref, ty: Type) !Air.Inst.Ref { + const any = try s.resolveRef(ref); + return try s.coerceRef(src, any, ty); +} + fn resolveType(s: *Sema, src: Teg.InstOp, ref: Teg.Inst.Ref) !Type { - return (try s.resolveRefComptimeType(src, ref, .type)).toTypeUnsafe(s.ip); + return (try s.resolveCoerceValue(src, ref, .type)).toTypeUnsafe(s.ip); } -/// Coerce `value` runtime ref to `coerce_type`, and emit an error at `teg_ref`'s location +/// Coerce `value` runtime ref to `coerce_type`, and emit an error at `src`'s location fn coerceRef(s: *Sema, src: InstOp, value: Air.Inst.Ref, to: Type) !Air.Inst.Ref { + const t = tracer.traceFmt(@src(), "{} to {}", .{ value.dump(s.ip), to.dump(s.ip) }); + defer t.end(); + const from = s.typeOfRef(value); if (from == to) { return value; @@ -721,6 +1052,7 @@ fn coerceRef(s: *Sema, src: InstOp, value: Air.Inst.Ref, to: Type) !Air.Inst.Ref return s.coercionError(src, from, to); } +/// Same as `coerceRef` but on a compile-time known value fn coerceValue(s: *Sema, src: InstOp, value: Value, to: Type) !Value { const from = value.typeOf(s.ip); if (from == to) { @@ -728,11 +1060,10 @@ fn coerceValue(s: *Sema, src: InstOp, value: Value, to: Type) !Value { } switch (to) { .@"opaque" => { - - // + // TODO: }, .bool => { - // + // TODO: }, .int32 => switch (from) { .int32 => unreachable, // same type handled @@ -818,8 +1149,10 @@ fn ensureValueRuntime(s: *Sema, src: InstOp, ref: Air.Inst.Ref) !Air.Inst.Ref { => {}, _ => switch (ty.tag(s.ip)) { .simple_type => unreachable, // handled above + // Allowed at runtime - .type_function => {}, + .function_type => {}, + .struct_type => {}, }, } }, @@ -867,7 +1200,7 @@ fn resolveDecl(s: *Sema, src: InstOp, decl_index: Decl.Index) Error!Value { }); }, else => { - return decl.val.?; + return decl.value(); }, } } @@ -904,8 +1237,12 @@ const BinaryOp = enum { }; fn binTypeResolution(s: *Sema, src: Teg.Inst.Index, op: BinaryOp, lhs: Type, rhs: Type) !Type { - if (lhs == .@"opaque") return rhs; - if (rhs == .@"opaque") return rhs; + if (lhs == .@"opaque") { + return if (rhs == .numeric_literal) .float else rhs; + } + if (rhs == .@"opaque") { + return if (lhs == .numeric_literal) .float else lhs; + } switch (lhs) { .string => if (op == .add) switch (rhs) { @@ -1016,9 +1353,7 @@ fn comptimeBin(s: *Sema, op: BinaryOp, lhs: Value, rhs: Value) !Value { }, else => unreachable, }, - inline .lt, - .gt, - => |tag| { + inline .lt, .gt => |tag| { const l = lhs.toFloat(s.ip) orelse unreachable; // must be coerced const r = rhs.toFloat(s.ip) orelse @@ -1034,8 +1369,7 @@ fn comptimeBin(s: *Sema, op: BinaryOp, lhs: Value, rhs: Value) !Value { if (result) .bool_true else .bool_false, ); }, - .eq, - => { + .eq => { const result = lhs == rhs; return @as( InternPool.Value, @@ -1045,7 +1379,6 @@ fn comptimeBin(s: *Sema, op: BinaryOp, lhs: Value, rhs: Value) !Value { } } -/// --- helper --- const IncompleteBlock = struct { block_index: Air.Inst.Index, @@ -1177,30 +1510,26 @@ const math = std.math; const Allocator = std.mem.Allocator; const assert = std.debug.assert; const log = std.log.scoped(.sema); +const DynamicBitSetUnmanaged = std.DynamicBitSetUnmanaged; -inline fn assertWithMessage(cond: bool) bool { - if (std.debug.runtime_safety) { - return !cond; - } else if (!cond) { - unreachable; // assertion failure - } -} +const core = @import("mod.zig"); +const assertFailed = core.assertFailed; -const Comp = @import("Comp.zig"); +const Comp = core.Comp; const File = Comp.File; const Decl = Comp.Decl; const Namespace = Comp.Namespace; -const Teg = @import("Teg.zig"); +const Teg = core.Teg; const InstOp = Teg.InstOp; -const Air = @import("Air.zig"); -const Report = @import("Report.zig"); -const Ast = @import("Ast.zig"); -const dod = @import("array.zig"); +const Air = core.Air; +const Report = core.Report; +const Ast = core.Ast; +const store = core.array; const tracer = @import("tracer"); -const InternPool = @import("InternPool.zig"); +const InternPool = core.InternPool; const Value = InternPool.Value; const Type = InternPool.Type; const NullTerminatedString = InternPool.NullTerminatedString; diff --git a/src/core/Teg.Dump.zig b/src/core/Teg.Dump.zig index 66d5768ff7bef497ef43b67e40b750f84daaef37..0c77e875872cb4bdc0e51e5ae66b1b086017c277 100644 --- a/src/core/Teg.Dump.zig +++ b/src/core/Teg.Dump.zig @@ -46,10 +46,10 @@ const State = struct { try s.dumpInstList(writer, teg.moduleDeclarations(inst), '{', '}'); }, .declaration => { - const decl_info = teg.declData(inst); - try writer.print(" ({s}) ", .{@tagName(decl_info.kind)}); + const decl = teg.fullDecl(inst); + try writer.print(" ({s}) ", .{@tagName(decl.data)}); - const name = decl_info.name.slice(teg.string_bytes); + const name = decl.name.slice(teg.string_bytes); if (name.len == 0) { try tty.setColor(writer, .bold); try writer.writeAll(""); @@ -63,19 +63,19 @@ const State = struct { s.indent += 1; try s.newline(writer); - switch (decl_info.kind) { - .function => { - const fn_data: Teg.Inst.DeclFn = teg.fullFuncDecl(inst).@"1"; + switch (decl.data) { + .function => |fn_data| { try writer.writeAll("params: "); try s.dumpRefList(writer, fn_data.params.slice(teg), '[', ']'); try s.newline(writer); try writer.writeAll("return_type: "); try s.dumpRef(writer, fn_data.return_type); try s.newline(writer); - try writer.writeAll("body: "); - try s.dumpRef(writer, fn_data.body); }, + .immutable => {}, } + try writer.writeAll("body: "); + try s.dumpRef(writer, decl.body); s.indent -= 1; try s.newline(writer); try writer.writeAll("}"); @@ -223,6 +223,38 @@ const State = struct { try s.dumpRef(writer, data.operand); try writer.writeAll(")"); }, + .struct_decl => { + const data: Teg.Inst.StructDecl = teg.instPayload(.struct_decl, inst); + try writer.writeAll(" {"); + s.indent += 1; + try s.newline(writer); + for (data.keys(teg), data.types(teg), 0..) |key, value, i| { + if (i != 0) + try s.newline(writer); + try writer.print("{s}: ", .{key.slice(teg.string_bytes)}); + try s.dumpRefMarker(writer, value); + } + s.indent -= 1; + try s.newline(writer); + try writer.writeAll("}"); + }, + .struct_init => { + const data: Teg.Inst.StructInit = teg.instPayload(.struct_init, inst); + try writer.writeAll("("); + try s.dumpRefMarker(writer, data.base_type); + try writer.writeAll(") {"); + s.indent += 1; + try s.newline(writer); + for (data.keys(teg), data.inits(teg), 0..) |key, value, i| { + if (i != 0) + try s.newline(writer); + try writer.print("{s}: ", .{key.slice(teg.string_bytes)}); + try s.dumpRefMarker(writer, value); + } + s.indent -= 1; + try s.newline(writer); + try writer.writeAll("}"); + }, } } diff --git a/src/core/Teg.zig b/src/core/Teg.zig index e277b8711a21a19c5375f8d74076a199691f4600..d67c89ab0e6ab7f8c795cc3f8b56ce1e8353d820 100644 --- a/src/core/Teg.zig +++ b/src/core/Teg.zig @@ -16,7 +16,7 @@ pub const Inst = struct { tag: Tag, data: EncodedData, - // todo: fully document every thing + // TODO: fully document every thing pub const Tag = enum { module, declaration, @@ -43,6 +43,9 @@ pub const Inst = struct { @"if", + struct_decl, + struct_init, + add, sub, mul, @@ -98,6 +101,8 @@ pub const Inst = struct { .param => DataNode, .store => PlNode(Store), .js_new => PlNode(JsNew), + .struct_decl => PlNode(StructDecl), + .struct_init => PlNode(StructInit), // Single instruction operand .memory, @@ -271,7 +276,7 @@ pub const Inst = struct { pub fn toValue(inst: Ref) ?InternPool.Value { const ref_int = @intFromEnum(inst); - // todo: is this > or >= + // TODO: is this > or >= if (ref_int > Index.static_len) return null; @@ -315,16 +320,57 @@ pub const Inst = struct { args: RefList, }; + /// Index into extra which contains `field_len * 2` u32s. The first half + /// are the keys (NullTerminatedString) and the second half are field types + /// (Ref). This basically is like a specialized MultiArrayList. + pub const StructDecl = struct { + payload_index: u32, + field_len: u32, + + pub fn keys(decl: StructDecl, teg: *const Teg) []const NullTerminatedString { + return @ptrCast( + teg.extra.list.items[decl.payload_index..][0..decl.field_len], + ); + } + + pub fn types(decl: StructDecl, teg: *const Teg) []const Ref { + return @ptrCast( + teg.extra.list.items[decl.payload_index + decl.field_len ..][0..decl.field_len], + ); + } + }; + + /// This memory is laid out with the same strategy described in StructDecl + pub const StructInit = struct { + base_type: Ref, + payload_index: u32, + field_len: u32, + + pub fn keys(decl: StructInit, teg: *const Teg) []const NullTerminatedString { + return @ptrCast( + teg.extra.list.items[decl.payload_index..][0..decl.field_len], + ); + } + + pub fn inits(decl: StructInit, teg: *const Teg) []const Ref { + return @ptrCast( + teg.extra.list.items[decl.payload_index + decl.field_len ..][0..decl.field_len], + ); + } + }; + /// This will contain trailing data depending on the .kind pub const Decl = struct { name: NullTerminatedString, hash: Cache.BinDigest, kind: Kind, instruction_count: u32, + body: Ref, pub const Kind = enum { + // TODO: remove function, see #45 function, - // future: struct, let, var, etc + immutable, }; }; @@ -332,7 +378,6 @@ pub const Inst = struct { pub const DeclFn = struct { params: RefList, return_type: Ref, - body: Ref, }; }; @@ -371,6 +416,8 @@ pub const SlicedString = struct { } }; +// TODO: Make the next three structs using a generic comptime thing + pub const RefList = struct { start: u32, len: u32, @@ -389,6 +436,15 @@ pub const IndexList = struct { } }; +pub const NullTermStringList = struct { + start: u32, + len: u32, + + pub fn slice(self: IndexList, teg: *const Teg) []const NullTerminatedString { + return @ptrCast(teg.extra.list.items[self.start..][0..self.len]); + } +}; + pub fn deinit(teg: *Teg, gpa: std.mem.Allocator) void { gpa.free(teg.string_bytes); teg.extra.deinit(gpa); @@ -426,10 +482,44 @@ pub fn blockInstructions(teg: *const Teg, index: Inst.Index) []const Inst.Index pub fn fullFuncDecl(teg: *const Teg, index: Inst.Index) struct { Inst.Decl, Inst.DeclFn } { const decl, var trail = teg.instData(.declaration, index).extra_index.getTrail(teg); + assert(decl.kind == .function); const decl_fn = trail.read(&teg.extra, Inst.DeclFn); return .{ decl, decl_fn }; } +pub const FullDecl = struct { + name: NullTerminatedString, + hash: Cache.BinDigest, + instruction_count: u32, + data: Data, + body: Inst.Ref, + + const Data = union(enum) { + function: Inst.DeclFn, + immutable: struct {}, + }; +}; + +pub fn fullDecl(teg: *const Teg, index: Inst.Index) FullDecl { + const decl, var trail = teg.instData(.declaration, index).extra_index.getTrail(teg); + return .{ + .name = decl.name, + .hash = decl.hash, + .instruction_count = decl.instruction_count, + .body = decl.body, + .data = switch (decl.kind) { + inline else => |tag| @unionInit( + FullDecl.Data, + @tagName(tag), + trail.read(&teg.extra, std.meta.FieldType( + FullDecl.Data, + @field(std.meta.FieldEnum(FullDecl.Data), @tagName(tag)), + )), + ), + }, + }; +} + pub fn declData(teg: *const Teg, index: Inst.Index) Inst.Decl { return teg.instData(.declaration, index).extra_index.get(teg); } diff --git a/src/core/Tokenizer.zig b/src/core/Tokenizer.zig index 3f1810f35b97f4cd4f2b472082a475a7a1e26916..9f55efbc4d357bfc303c0b3e146714ea45220499 100644 --- a/src/core/Tokenizer.zig +++ b/src/core/Tokenizer.zig @@ -68,11 +68,10 @@ pub const Token = struct { kw_else, kw_enum, kw_fn, - kw_infer, kw_for, - kw_while, kw_if, kw_impl, + kw_infer, kw_json, kw_let, kw_match, @@ -83,6 +82,7 @@ pub const Token = struct { kw_struct, kw_trait, kw_var, + kw_while, // sort-lines: end // sort-lines: start diff --git a/src/core/Transform.zig b/src/core/Transform.zig index 36bab689bb7a33830a3888379b674afee6ce5861..e2cbb961dcb9394a1bc6e1b3103211f199d9009e 100644 --- a/src/core/Transform.zig +++ b/src/core/Transform.zig @@ -1,4 +1,13 @@ -//! Transform pass +//! Transform pass converts an Ast into a high-level/untyped instruction format. +//! See Teg.zig for more details about the IR. Transforming handles: +//! +//! - Resolving all local variables into reference or declaration names. +//! - Resolving breaks and returns to their block id. +//! - Annotating expression statements with `.discard` and `.ensure_void` +//! - Flattening nested expressions into instruction sequences. +//! +//! Some trivial diagnostics such as unused variables, unknown result locations, and +//! invalid assignment targets are also reported here. gpa: Allocator, ast: Ast, insts: Inst.List = .{}, @@ -8,11 +17,6 @@ extra: ExtraStore = .{}, scratch: std.ArrayListUnmanaged(u32) = .{}, id_decode_buf: std.ArrayList(u8), -// TODO: refactor this away -// The new home for this memory should be pre-allocated on scratch, -// but then remove the elements, moving scratch items on top of this -// backwards a few. -call_args: std.ArrayListUnmanaged(Inst.Ref) = .{}, scopes: std.ArrayListUnmanaged(Scope) = .{}, report: Report.Builder, @@ -67,7 +71,6 @@ pub fn transform( pub fn deinit(t: *Transform) void { t.scratch.deinit(t.gpa); - t.call_args.deinit(t.gpa); assert(t.scopes.items.len == 0); t.scopes.deinit(t.gpa); t.string_bytes.deinit(t.gpa); @@ -268,7 +271,7 @@ fn transformModule(t: *Transform, module_node: NodeIndex) Allocator.Error!Inst.I s.symbols.lockPointers(); defer s.symbols.unlockPointers(); for (s.symbols.values(), 0..) |value, i| { - temp[i] = try t.transformFunction(decl, value.node); + temp[i] = try t.transformDecl(decl, value.node); } } @@ -316,12 +319,12 @@ fn transformModule(t: *Transform, module_node: NodeIndex) Allocator.Error!Inst.I .name = NullTerminatedString.empty, .hash = .{0} ** 16, .instruction_count = @as(u32, @intCast(t.insts.len)) - @intFromEnum(top_level_fn), + .body = top_level_code_ref, }, dod.extraSizeOf(Inst.DeclFn)); trail.writeStruct(Inst.DeclFn, .{ .params = .{ .start = 0, .len = 0 }, .return_type = Inst.Ref.void, - .body = top_level_code_ref, }); t.getInstData(top_level_fn).* = EncodedData.encode(.declaration, .{ @@ -346,13 +349,13 @@ fn previsitModuleStatement(t: *Transform, node: NodeIndex) Allocator.Error!void const tag = t.ast.nodes.items(.tag)[node]; switch (tag) { .fn_decl => { - const fn_decl = t.ast.fullFnDecl(node); - var id, const id_allocated = try t.decodeIdentifierExtra(fn_decl.id); + const decl = t.ast.fullFnDecl(node); + var id, const id_allocated = try t.decodeIdentifierExtra(decl.id); if (id_allocated) id = try t.gpa.dupe(u8, id); const gop = try t.scope().symbols.getOrPut(t.gpa, id); if (gop.found_existing) { - t.gpa.free(id); + if (id_allocated) t.gpa.free(id); try t.report.err(.duplicate_function, .{ .node = node, .prev = gop.value_ptr.node, @@ -364,6 +367,22 @@ fn previsitModuleStatement(t: *Transform, node: NodeIndex) Allocator.Error!void .data = .declaration, }; }, + .struct_decl => { + const decl = t.ast.fullStructDecl(node); + const gop = try t.scope().symbols.getOrPut(t.gpa, decl.id); + if (gop.found_existing) { + try t.report.err(.duplicate_function, .{ + .node = node, + .prev = gop.value_ptr.node, + // TODO: put type of both declarations + }); + return; + } + gop.value_ptr.* = .{ + .node = node, + .data = .declaration, + }; + }, else => {}, } } @@ -378,12 +397,22 @@ fn transformModuleStatement(t: *Transform, decl: BlockContext, node: NodeIndex) .param_decl, => unreachable, - .fn_decl => {}, // functions analyzed separately + .fn_decl, + .struct_decl, + => {}, // declarations are analyzed separately else => _ = try t.transformBlockStatement(decl, node, false), } } +fn transformDecl(t: *Transform, outer_decl: BlockContext, node: NodeIndex) Allocator.Error!Inst.Index { + return switch (t.ast.nodeTag(node)) { + .fn_decl => t.transformFunction(outer_decl, node), + .struct_decl => t.transformStruct(outer_decl, node), + else => unreachable, // not a declaration + }; +} + fn transformFunction(t: *Transform, outer_decl: BlockContext, node: NodeIndex) Allocator.Error!Inst.Index { const trace = tracer.trace(@src()); defer trace.end(); @@ -391,7 +420,7 @@ fn transformFunction(t: *Transform, outer_decl: BlockContext, node: NodeIndex) A const decl = outer_decl.declOffset(node); const fn_decl = t.ast.fullFnDecl(node); - const func = try t.addRootInst(.{ + const decl_inst = try t.addRootInst(.{ .tag = .declaration, .data = undefined, }); @@ -421,23 +450,53 @@ fn transformFunction(t: *Transform, outer_decl: BlockContext, node: NodeIndex) A .kind = .function, .name = try t.addNullTerminatedString(fn_decl.id), .hash = .{0} ** 16, - .instruction_count = @as(u32, @intCast(t.insts.len)) - @intFromEnum(func), + .instruction_count = @as(u32, @intCast(t.insts.len)) - @intFromEnum(decl_inst), + .body = body, }); _ = try t.extra.append(t.gpa, Inst.DeclFn, .{ .params = param_list, .return_type = return_type, - .body = body, }); - t.getInstData(func).* = EncodedData.encode(.declaration, .{ + t.getInstData(decl_inst).* = EncodedData.encode(.declaration, .{ .src_node = outer_decl.nodeOffset(node), .extra_index = decl_extra, }); try t.scope().addWarnings(t); - return func; + return decl_inst; +} + +fn transformStruct(t: *Transform, outer_decl: BlockContext, node: NodeIndex) Allocator.Error!Inst.Index { + const trace = tracer.trace(@src()); + defer trace.end(); + + const decl = outer_decl.declOffset(node); + const decl_data = t.ast.fullStructDecl(node); + + const decl_inst = try t.addRootInst(.{ + .tag = .declaration, + .data = undefined, + }); + + const body = try t.transformExpressionToBlock(decl, node, false); + + const decl_extra = try t.extra.append(t.gpa, Inst.Decl, .{ + .kind = .immutable, + .name = try t.addNullTerminatedString(decl_data.id), + .hash = .{0} ** 16, + .instruction_count = @as(u32, @intCast(t.insts.len)) - @intFromEnum(decl_inst), + .body = body, + }); + + t.getInstData(decl_inst).* = EncodedData.encode(.declaration, .{ + .src_node = outer_decl.nodeOffset(node), + .extra_index = decl_extra, + }); + + return decl_inst; } const TransformFunctionCtx = struct { @@ -555,7 +614,7 @@ fn transformParamDecl(t: *Transform, decl: BlockContext, param: NodeIndex) Alloc defer trace.end(); const param_decl = t.ast.fullParamDecl(param); - const value = try t.transformExpressionToBlock(decl, param_decl.type, false); + const value = try t.transformExpressionToBlock(decl, param_decl.type orelse @panic("TODO: inferred param types"), false); var id, const id_allocated = try t.decodeIdentifierExtra(param_decl.id); if (id_allocated) @@ -699,7 +758,6 @@ fn transformExpression(t: *Transform, decl: BlockContext, node: NodeIndex) Alloc .root, .param_decl, .var_decl, - .fn_decl, => unreachable, // not an expression .id => { @@ -1157,29 +1215,91 @@ fn transformExpression(t: *Transform, decl: BlockContext, node: NodeIndex) Alloc return try t.transformExpression(decl, group_node.expr); }, + .struct_decl => { + const struct_node = t.ast.fullStructDecl(node); + const payload_index = try t.extra.alloc(t.gpa, struct_node.fields.len * 2); + + for ( + struct_node.fields, + payload_index.., + payload_index + struct_node.fields.len.., + ) |field_node, i, j| { + const field = t.ast.fullStructField(field_node); + + if (mem.startsWith(u8, field.id, "@")) { + @panic("TODO: string identifiers"); + } + + t.extra.setU32(i, @intFromEnum(try t.addNullTerminatedString(field.id))); + t.extra.setU32(j, @intFromEnum(try t.transformExpressionToBlock(decl, field.type, false))); + } + + return try t.addInstRef(.{ + .tag = .struct_decl, + .data = EncodedData.encode(.struct_decl, .{ + .src_node = decl.nodeOffset(node), + .extra_index = try t.extra.append(t.gpa, Inst.StructDecl, .{ + .payload_index = payload_index, + .field_len = @intCast(struct_node.fields.len), + }), + }), + }); + }, + + .struct_init => { + const struct_node = t.ast.fullStructInit(node); + const payload_index = try t.extra.alloc(t.gpa, struct_node.fields.len * 2); + + const base_type = try t.transformExpression( + decl, + struct_node.type orelse @panic("TODO: TEG implicit struct"), + ) orelse + @panic("TODO: print better error"); + + for ( + struct_node.fields, + payload_index.., + payload_index + struct_node.fields.len.., + ) |field_node, i, j| { + const field = t.ast.fullStructField(field_node); + + if (mem.startsWith(u8, field.id, "@")) { + @panic("TODO: string identifiers"); + } + + t.extra.setU32(i, @intFromEnum(try t.addNullTerminatedString(field.id))); + t.extra.setU32(j, @intFromEnum((try t.transformExpression(decl, field.type)) orelse + @panic("TODO: print better error"))); + } + + return try t.addInstRef(.{ + .tag = .struct_init, + .data = EncodedData.encode(.struct_init, .{ + .src_node = decl.nodeOffset(node), + .extra_index = try t.extra.append(t.gpa, Inst.StructInit, .{ + .base_type = base_type, + .payload_index = payload_index, + .field_len = @intCast(struct_node.fields.len), + }), + }), + }); + }, + else => |tag| std.debug.panic("TODO: expr {s}", .{@tagName(tag)}), } } fn transformCallArgs(t: *Transform, decl: BlockContext, args: []const NodeIndex) !?RefList { - const args_top = t.call_args.items.len; - defer t.call_args.shrinkRetainingCapacity(args_top); + const extra_top = try t.extra.alloc(t.gpa, args.len); - for (args) |arg| { - try t.call_args.append( - t.gpa, - try t.transformExpression(decl, arg) orelse - return null, - ); + for (args, extra_top..) |arg, i| { + t.extra.setU32(i, @intFromEnum(try t.transformExpression(decl, arg) orelse + return null)); } - const items = t.call_args.items[args_top..]; - const extra_top = t.extra.list.items.len; - try t.extra.list.appendSlice(t.gpa, @ptrCast(items)); - return .{ .start = @intCast(extra_top), - .len = @intCast(items.len), + .len = @intCast(args.len), }; } @@ -1193,7 +1313,6 @@ fn transformAssignmentTarget(t: *Transform, decl: BlockContext, node: NodeIndex) .root, .param_decl, .var_decl, - .fn_decl, => unreachable, // not an expression .id => { diff --git a/src/core/array.zig b/src/core/array.zig index 0356b1d82383dfcc97deca479fbb9e172d589b2b..1a8286399d1643c2443a627deb066b4e2d9b50aa 100644 --- a/src/core/array.zig +++ b/src/core/array.zig @@ -40,44 +40,46 @@ fn setAsU32(items: []u32, comptime T: type, index: usize, payload: T) void { } fn getAsU32(items: []const u32, comptime T: type, index: usize) T { - const fields = std.meta.fields(T); - var i = index; - var result: T = undefined; - inline for (fields) |field| { - @field(result, field.name) = switch (field.type) { - u32 => items[i], - i32 => @bitCast(items[i]), + return switch (T) { + u32 => items[index], + i32 => @bitCast(items[index]), - [16]u8 => @bitCast(items[i..][0..4].*), + [16]u8 => @bitCast(items[index..][0..4].*), - else => brk: { - const info = @typeInfo(field.type); - if (info == .Enum) { - break :brk @enumFromInt(items[i]); - } + else => brk: { + const info = @typeInfo(T); + if (info == .Enum) { + break :brk @enumFromInt(items[index]); + } - const struct_data = info.Struct; - if (struct_data.layout == .@"packed") { - break :brk @bitCast(items[i]); - } + const struct_data = info.Struct; + if (struct_data.layout == .@"packed") { + break :brk @bitCast(items[index]); + } - break :brk @call( + const fields = std.meta.fields(T); + var i = index; + var result: T = undefined; + + inline for (fields) |field| { + @field(result, field.name) = @call( .always_inline, getAsU32, .{ items, field.type, i }, ); - }, - }; - i += extraSizeOf(field.type); - } - return result; + i += comptime extraSizeOf(field.type); + } + + break :brk result; + }, + }; } pub const StoreKind = enum { intern_pool, teg, air, - tfr, + js_tree, pub fn resolveStoreConst( comptime kind: StoreKind, @@ -204,6 +206,7 @@ pub fn extraSizeOf(comptime T: type) usize { }; } +// TODO: rename to Store pub fn ExtraStore(comptime set_kind: StoreKind, comptime set_mutability: Mutability) type { return struct { list: List = .{}, @@ -240,7 +243,19 @@ pub fn ExtraStore(comptime set_kind: StoreKind, comptime set_mutability: Mutabil return ip.get(T, trail.index); } - pub fn readValueSlice(trail: *TrailReader, len: u32) @import("InternPool.zig").Value.Slice { + pub fn at(trail: *const TrailReader, ip: *const Store, comptime T: type, index: u32) T { + return ip.get(T, trail.index + index); + } + + pub fn readValueSlice(trail: *TrailReader, len: u32) InternPool.Value.Slice { + defer trail.index += len; + return .{ + .start = @intCast(trail.index), + .len = len, + }; + } + + pub fn readTypeSlice(trail: *TrailReader, len: u32) InternPool.Type.Slice { defer trail.index += len; return .{ .start = @intCast(trail.index), @@ -248,7 +263,10 @@ pub fn ExtraStore(comptime set_kind: StoreKind, comptime set_mutability: Mutabil }; } - pub fn readTypeSlice(trail: *TrailReader, len: u32) @import("InternPool.zig").Type.Slice { + pub fn readNullTerminatedStringSlice( + trail: *TrailReader, + len: u32, + ) InternPool.NullTerminatedString.Slice { defer trail.index += len; return .{ .start = @intCast(trail.index), @@ -321,12 +339,18 @@ pub fn ExtraStore(comptime set_kind: StoreKind, comptime set_mutability: Mutabil }; } - fn set(store: *Store, comptime T: type, index: usize, payload: T) void { + // TODO: make setAsU32 allow any type + pub fn setU32(store: *Store, index: usize, payload: u32) void { + comptime assert(mutability == .@"var"); + store.list.items[index] = payload; + } + + pub fn set(store: *Store, comptime T: type, index: usize, payload: T) void { comptime assert(mutability == .@"var"); setAsU32(store.list.items, T, index, payload); } - fn get(store: *const Store, comptime T: type, index: usize) T { + pub fn get(store: *const Store, comptime T: type, index: usize) T { return getAsU32(store.list.items, T, index); } @@ -338,6 +362,16 @@ pub fn ExtraStore(comptime set_kind: StoreKind, comptime set_mutability: Mutabil pub fn constant(store: *const Store) ExtraStore(set_kind, .@"const") { return .{ .list = .{ .items = store.list.items } }; } + + // TODO: Add some safety to the Index returned from this + /// Allocate a number of u32s into this store, but they are left uninitialized. + pub fn alloc(store: *Store, gpa: Allocator, len: usize) !u32 { + comptime assert(mutability == .@"var"); + const idx = store.list.items.len; + try store.list.ensureUnusedCapacity(gpa, len); + store.list.items.len += len; + return @intCast(idx); + } }; } @@ -388,3 +422,4 @@ pub const Float = struct { const std = @import("std"); const assert = std.debug.assert; const Allocator = std.mem.Allocator; +const InternPool = @import("InternPool.zig"); diff --git a/src/core/js/Js.zig b/src/core/js/Js.zig new file mode 100644 index 0000000000000000000000000000000000000000..f2e0bbed4f452c9df2762b475ff876aee4c2c7cf --- /dev/null +++ b/src/core/js/Js.zig @@ -0,0 +1,65 @@ +//! JavaScript code generation backend +//! +//! * Air -(NodeGen)-> JsTree +//! * []JsTree -(Link)-> []const u8 +//! +pub const NodeGen = @import("NodeGen.zig"); +pub const Tree = @import("Tree.zig"); +pub const Link = @import("Link.zig"); + +codegen_map: std.AutoHashMapUnmanaged(Value, Tree) = .{}, + +pub fn deinit(backend: *Js, comp: *Comp) void { + { + var it = backend.codegen_map.valueIterator(); + while (it.next()) |item| { + item.deinit(comp.gpa); + } + } + + backend.codegen_map.deinit(comp.gpa); +} + +pub fn handleFunctionAir( + backend: *Js, + comp: *Comp, + value: Value, + air: *const Air, + arena: Allocator, +) !u32 { + var ng = try NodeGen.init(comp.gpa, arena, &comp.intern_pool, air); + errdefer ng.deinit(); + try ng.build(); + + const tree = try ng.finalize(); + try backend.codegen_map.put(comp.gpa, value, tree); + + return 0; +} + +pub fn link( + backend: *Js, + comp: *Comp, + entry_point: Comp.Decl.Index, + writer: std.io.AnyWriter, +) !void { + var state = Link{ + .comp = comp, + .backend = backend, + }; + defer state.deinit(); + try state.linkJsCode(entry_point, writer); +} + +const Js = @This(); + +const std = @import("std"); +const assert = std.debug.assert; +const log = std.log.scoped(.js); +const Allocator = std.mem.Allocator; + +const core = @import("../mod.zig"); +const Comp = core.Comp; +const Air = core.Air; +const InternPool = core.InternPool; +const Value = InternPool.Value; diff --git a/src/core/js/Link.zig b/src/core/js/Link.zig new file mode 100644 index 0000000000000000000000000000000000000000..05c02bcf04e742c33168e5e87396688a539534c6 --- /dev/null +++ b/src/core/js/Link.zig @@ -0,0 +1,821 @@ +//! This is the JavaScript backend's linker. It takes in a set of `Js.Tree`s and +//! produces a JavaScript bundle out of them. +//! +//! This approach is flawed because it combining too much work in one unit. +//! Printing and Linking should be separate stages, so that more work can be +//! done in parallel. This approach is used for now to make identifier name +//! collisions easier to deal with. +comp: *Comp, +backend: *Js, + +/// Value is an index into `link_strings` +decls: std.AutoHashMapUnmanaged(InternPool.Value, LinkString.Index) = .{}, +link_strings: std.ArrayHashMapUnmanaged( + LinkString, + GlobalOwner, + LinkString.Context, + true, +) = .{}, +string_bytes: std.ArrayListUnmanaged(u8) = .{}, + +pub fn linkJsCode(link: *Link, entry_decl: Decl.Index, writer: Writer) !void { + var t = tracer.trace(@src()); + defer t.end(); + + // 1. Resolve all global identifiers. Function names must not shadow external + // names. We also need this list prepared so that local identifier names do not + // shadow functions or externals. + const root_decl = link.comp.intern_pool.declPtrConst(entry_decl); + assert(root_decl.name == .empty); + + const root_function = link.treeForFunction(root_decl.value()); + + for (root_function.extern_symbols) |extern_symbol| { + try link.reserveExternSymbol(extern_symbol); + } + + var dependency_queue: DependencyQueue = .{}; + defer dependency_queue.list.deinit(link.comp.gpa); + try dependency_queue.list.appendSlice(link.comp.gpa, root_function.top_level_value_deps); + + while (dependency_queue.next()) |value| { + const tree = link.treeForFunction(value); + + for (tree.extern_symbols) |extern_symbol| { + try link.reserveExternSymbol(extern_symbol); + } + + try dependency_queue.list.appendSlice(link.comp.gpa, tree.top_level_value_deps); + + const decl = link.comp.intern_pool.declPtrConst( + value.valueData(&link.comp.intern_pool, .func) + .get(&link.comp.intern_pool) + .owner_decl, + ); + const name = decl.name.toSlice(&link.comp.intern_pool); + assert(name.len > 0); + + const gop = try link.link_strings.getOrPutContextAdapted( + link.comp.gpa, + name, + LinkString.SliceContext{ .link = link }, + .{ .link = link }, + ); + if (gop.found_existing) { + @panic("TODO: handle this name collision case"); + } + const name_str = try link.appendLinkString(name); + gop.key_ptr.* = name_str; + gop.value_ptr.* = .decl; + try link.decls.putNoClobber(link.comp.gpa, value, @enumFromInt(gop.index)); + } + + try writer.print("// Language Compiler v0.1.4. Alpha quality software.\n", .{}); + + if (root_function.nodes.len > 1) { + // 2. Print every function decl + dependency_queue.i = 0; + while (dependency_queue.next()) |value| { + try link.printTopLevelValue(value, writer); + } + + // 3. Print the top-level code + try link.printDeclContents(root_function, writer); + } else { + try writer.print("// (program has no observable effects, thus an empty file)\n", .{}); + } +} + +fn treeForFunction(link: *const Link, value: InternPool.Value) *Tree { + assert(value.valueTag(&link.comp.intern_pool) == .func); // must be a function + return link.backend.codegen_map.getPtr(value) orelse unreachable; +} + +const DependencyQueue = struct { + list: std.ArrayListUnmanaged(InternPool.Value) = .{}, + i: usize = 0, + + pub fn next(queue: *DependencyQueue) ?InternPool.Value { + if (queue.i >= queue.list.items.len) { + return null; + } + + defer queue.i += 1; + return queue.list.items[queue.i]; + } +}; + +pub fn deinit(link: *Link) void { + var t = tracer.trace(@src()); + defer t.end(); + + defer link.* = undefined; + link.decls.deinit(link.comp.gpa); + link.link_strings.deinit(link.comp.gpa); + link.string_bytes.deinit(link.comp.gpa); +} + +fn reserveExternSymbol(link: *Link, extern_symbol: String.WithLen) !void { + var t = tracer.trace(@src()); + defer t.end(); + + const name = extern_symbol.toSlice(&link.comp.intern_pool); + const gop = try link.link_strings.getOrPutContextAdapted( + link.comp.gpa, + name, + LinkString.SliceContext{ .link = link }, + .{ .link = link }, + ); + + if (gop.found_existing) { + if (gop.value_ptr.* != .external) { + @panic("TODO: Rename Decl"); + } + } + gop.key_ptr.* = try link.appendLinkString(name); + gop.value_ptr.* = .external; +} + +fn appendLinkString(link: *Link, string: []const u8) !LinkString { + var t = tracer.trace(@src()); + defer t.end(); + + const start = link.string_bytes.items.len; + try link.string_bytes.appendSlice(link.comp.gpa, string); + return .{ .start = @intCast(start), .len = @intCast(string.len) }; +} + +fn valueRefIdentifier(link: *const Link, value: InternPool.Value) []const u8 { + var t = tracer.trace(@src()); + defer t.end(); + + return (link.decls.get(value) orelse if (std.debug.runtime_safety) { + std.debug.panic("Value \"{}\" is not present in top level value map", .{ + value.dump(&link.comp.intern_pool), + }); + } else unreachable).toString(link).slice(link); +} + +fn printTopLevelValue(link: *Link, value: InternPool.Value, writer: Writer) !void { + var t = tracer.trace(@src()); + defer t.end(); + + try writer.writeAll("let "); + try writer.writeAll(link.valueRefIdentifier(value)); + try writer.writeAll(" = "); + + const tree = link.treeForFunction(value); + + if (tree.param_count != 1) + try writer.writeAll("("); + for (0..tree.param_count) |id| { + try link.printLocal(@intCast(id), writer); + if (id != tree.param_count - 1) try writer.writeAll(", "); + } + if (tree.param_count != 1) + try writer.writeAll(")"); + try writer.writeAll(" => "); + + const stmts = tree.mainStatements(); + if (stmts.len == 1 and tree.nodeTag(stmts[0]) == .stmt_return) { + if (tree.nodeData(.stmt_return, stmts[0]).operand.unwrap()) |operand| { + try link.printExprLevel(tree, operand, .statement, writer); + } else { + try writer.writeAll("{}"); + } + } else { + try writer.writeAll("{\n"); + try link.printDeclContents(tree, writer); + try writer.writeAll("}"); + } + + try writer.writeAll(";\n"); +} + +fn printDeclContents(link: *Link, tree: *const Tree, writer: Writer) !void { + var t = tracer.trace(@src()); + defer t.end(); + + for (tree.mainStatements()) |stmt| { + try link.printExprLevel(tree, stmt, .statement, writer); + try writer.writeAll(";\n"); + } +} + +fn printBlockContents( + link: *Link, + tree: *const Tree, + list: Tree.IndexList, + writer: Writer, +) !void { + var t = tracer.trace(@src()); + defer t.end(); + + assert(list.slice(tree).len > 0); + for (list.slice(tree)) |stmt| { + try link.printExpr(tree, stmt, writer); + switch (tree.nodeTag(stmt)) { + .stmt_block, + .stmt_if, + => {}, + else => try writer.writeAll(";"), + } + try writer.writeAll("\n"); + } +} + +fn printBlock(link: *Link, tree: *const Tree, list: Tree.IndexList, writer: Writer) !void { + var t = tracer.trace(@src()); + defer t.end(); + + if (list.len != 1) { + try writer.writeAll("{"); + try link.printBlockContents(tree, list, writer); + try writer.writeAll("}"); + } else { + try link.printBlockContents(tree, list, writer); + } +} + +fn printExpr( + link: *Link, + tree: *const Tree, + node: Node.Index, + writer: Writer, +) @TypeOf(writer).Error!void { + return link.printExprLevel(tree, node, .comma, writer); +} + +fn printValue(link: *Link, value: InternPool.Value, level: Level, writer: anytype) !void { + switch (value.toKey(&link.comp.intern_pool)) { + // TODO: minification + .int32 => |val| try writer.print("{d}", .{val}), + // TODO: correct printing of many value types + .float => |val| try writer.print("{d}", .{val}), + .simple_value => |val| try writer.writeAll(switch (val) { + .true => "true", + .false => "false", + // TODO: make codegen decide from null and undefined + // instead of emitting void nodes. + .void => "/* void */ undefined", + }), + .string => |val| try writeString(val, writer), + + .comptime_float => unreachable, // was not lowered to float + + .struct_value => |struct_value| { + const wrap = level == .statement; + if (wrap) + try writer.writeAll("("); + + try writer.writeAll("{"); + + for ( + struct_value.ty.toStructType(&link.comp.intern_pool).?.field_keys, + struct_value.field_values, + 0.., + ) |key, inner_value, i| { + if (i != 0) + try writer.writeAll(","); + + // TODO: handle special identifiers + try writer.writeAll(key.toSlice(&link.comp.intern_pool)); + try writer.writeAll(":"); + try link.printValue(inner_value, .comma, writer); + } + try writer.writeAll("}"); + + if (wrap) + try writer.writeAll(")"); + }, + + else => unreachable, // this value is not a plain_value + } +} + +fn printExprLevel( + link: *Link, + tree: *const Tree, + node: Node.Index, + level: Level, + writer: Writer, +) @TypeOf(writer).Error!void { + var t = tracer.trace(@src()); + defer t.end(); + + switch (tree.nodeTag(node)) { + .root => unreachable, + + .plain_value => { + const plain_value: Tree.PlainValue = tree.nodeData(.plain_value, node); + try link.printValue(plain_value.value, level, writer); + }, + + .local => { + const ref: Tree.LocalRef = tree.nodeData(.local, node); + try link.printLocal(ref.id, writer); + }, + + .global => { + const ref: Tree.GlobalRef = tree.nodeData(.global, node); + try writer.writeAll(ref.name.toSlice(&link.comp.intern_pool)); + }, + + .top_level_value => { + const ref: Tree.ValueRef = tree.nodeData(.top_level_value, node); + try writer.writeAll(link.valueRefIdentifier(ref.value)); + }, + + .stmt_break => { + const stmt: Tree.StmtBreak = tree.nodeData(.stmt_break, node); + try writer.writeAll("break"); + if (true) { + try writer.writeAll(" "); + try link.printLabelIdentifier(stmt.label, writer); + } + }, + + .stmt_return => { + const stmt: Tree.StmtReturn = tree.nodeData(.stmt_return, node); + try writer.writeAll("return"); + if (stmt.operand.unwrap()) |operand| { + try writer.writeAll(" "); + try link.printExpr(tree, operand, writer); + } + }, + + .stmt_declare_local => { + const stmt: Tree.StmtDeclareLocal = tree.nodeData(.stmt_declare_local, node); + try writer.writeAll("let "); + try link.printLocal(stmt.identifier_index, writer); + if (stmt.init.unwrap()) |init| { + try writer.writeAll(" = "); + try link.printExpr(tree, init, writer); + } + }, + + .stmt_if => { + const stmt: Tree.StmtIf = tree.nodeData(.stmt_if, node); + const data = stmt.data.get(tree); + try writer.writeAll("if ("); + try link.printExpr(tree, stmt.cond, writer); + try writer.writeAll(") "); + if (data.flags.yes_label) { + try link.printLabelIdentifier(data.flags.block_label, writer); + try writer.writeAll(":"); + } + try link.printBlock(tree, data.yes, writer); + if (data.no.len > 0) { + try writer.writeAll(" else "); + if (data.flags.no_label) { + try link.printLabelIdentifier(data.flags.block_label, writer); + try writer.writeAll(":"); + } + try link.printBlock(tree, data.no, writer); + } + }, + + .stmt_block => { + const stmt: Tree.StmtBlock = tree.nodeData(.stmt_block, node); + try link.printLabelIdentifier(stmt.label, writer); + try writer.writeAll(": {\n"); + try link.printBlockContents(tree, stmt.content.get(tree), writer); + try writer.writeAll("}"); + }, + + inline .assign, + .add, + .sub, + .mul, + .div, + .eq, + .lt, + .gt, + .assign_add, + .assign_sub, + .assign_mul, + .assign_div, + => |tag| { + const bin: Tree.Bin = tree.nodeData(tag, node); + const op: Operator = @field(Operator, @tagName(tag)); + const inner_level = comptime op.level(); + if (inner_level.needsParens(level)) + try writer.writeAll("("); + try link.printExprLevel(tree, bin.lhs, inner_level, writer); + try writer.writeAll(" "); + try writer.writeAll(comptime op.symbol()); + try writer.writeAll(" "); + try link.printExprLevel(tree, bin.rhs, inner_level, writer); + if (inner_level.needsParens(level)) + try writer.writeAll(")"); + }, + + inline .negate, + .bool_not, + .pre_dec, + .pre_inc, + => |tag| { + const un: Tree.Un = tree.nodeData(tag, node); + const op: Operator = @field(Operator, @tagName(tag)); + try writer.writeAll(comptime op.symbol()); + try link.printExpr(tree, un.operand, writer); + }, + + .ternary => { + const ternary: Tree.Ternary = tree.nodeData(.ternary, node); + const data = ternary.data.get(tree); + try writer.writeAll("("); + try link.printExpr(tree, ternary.cond, writer); + try writer.writeAll(" ? "); + try link.printExpr(tree, data.yes, writer); + try writer.writeAll(" : "); + try link.printExpr(tree, data.no, writer); + try writer.writeAll(")"); + }, + + .call => { + const call: Tree.Call = tree.nodeData(.call, node); + const args = call.args.get(tree); + + try link.printExpr(tree, call.base, writer); + try writer.writeAll("("); + + for (args.slice(tree), 0..) |arg, i| { + if (i != 0) try writer.writeAll(", "); + try link.printExpr(tree, arg, writer); + } + + try writer.writeAll(")"); + }, + .new => { + const call: Tree.Call = tree.nodeData(.new, node); + const args = call.args.get(tree); + + if (Level.new.needsParens(level)) + try writer.writeAll("("); + try writer.writeAll("new "); + try link.printExpr(tree, call.base, writer); + + if (args.len > 0) { + try writer.writeAll("("); + + for (args.slice(tree), 0..) |arg, i| { + if (i != 0) try writer.writeAll(", "); + try link.printExpr(tree, arg, writer); + } + + try writer.writeAll(")"); + } + if (Level.new.needsParens(level)) + try writer.writeAll(")"); + }, + + .field => { + const field: Tree.Field = tree.nodeData(.field, node); + + try link.printExpr(tree, field.base, writer); + try writer.writeAll("."); + try writer.writeAll(field.field.toSlice(&link.comp.intern_pool)); + }, + + .object_literal => { + const obj: Tree.ObjectLiteral = tree.nodeData(.object_literal, node); + + const keys: []const Tree.ObjectLiteral.Key = + @ptrCast(tree.extra.list.items[obj.extra_index..][0..obj.property_count]); + const values: []const Node.Index = + @ptrCast(tree.extra.list.items[obj.extra_index + obj.property_count ..][0..obj.property_count]); + + if (level == .statement) + try writer.writeAll("("); + + try writer.writeAll("{"); + + for (keys, values, 0..) |key, value, i| { + if (i != 0) + try writer.writeAll(","); + + switch (key.decode()) { + .static => |str| { + // TODO: handle strings that cannot be printed as identifiers. + try writer.writeAll(str.toSlice(&link.comp.intern_pool)); + }, + .computed => @panic("TODO: computed property"), + } + + try writer.writeAll(":"); + + try link.printExpr(tree, value, writer); + } + + try writer.writeAll("}"); + + if (level == .statement) + try writer.writeAll(")"); + }, + } +} + +pub fn canPrintWithoutEscape(c: u21) bool { + const first_ascii_printable = 0x20; + const last_ascii_printable = 0x7E; + const first_high_surrogate = 0xD800; + const last_low_surrogate = 0xDFFF; + + if (c <= last_ascii_printable) { + return c >= first_ascii_printable and c != '\\' and c != '"'; + } else { + return c != 0xFEFF and (c < first_high_surrogate or c > last_low_surrogate); + } +} + +pub fn indexOfNeedsEscape(slice: []const u8) ?usize { + var it = std.unicode.Wtf8Iterator{ .bytes = slice, .i = 0 }; + var i: usize = 0; + while (it.nextCodepoint()) |c| { + if (!canPrintWithoutEscape(c)) + return i; + i = it.i; + } + return null; +} + +// This function is loosely based on Bun's `writeJSONString`, which +// is a Zig port of esbuild's `printUnquotedUTF16` function. +// Both projects are licensed under MIT +fn writeString(decoded: []const u8, writer: Writer) !void { + try writer.writeByte('"'); + + var text = decoded; + while (text.len > 0) { + const width = std.unicode.utf8ByteSequenceLength(text[0]) catch + unreachable; // string is encoded incorrectly + + const c: u21 = std.unicode.wtf8Decode(text[0..width]) catch + unreachable; // string is encoded incorrectly + + if (canPrintWithoutEscape(c)) { + if (indexOfNeedsEscape(text)) |j| { + try writer.writeAll(text[0..j]); + text = text[j..]; + continue; + } else { + try writer.writeAll(text); + break; + } + } + + text = text[width..]; + + switch (c) { + // Special-case the bell character since it may cause dumping this file to + // the terminal to make a sound, which is undesirable. Note that we can't + // use an octal literal to print this shorter since octal literals are not + // allowed in strict mode (or in template strings). + 0x07 => { + try writer.writeAll("\\x07"); + }, + 0x08 => { + try writer.writeAll("\\b"); + }, + 0x0C => { + try writer.writeAll("\\f"); + }, + '\n' => { + try writer.writeAll("\\n"); + }, + std.ascii.control_code.cr => { + try writer.writeAll("\\r"); + }, + std.ascii.control_code.vt => { + try writer.writeAll("\\v"); + }, + '\\' => { + try writer.writeAll("\\\\"); + }, + '"' => { + try writer.writeAll("\\\""); + }, + '\t' => { + try writer.writeAll("\\t"); + }, + + else => { + const hex_chars = "0123456789ABCDEF"; + + if (c < 0xFFFF) { + const k = @as(usize, @intCast(c)); + + try writer.writeAll(&[_]u8{ + '\\', + 'u', + hex_chars[(k >> 12) & 0xF], + hex_chars[(k >> 8) & 0xF], + hex_chars[(k >> 4) & 0xF], + hex_chars[k & 0xF], + }); + } else { + const first_high_surrogate = 0xD800; + const first_low_surrogate = 0xDC00; + + const k = c - 0x10000; + const lo = @as(usize, @intCast(first_high_surrogate + ((k >> 10) & 0x3FF))); + const hi = @as(usize, @intCast(first_low_surrogate + (k & 0x3FF))); + + try writer.writeAll(&[_]u8{ + '\\', + 'u', + hex_chars[lo >> 12], + hex_chars[(lo >> 8) & 15], + hex_chars[(lo >> 4) & 15], + hex_chars[lo & 15], + '\\', + 'u', + hex_chars[hi >> 12], + hex_chars[(hi >> 8) & 15], + hex_chars[(hi >> 4) & 15], + hex_chars[hi & 15], + }); + } + }, + } + } + try writer.writeByte('"'); +} + +fn printLocal(link: *Link, local: Tree.LocalIndex, writer: Writer) !void { + var t = tracer.trace(@src()); + defer t.end(); + + _ = link; // todo: be smarter about this + try writer.print("v{d}", .{local}); +} + +fn printLabelIdentifier(link: *Link, local: Tree.LocalIndex, writer: Writer) !void { + var t = tracer.trace(@src()); + defer t.end(); + + _ = link; // todo: be smarter about this + try writer.print("label{d}", .{local}); +} + +const GlobalOwner = enum { + external, + decl, +}; + +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_precedence +const Level = enum { + /// Extra level used for object literals in arrow function returns + statement, + comma, + assignment, + conditional, + logical_or, + logical_and, + bitwise_or, + bitwise_xor, + bitwise_and, + equality, + relational, + bitshift, + additive, + multiplicative, + exponentiation, + prefix, + postfix, + new, + access_and_call, + member, + + pub fn needsParens(inner_level: Level, outer_level: Level) bool { + return @intFromEnum(outer_level) > @intFromEnum(inner_level); + } +}; + +const Operator = enum { + assign, + assign_add, + assign_sub, + assign_mul, + assign_div, + add, + sub, + mul, + div, + eq, + lt, + gt, + negate, + bool_not, + pre_inc, + pre_dec, + + pub fn level(op: Operator) Level { + return switch (op) { + .assign, + .assign_add, + .assign_sub, + .assign_mul, + .assign_div, + => .assignment, + .add, .sub => .additive, + .mul, .div => .multiplicative, + .eq => .equality, + .lt, .gt => .relational, + + .pre_inc, + .pre_dec, + .bool_not, + .negate, + => .prefix, + }; + } + + pub fn symbol(op: Operator) []const u8 { + return switch (op) { + .assign => "=", + .assign_add => "+=", + .assign_sub => "-=", + .assign_mul => "*=", + .assign_div => "/=", + .add => "+", + .sub => "-", + .mul => "*", + .div => "/", + .eq => "===", + .lt => "<", + .gt => ">", + + .pre_inc => "++", + .pre_dec => "++", + + .negate => "-", + .bool_not => "!", + }; + } +}; + +const LinkString = struct { + start: u32, + len: u32, + + fn slice(str: LinkString, link: *const Link) []const u8 { + return link.string_bytes.items[str.start..][0..str.len]; + } + + const Index = enum(u32) { + _, + + pub fn toString(index: Index, link: *const Link) LinkString { + return link.link_strings.keys()[@intFromEnum(index)]; + } + }; + + const Context = struct { + link: *const Link, + + pub fn hash(ctx: Context, str: LinkString) u32 { + assert(str.start + str.len < ctx.link.string_bytes.items.len); + return @truncate(Wyhash.hash(0, str.slice(ctx.link))); + } + + pub fn eql(ctx: Context, a: LinkString, b: LinkString, _: usize) bool { + assert(a.start + a.len < ctx.link.string_bytes.items.len); + assert(b.start + b.len < ctx.link.string_bytes.items.len); + return a.start == b.start; + } + }; + + const SliceContext = struct { + link: *const Link, + + pub fn hash(_: SliceContext, str: []const u8) u32 { + return @truncate(Wyhash.hash(0, str)); + } + + pub fn eql(ctx: SliceContext, a: []const u8, b: LinkString, _: usize) bool { + // since the given string is not in string_bytes we do comparisons + return std.mem.eql(u8, a, b.slice(ctx.link)); + } + }; +}; + +const Link = @This(); +const std = @import("std"); +const assert = std.debug.assert; +const Wyhash = std.hash.Wyhash; +const log = std.log.scoped(.js_link); +const Writer = std.io.AnyWriter; + +const Comp = @import("../Comp.zig"); +const Decl = Comp.Decl; + +const Js = @import("Js.zig"); +const Tree = @import("Tree.zig"); +const Node = Tree.Node; + +const InternPool = @import("../InternPool.zig"); +const String = InternPool.String; + +const tracer = @import("tracer"); diff --git a/src/core/js/NodeGen.zig b/src/core/js/NodeGen.zig new file mode 100644 index 0000000000000000000000000000000000000000..f1c2f1d6bad461d7bca3be6a0cdc622ed75fd303 --- /dev/null +++ b/src/core/js/NodeGen.zig @@ -0,0 +1,751 @@ +//! Air -> Js.Tree +gpa: Allocator, +arena: Allocator, +air: *const Air, +ip: *const InternPool, + +nodes: Node.List, +extra: dod.ExtraStore(.js_tree, .@"var"), +extern_symbols: std.AutoArrayHashMapUnmanaged(InternPool.String.WithLen, void), +top_level_value_deps: std.AutoArrayHashMapUnmanaged(Value, void), + +param_count: u32, +air_node_map: []MapEntry, +scratch: std.ArrayListUnmanaged(u32), +next_id: u32, +next_label: u32, + +const Error = error{ + OutOfMemory, +}; + +const MapEntry = union { + none: void, + node: Node.Index, + block: packed struct { + used: bool = false, + index: u31, + }, +}; + +pub fn init(gpa: Allocator, arena: Allocator, ip: *const InternPool, air: *const Air) !NodeGen { + const t = tracer.trace(@src()); + defer t.end(); + + const air_node_map = try arena.alloc(MapEntry, air.insts.len); + if (std.debug.runtime_safety) { + @memset(air_node_map, .{ .none = {} }); + } + const param_count = air.paramCount(); + return .{ + .gpa = gpa, + .arena = arena, + .air = air, + .ip = ip, + .param_count = param_count, + .nodes = .{}, + .extern_symbols = .{}, + .top_level_value_deps = .{}, + .air_node_map = air_node_map, + .extra = .{}, + .scratch = .{}, + .next_id = param_count, + .next_label = 0, + }; +} + +pub fn deinit(c: *NodeGen) void { + const t = tracer.trace(@src()); + defer t.end(); + + defer c.* = undefined; + c.nodes.deinit(c.gpa); + c.extra.deinit(c.gpa); + c.extern_symbols.deinit(c.gpa); + c.top_level_value_deps.deinit(c.gpa); + + // arena can deinit the rest +} + +pub fn finalize(c: *NodeGen) !Tree { + const t = tracer.trace(@src()); + defer t.end(); + + // todo: errdefer will probably leave memory in a fully invalid state + try c.nodes.setCapacity(c.gpa, c.nodes.len); + + var extra = try c.extra.toOwnedStore(c.gpa); + errdefer extra.deinit(c.gpa); + + // todo: i dont like this dupe + const extern_symbols = try c.gpa.dupe(InternPool.String.WithLen, c.extern_symbols.keys()); + errdefer c.gpa.free(extern_symbols); + + const top_level_value_deps = try c.gpa.dupe(Value, c.top_level_value_deps.keys()); + errdefer c.gpa.free(top_level_value_deps); + + defer c.deinit(); + + return .{ + .param_count = c.param_count, + .nodes = c.nodes.toOwnedSlice(), + .extern_symbols = extern_symbols, + .top_level_value_deps = top_level_value_deps, + .extra = extra, + }; +} + +pub fn build(c: *NodeGen) !void { + const t = tracer.trace(@src()); + defer t.end(); + + assert(c.air.instTag(.main_block) == .block); + const statements = c.air.instData(.block, .main_block).slice(c.air); + + const scratch_top = c.scratch.items.len; + defer c.scratch.shrinkRetainingCapacity(scratch_top); + + _ = try c.add(.root, undefined); + + if (statements.len > 0) { + for (statements[0..c.param_count], 1..) |stmt, expected_id| { + assert(@intFromEnum(stmt) == expected_id); + assert(c.air.instTag(stmt) == .arg); + c.air_node_map[@intFromEnum(stmt)] = .{ + .node = try c.add(.local, .{ .id = @intFromEnum(stmt) - 1 }), + }; + } + + for (statements[c.param_count..]) |stmt| { + try c.codeGenInst(stmt); + } + } + + const items = c.scratch.items[scratch_top..]; + const top = c.extra.list.items.len; + try c.extra.list.appendSlice(c.gpa, items); + c.nodes.items(.data)[0] = EncodedData.encode(.root, .{ + .start = @intCast(top), + .len = @intCast(items.len), + }); +} + +fn codeGenInst(c: *NodeGen, inst: Air.Inst.Index) Error!void { + const t = tracer.traceFmt(@src(), "#{d} {s}", .{ @intFromEnum(inst), @tagName(c.air.instTag(inst)) }); + defer t.end(); + + switch (c.air.instTag(inst)) { + .arg => unreachable, // codegen must skip over these instructions. + + .block => try c.codeGenBlock(inst), + + .js_global_ref => { + const data: InternPool.String.WithLen = c.air.instData(.js_global_ref, inst); + try c.extern_symbols.put(c.gpa, data, {}); + c.air_node_map[@intFromEnum(inst)] = .{ + .node = try c.add(.global, .{ .name = data }), + }; + }, + + .memory => { + const i = @intFromEnum(inst); + const data: Air.Memory = c.air.instData(.memory, inst); + const value: Tree.Node.Index.Optional = if (data.init.isVoid()) + .none + else + (try c.fetchExpression(data.init)).toOptional(); + + try c.addStmt(try c.add(.stmt_declare_local, .{ + .identifier_index = c.next_id, + .init = value, + })); + const id = try c.add(.local, .{ .id = c.next_id }); + c.next_id += 1; + c.air_node_map[i] = .{ .node = id }; + }, + + inline .gt, + .lt, + .eq, + .add, + .sub, + .mul, + .div, + => |tag| { + const data: Air.Bin = c.air.instData(tag, inst); + const node = try c.add(@field(Tree.Tag, @tagName(tag)), .{ + .lhs = try c.fetchExpression(data.lhs), + .rhs = try c.fetchExpression(data.rhs), + }); + try c.markExpression(inst, node); + }, + + .store => { + const data: Air.Store = c.air.instData(.store, inst); + + const target = try c.fetchInstExpression(data.memory); + + if (data.operand.kind == .index) { + const index = data.operand.decode().index; + switch (c.air.instTag(index)) { + inline .add, + .sub, + .mul, + .div, + => |tag| { + const inner_data: Air.Bin = c.air.instData(tag, index); + if (inner_data.lhs.eql(data.memory.toRef())) { + if ((tag == .add or tag == .sub) and + inner_data.rhs.kind == .value) + { + // if (inner_data.rhs.decode().value.toFloat(c.ip)) |f| { + // if (f == 1) { + // // convert this into ++ or -- + // const node = try c.add( + // if (tag == .add) + // .pre_inc + // else + // .pre_dec, + // .{ + // .operand = target, + // }, + // ); + // try c.markExpression(inst, node); + + // return; + // } + // } + } + + // convert into a compound assignment + const node = try c.add( + @field(Tree.Tag, "assign_" ++ @tagName(tag)), + .{ + .lhs = target, + .rhs = try c.fetchExpression(inner_data.rhs), + }, + ); + try c.markExpression(inst, node); + + return; + } + }, + else => {}, + } + } + + const node = try c.add(.assign, .{ + .lhs = target, + .rhs = try c.fetchExpression(data.operand), + }); + try c.markExpression(inst, node); + }, + + inline .negate, + .bool_not, + => |tag| { + const data: Air.Un = c.air.instData(tag, inst); + const node = try c.add(@field(Tree.Tag, @tagName(tag)), .{ + .operand = try c.fetchExpression(data.operand), + }); + try c.markExpression(inst, node); + }, + + .field_opaque => { + const data: Air.FieldOpaque = c.air.instData(.field_opaque, inst); + const node = try c.add(.field, .{ + .base = try c.fetchExpression(data.base), + .field = data.field, + }); + try c.markExpression(inst, node); + }, + + .field_struct => { + const data: Air.FieldStruct = c.air.instData(.field_struct, inst); + const node = try c.add(.field, .{ + .base = try c.fetchInstExpression(data.base), + .field = field_name: { + break :field_name c.air.typeOfIndex(data.base, c.ip) + .toStructType(c.ip).? + .field_keys[data.field_index]; + }, + }); + try c.markExpression(inst, node); + }, + + inline .call, .new => |tag| { + const data: Air.Call = c.air.instData(tag, inst); + const call_data = data.call_data.get(c.air); + + try c.extra.list.ensureUnusedCapacity(c.gpa, call_data.args.len); + const start = c.extra.list.items.len; + c.extra.list.items.len += call_data.args.len; + + const items = c.extra.list.items[start..][0..call_data.args.len]; + + for (call_data.args.slice(c.air), items) |arg, *dest| { + dest.* = @intFromEnum(try c.fetchExpression(arg)); + } + + const node = try c.add(@field(Tree.Tag, @tagName(tag)), .{ + .base = try c.fetchExpression(data.base), + .args = try c.extra.append(c.gpa, Tree.IndexList, .{ + .start = @intCast(start), + .len = call_data.args.len, + }), + }); + try c.markExpression(inst, node); + }, + + .@"if" => { + const data: Air.If = c.air.instData(.@"if", inst); + const extra = data.data.get(c.air); + var cond = try c.fetchExpression(data.cond); + + if (c.canLowerToExpression(extra.yes) and + c.canLowerToExpression(extra.no)) + { + const yes = try c.lowerToExpression(extra.yes); + const no = try c.lowerToExpression(extra.yes); + + const ternary = try c.add(.ternary, .{ + .cond = cond, + .data = try c.extra.append(c.gpa, Tree.Ternary.Data, .{ + .yes = yes, + .no = no, + }), + }); + + try c.markExpression(inst, ternary); + } else { + const flags = c.air.instFlags(inst); + const uses = flags.uses; + + const target = if (uses != .none) + try c.addTemporary() + else + null; + + var yes = try c.codeGenBranch(extra.yes, target); + var no = try c.codeGenBranch(extra.no, target); + assert(!((yes == null) and (no == null))); // no "void else void" branches + + // when `yes` is void, swap the condition so that the `else` is omitted + if (yes == null) { + const temp_yes = yes; + yes = no; + no = temp_yes; + cond = try c.add(.bool_not, .{ + .operand = cond, + }); + } + + const main_expr = try c.add(.stmt_if, .{ + .cond = cond, + .data = try c.extra.append(c.gpa, Tree.StmtIf.Data, .{ + .yes = yes.?.list, + .no = if (no) |branch| + branch.list + else + .{ .start = 0, .len = 0 }, + .flags = .{ + .block_label = @intCast(c.next_label + 1), + .yes_label = yes.?.requires_label, + .no_label = if (no) |branch| branch.requires_label else false, + }, + }), + }); + + try c.addStmt(main_expr); + + if (target) |node| { + try c.markExpression(inst, node); + } + } + }, + + .@"break" => { + const brk: Air.Break = c.air.instData(.@"break", inst); + + const operand = try c.fetchExpression(brk.value); + + // break #0 -> return + if (brk.block_ref == .main_block) { + try c.addStmt(try c.add(.stmt_return, .{ + .operand = operand.toOptional(), + })); + } else { + const block = &c.air_node_map[@intFromEnum(brk.block_ref)].block; + + const block_insts = c.air.instData(.block, brk.block_ref).slice(c.air); + if (block_insts[block_insts.len - 1] != inst) { + block.used = true; + } + + const assignment: Node.Index.Optional = @enumFromInt(c.scratch.items[block.index]); + if (assignment.unwrap()) |target| { + try c.addStmt(try c.add(.assign, .{ + .lhs = target, + .rhs = operand, + })); + } + + try c.addStmt(try c.add(.stmt_break, .{ + .label = c.scratch.items[block.index + 1], + })); + } + }, + + .struct_init => { + const struct_init: Air.StructInit = c.air.instData(.struct_init, inst); + const struct_type = struct_init.ty.toStructType(c.ip).?; + + const property_count: u32 = @intCast(struct_type.field_types.len); + const store_offset = try c.extra.alloc(c.gpa, property_count * 2); + + for ( + struct_type.field_keys, + struct_init.valuesPtr(c.air), + store_offset.., + store_offset + property_count.., + ) |key, value, key_i, value_i| { + const key_encoded = Tree.ObjectLiteral.Key.Decoded.encode(.{ .static = key }); + c.extra.list.items[key_i] = @bitCast(key_encoded); + const in = try c.fetchExpression(value); + c.extra.list.items[value_i] = @intFromEnum(in); + } + + const node = try c.add(.object_literal, .{ + .extra_index = store_offset, + .property_count = property_count, + }); + try c.markExpression(inst, node); + }, + + // else => |tag| std.debug.panic("TODO JS codeGenInst for .{s}", .{@tagName(tag)}), + } +} + +fn markExpression(c: *NodeGen, inst: Air.Inst.Index, node: Node.Index) !void { + const t = tracer.trace(@src()); + defer t.end(); + + const flags: Air.Flags = c.air.instFlags(inst); + const i = @intFromEnum(inst); + switch (flags.uses) { + .none => { + try c.addStmt(node); + }, + .single => { + // TODO: this is not aware of side effect fences + c.air_node_map[i] = .{ .node = node }; + }, + .multi => { + const decl = try c.add(.stmt_declare_local, .{ + .identifier_index = c.next_id, + .init = node.toOptional(), + }); + try c.addStmt(decl); + const id = try c.add(.local, .{ .id = c.next_id }); + c.next_id += 1; + c.air_node_map[i] = .{ .node = id }; + }, + } +} + +fn fetchExpression(c: *NodeGen, ref: Air.Inst.Ref) !Node.Index { + const t = tracer.trace(@src()); + defer t.end(); + + return switch (ref.decode()) { + .value => |v| c.valueToExpression(v), + .index => |index| fetchInstExpression(c, index), + }; +} + +fn fetchInstExpression(c: *NodeGen, index: Air.Inst.Index) !Node.Index { + const t = tracer.trace(@src()); + defer t.end(); + + const data = c.air_node_map[@intFromEnum(index)]; + if (std.debug.runtime_safety) { + const flags: Air.Flags = c.air.instFlags(index); + switch (flags.uses) { + .none => if (std.debug.runtime_safety) { + // incorrectly marked instruction as unused + std.debug.panic("called fetchInstExpression(#{d}), but no expression was generated", .{ + @intFromEnum(index), + }); + } else unreachable, + .single => {}, + .multi => assert(c.nodes.items(.tag)[@intFromEnum(data.node)] == .local), + } + } + return data.node; +} + +fn valueToExpression(c: *NodeGen, v: Value) !Node.Index { + const t = tracer.trace(@src()); + defer t.end(); + + const intern_tag = v.valueTag(c.ip); + switch (intern_tag) { + .removed => unreachable, // UAF + + .function_type, + .simple_type, + .struct_type, + => unreachable, // Type values are not allowed here + + .int32, + .float, + .comptime_float, + .string, + .simple_value, + .struct_value, + => return try c.add(.plain_value, .{ .value = v }), + + // Hoist functions as top level values + // TODO: Consider hoisting large values too + .func => { + try c.top_level_value_deps.put(c.gpa, v, {}); + return try c.add(.top_level_value, .{ .value = v }); + }, + } +} + +fn addTemporary(c: *NodeGen) !Node.Index { + const t = tracer.trace(@src()); + defer t.end(); + + try c.addStmt(try c.add(.stmt_declare_local, .{ + .identifier_index = c.next_id, + .init = .none, + })); + const id = try c.add(.local, .{ .id = c.next_id }); + c.next_id += 1; + return id; +} + +const Branch = struct { + requires_label: bool, + list: Tree.IndexList, +}; + +fn codeGenBranch(c: *NodeGen, inst: Air.Inst.Ref, temporary: ?Node.Index) !?Branch { + const t = tracer.trace(@src()); + defer t.end(); + + const scratch_top = c.scratch.items.len; + defer c.scratch.shrinkRetainingCapacity(scratch_top); + + c.next_label += 1; + defer c.next_label -= 1; + const this_label = c.next_label; + + var requires_label = false; + var items = switch (inst.decode()) { + .value => |v| items: { + if (v == .void) + return null; + if (temporary) |temp| { + const node = try c.valueToExpression(v); + try c.addStmt(try c.add(.assign, .{ + .lhs = temp, + .rhs = node, + })); + } + break :items c.scratch.items[scratch_top..]; + }, + .index => |i| items: { + assert(c.air.instTag(i) == .block); + c.air_node_map[@intFromEnum(i)] = .{ + .block = .{ .index = @intCast(scratch_top) }, + }; + + try c.scratch.appendSlice(c.arena, &.{ + @intFromEnum(if (temporary) |temp| temp.toOptional() else .none), + this_label, + }); + + const block: Air.IndexList = c.air.instData(.block, i); + for (block.slice(c.air)) |item| { + try c.codeGenInst(item); + } + + requires_label = c.air_node_map[@intFromEnum(i)].block.used; + + break :items c.scratch.items[scratch_top + 2 ..]; + }, + }; + + const last_node = c.scratch.items[c.scratch.items.len - 1]; + if (c.nodes.items(.tag)[last_node] == .stmt_break and + c.nodes.items(.data)[last_node].decode(.stmt_break, .stmt_break).label == this_label) + { + items.len -= 1; + c.nodes.len -= 1; + } + + const top = c.extra.list.items.len; + try c.extra.list.appendSlice(c.gpa, items); + return .{ + .requires_label = requires_label, + .list = .{ + .start = @intCast(top), + .len = @intCast(items.len), + }, + }; +} + +fn codeGenBlock(c: *NodeGen, inst: Air.Inst.Index) !void { + const t = tracer.trace(@src()); + defer t.end(); + + const scratch_top = c.scratch.items.len; + errdefer c.scratch.shrinkRetainingCapacity(scratch_top); + + c.next_label += 1; + defer c.next_label -= 1; + + const next_label = c.next_label; + + try c.scratch.appendSlice(c.arena, &.{ + // @intFromEnum(if (temporary) |temp| temp.toOptional() else .none), + @intFromEnum(Node.Index.Optional.none), + next_label, + }); + + c.air_node_map[@intFromEnum(inst)] = .{ + .block = .{ .index = @intCast(scratch_top) }, + }; + const block: Air.IndexList = c.air.instData(.block, inst); + + for (block.slice(c.air)) |item| { + try c.codeGenInst(item); + } + + const items = c.scratch.items[scratch_top + 2 ..]; + const top = c.extra.list.items.len; + try c.extra.list.appendSlice(c.gpa, items); + c.scratch.shrinkRetainingCapacity(scratch_top); + try c.addStmt(try c.add(.stmt_block, .{ + .label = c.next_label, + .content = try c.extra.append(c.gpa, Tree.IndexList, .{ + .start = @intCast(top), + .len = @intCast(items.len), + }), + })); +} + +// Ideally, we convert everything to an expression. This allows for much +// more compact code. When that isnt possible (`return`, `while`, etc), +// we detect that here and ensure to lower `if` to actual if statements, +// and so on. +fn canLowerToExpression(c: *const NodeGen, inst: Air.Inst.Ref) bool { + const t = tracer.trace(@src()); + defer t.end(); + + return switch (inst.decode()) { + .value => true, + .index => |index| switch (c.air.instTag(index)) { + .js_global_ref, + .call, + .new, + .arg, + .field_opaque, + .field_struct, + .add, + .sub, + .mul, + .div, + .eq, + .lt, + .gt, + .negate, + .bool_not, + .store, + .struct_init, + => true, + + // JavaScript variable declarations require statements. + // Later, this could be made true if we can hoist temporaries up. + .memory => false, + + // break/return must be a js statement + .@"break", + => false, + + .block => { + // TODO: blocks that are + // - all statements except last are lowerable + // - the last statement is lowerable OR a break of this block + return false; + }, + + .@"if" => { + const call = c.air.instData(.@"if", index); + const data = call.data.get(c.air); + + return c.canLowerToExpression(call.cond) and + c.canLowerToExpression(data.yes) and + c.canLowerToExpression(data.no); + }, + }, + }; +} + +fn lowerToExpression(c: *NodeGen, ref: Air.Inst.Ref) !Node.Index { + const t = tracer.trace(@src()); + defer t.end(); + + assert(c.canLowerToExpression(ref)); + return switch (ref.decode()) { + .value => |v| try c.valueToExpression(v), + .index => |index| { + try c.codeGenInst(index); + return c.fetchExpression(ref); + }, + }; +} + +fn add(c: *NodeGen, comptime tag: Tree.Tag, data: tag.Data()) !Node.Index { + const t = tracer.trace(@src()); + defer t.end(); + + try c.nodes.append(c.gpa, .{ + .tag = tag, + .data = EncodedData.encode(tag, data), + }); + return @enumFromInt(c.nodes.len - 1); +} + +fn addStmt(c: *NodeGen, stmt: Node.Index) !void { + const t = tracer.trace(@src()); + defer t.end(); + + try c.scratch.append(c.arena, @intFromEnum(stmt)); +} + +const NodeGen = @This(); + +const std = @import("std"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; + +const InternPool = @import("../InternPool.zig"); +const Value = InternPool.Value; +const NullTerminatedString = InternPool.NullTerminatedString; + +const Comp = @import("../Comp.zig"); +const Decl = Comp.Decl; + +const Tree = @import("Tree.zig"); +const EncodedData = Tree.EncodedData; +const Node = Tree.Node; +const LocalIndex = Tree.LocalIndex; + +const Air = @import("../Air.zig"); + +const dod = @import("../array.zig"); +const tracer = @import("tracer"); diff --git a/src/core/js/Tree.zig b/src/core/js/Tree.zig new file mode 100644 index 0000000000000000000000000000000000000000..cd43c1d8dcb8bc5398664fe286ccc02f3b725cd2 --- /dev/null +++ b/src/core/js/Tree.zig @@ -0,0 +1,300 @@ +//! JavaScript Function Representation is the last representation before javascript +//! source bytes are created. One notable thing here is that all local identifiers +//! are numbered, instead of being named. The first `param_count` local identifiers +//! refer to parameters, and the rest refer to the respective block that defined them. + +param_count: u32, +nodes: Node.List.Slice, +extern_symbols: []String.WithLen, +top_level_value_deps: []Value, +extra: ExtraStore, + +pub const LocalIndex = u32; +pub const ExtraStore = extra.ExtraStore(.js_tree, .@"const"); +pub const EncodedData = extra.EncodedData(Tag, u64); +pub const ExtraIndex = ExtraStore.Index; + +pub const Tag = enum { + root, + /// Only allow simple value types: int, float, string, bool + /// This value type is inlined into the call site + plain_value, + /// Reference to a top level value. (give a Value) + /// This is used for function values / large constant objects + top_level_value, + /// Reference to a local identifier (give an VarId) + local, + /// Reference to a global identifier (give a NullTerminatedString) + global, + // Bin ops + assign, + assign_add, + assign_sub, + assign_mul, + assign_div, + add, + sub, + mul, + div, + eq, + lt, + gt, + // Exprs + call, + new, + field, + ternary, + object_literal, + // Prefix ops + negate, + bool_not, + pre_inc, + pre_dec, + // Statements + /// `let ... = ...` + stmt_declare_local, + /// `if (...) ... else ...` + stmt_if, + /// `break ... ...` + stmt_break, + /// `return ...` (break from block #0) + stmt_return, + /// `...: { ... }` + stmt_block, + + pub fn Data(comptime tag: Tag) type { + return switch (tag) { + .root => IndexList, + .plain_value => PlainValue, + .top_level_value => ValueRef, + .local => LocalRef, + .global => GlobalRef, + .assign, + .assign_add, + .assign_sub, + .assign_mul, + .assign_div, + .add, + .sub, + .mul, + .div, + .eq, + .lt, + .gt, + => Bin, + .ternary => Ternary, + .negate, + .bool_not, + .pre_inc, + .pre_dec, + => Un, + .object_literal => ObjectLiteral, + .call, .new => Call, + .field => Field, + .stmt_if => StmtIf, + .stmt_break => StmtBreak, + .stmt_return => StmtReturn, + .stmt_declare_local => StmtDeclareLocal, + .stmt_block => StmtBlock, + }; + } +}; + +pub const Node = struct { + tag: Tag, + data: EncodedData, + + pub const List = std.MultiArrayList(Node); + + pub const Index = enum(u32) { + root = 0, + _, + + pub fn toOptional(i: Index) Optional { + return @enumFromInt(@intFromEnum(i)); + } + + pub const Optional = enum(u32) { + none = std.math.maxInt(u32), + _, + + pub fn unwrap(oi: Optional) ?Index { + return if (oi == .none) + null + else + @enumFromInt(@intFromEnum(oi)); + } + }; + }; +}; + +pub const PlainValue = struct { + value: Value, + unused: Unused = .zero, +}; + +pub const LocalRef = struct { + id: LocalIndex, + unused: Unused = .zero, +}; + +pub const GlobalRef = struct { + name: InternPool.String.WithLen, +}; + +pub const ValueRef = struct { + value: Value, + unused: Unused = .zero, +}; + +pub const StmtDeclareLocal = struct { + identifier_index: u32, + init: Node.Index.Optional, +}; + +pub const Bin = struct { + lhs: Node.Index, + rhs: Node.Index, +}; + +pub const Un = struct { + operand: Node.Index, + unused: Unused = .zero, +}; + +pub const Call = struct { + base: Node.Index, + args: ExtraIndex(IndexList), +}; + +pub const Field = struct { + base: Node.Index, + field: NullTerminatedString, +}; + +/// Extra list is `property_count * 2` items long, with the first half being +/// `ObjectLiteral.Key`s, and the second being value nodes. +pub const ObjectLiteral = struct { + extra_index: u32, + property_count: u32, + + pub const Key = packed struct(u32) { + kind: Kind, + data: u31, + + pub const Kind = enum(u1) { + static, + computed, + }; + + pub const Decoded = union(enum) { + static: NullTerminatedString, + computed: Node.Index, + + pub fn encode(decoded: Decoded) Key { + return switch (decoded) { + inline else => |data, tag| .{ + .kind = @field(Kind, @tagName(tag)), + .data = @intCast(@intFromEnum(data)), + }, + }; + } + }; + + pub fn decode(key: Key) Decoded { + return switch (key.kind) { + .static => .{ .static = @enumFromInt(key.data) }, + .computed => .{ .computed = @enumFromInt(key.data) }, + }; + } + }; +}; + +pub const IndexList = struct { + start: u32, + len: u32, + + pub fn slice(list: IndexList, tree: *const Tree) []const Node.Index { + return @ptrCast(tree.extra.list.items[list.start..][0..list.len]); + } +}; + +pub const StmtIf = struct { + cond: Node.Index, + data: ExtraIndex(Data), + + pub const Data = struct { + yes: IndexList, + no: IndexList, + flags: Flags, + }; + + pub const Flags = packed struct(u32) { + block_label: u16, + yes_label: bool, + no_label: bool, + _: u14 = 0, + }; +}; + +pub const Ternary = struct { + cond: Node.Index, + data: ExtraIndex(Data), + + pub const Data = struct { + yes: Node.Index, + no: Node.Index, + }; +}; + +pub const StmtBreak = struct { + label: u32, + unused: Unused = .zero, +}; + +pub const StmtReturn = struct { + operand: Node.Index.Optional, + unused: Unused = .zero, +}; + +pub const StmtBlock = struct { + label: u32, + content: ExtraIndex(IndexList), +}; + +pub fn deinit(tree: *Tree, gpa: std.mem.Allocator) void { + tree.nodes.deinit(gpa); + gpa.free(tree.top_level_value_deps); + gpa.free(tree.extern_symbols); + tree.extra.deinit(gpa); +} + +pub fn nodeTag(tree: Tree, index: Node.Index) Tag { + return tree.nodes.items(.tag)[@intFromEnum(index)]; +} + +pub fn nodeData(tree: Tree, comptime tag: Tag, index: Node.Index) tag.Data() { + return tree.nodes.items(.data)[@intFromEnum(index)].decode(tag, tree.nodeTag(index)); +} + +pub fn mainStatements(tree: *const Tree) []const Node.Index { + return tree.nodeData(.root, .root).slice(tree); +} + +const Tree = @This(); + +const std = @import("std"); +const assert = std.debug.assert; + +const InternPool = @import("../InternPool.zig"); +const Value = InternPool.Value; + +const NullTerminatedString = InternPool.NullTerminatedString; +const String = InternPool.String; + +const Comp = @import("../Comp.zig"); +const Decl = Comp.Decl; + +const extra = @import("../array.zig"); + +const Unused = @import("../Ast.zig").Unused; diff --git a/src/core/main.zig b/src/core/main.zig index f2b314fd4bfc22515cf121095da54542b5181aa2..0597ba074b93bad26537b41259942dfb6ad94aeb 100644 --- a/src/core/main.zig +++ b/src/core/main.zig @@ -101,11 +101,14 @@ pub fn main() !void { std.process.exit(1); }; + var js_backend = core.Js{}; + var comp: Comp = .{ .gpa = tracer.tracingAllocator(&alloc), .options = .{ .dump_ir = dump_ir, }, + .backend = .{ .js = &js_backend }, }; try comp.init(); defer comp.deinit(); @@ -130,7 +133,7 @@ pub fn main() !void { try teg.dumpToWriter(stderr); } - comp.semaEntryPoint(main_file) catch |err| switch (err) { + const entry_point = comp.semaEntryPoint(main_file) catch |err| switch (err) { error.AnalysisFail => std.process.exit(1), else => return err, }; @@ -138,7 +141,7 @@ pub fn main() !void { const output = std.io.getStdOut().writer(); var bufw = std.io.bufferedWriter(output); const writer = bufw.writer(); - try comp.link(writer); + try js_backend.link(&comp, entry_point, writer.any()); try bufw.flush(); std.process.cleanExit(); diff --git a/src/core/mod.zig b/src/core/mod.zig index 13e8201c4096daee413f86323e584132c7e7b9ca..0b57cd8206e6d313dad4757c9324a559e67cbe62 100644 --- a/src/core/mod.zig +++ b/src/core/mod.zig @@ -1,18 +1,75 @@ +//! Core language compiler module. + +// sort-lines: start + +/// Analyzed Intermediate Representation. pub const Air = @import("Air.zig"); + pub const array = @import("array.zig"); + +/// Abstract Syntax Tree - Preserves all syntax forms. +pub const Ast = @import("Ast.zig"); + pub const Cache = @import("Cache.zig"); + +/// Language Compilation Unit pub const Comp = @import("Comp.zig"); + +/// Storage for shared objects (`Value`/`Decl`/`Namespace`) in a compilation pub const InternPool = @import("InternPool.zig"); -pub const JsLink = @import("JsLink.zig"); + +/// JavaScript backend and related utilities +pub const Js = @import("js/Js.zig"); + +/// Recursive descent parser. Produces `Ast` pub const Parse = @import("Parse.zig"); + pub const Report = @import("Report.zig"); + +/// Semantic Analysis visits a `Teg` declaration and produces `Air` pub const Sema = @import("Sema.zig"); + +/// Untyped Intermediate Representation. Flat instruction list pub const Teg = @import("Teg.zig"); + +/// Iterate tokens from a `[:0]const u8`. Includes code to decode/encode string escapes. pub const Tokenizer = @import("Tokenizer.zig"); + +/// Transform pass converts `Ast` into `Teg` pub const Transform = @import("Transform.zig"); + pub const unicode_props = @import("unicode_props.zig"); + pub const util = @import("util.zig"); +// sort-lines: end + +// The rest of this file is utility functions, which are not specific to any of +// the above namespaces. Remember Zig discourages namespaces like `utils` + +/// This is an alternative to `std.debug.assert`, where you want to print some +/// debug info instead of "reached unreachable code". Usage is as follows: +/// +/// if (assertFailed(variantIsTrue())) { +/// std.debug.panic("...", .{ ... }); +/// } +/// +/// This allows you to perform any number of side effects in the failure case +/// with certainty that they are not analyzed in unsafe modes. +pub inline fn assertFailed(cond: bool) bool { + if (std.debug.runtime_safety) { + return !cond; + } else if (!cond) { + unreachable; // assertion failure + } +} + +pub const detectTTYFromWriter = util.detectTTYFromWriter; +pub const Ordinal = util.Ordinal; + +const std = @import("std"); +const assert = std.debug.assert; + comptime { - @import("std").testing.refAllDecls(@This()); + std.testing.refAllDecls(@This()); } diff --git a/src/core/util.zig b/src/core/util.zig index 1f41f2b2a29093c29ce432748ee169449e9ce093..2062d3eceb0f4931f825fb56ffdb0b95392db1ac 100644 --- a/src/core/util.zig +++ b/src/core/util.zig @@ -1,3 +1,5 @@ +// TODO: move all decls in this file into mod.zig + pub fn detectTTYFromWriter(writer: anytype) std.io.tty.Config { if (@hasField(@TypeOf(writer), "unbuffered_writer")) { return detectTTYFromWriter(writer.unbuffered_writer); @@ -40,15 +42,6 @@ pub fn Ordinal(Int: type) type { pub fn add(ord: @This(), inc: Int) @This() { return fromZeroBased(ord.zeroBased() + inc); } - - pub fn format( - _: @This(), - comptime _: []const u8, - _: std.fmt.FormatOptions, - _: anytype, - ) !void { - // @compileError("Do not format `Ordinal` directly. Call oneBased or zeroBased to specify."); - } }; }