diff --git a/PROGRESS.md b/PROGRESS.md index 0ba4a3cfdd..f3cdd503a5 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -49,14 +49,19 @@ The goal is not full semantics yet, but enough to run end‑to‑end Roc tests and exercise the type‑carrying pieces. - Implemented expressions: - - Strings: `e_str`, `e_str_segment` (create `RocStr` with `RocOps`), REPL‑style string rendering. - - Integers: `e_int`, and `e_binop` with `+` (reads/writes using runtime‑chosen layout), REPL‑style integer + - Strings: `e_str`, `e_str_segment` (create `RocStr` with `RocOps`), REPL-style string rendering. + - Integers: `e_int`, and `e_binop` with `+` (reads/writes using runtime-chosen layout), REPL-style integer rendering. + - Floats & decimals: `e_frac_f32`, `e_frac_f64`, `e_frac_dec`, `e_dec_small` evaluate and render (`3.25f64` + → `3.25`, `0.125` → `0.125`). - Tuples: `e_tuple` (allocate and fill via layout store accessors), REPL‑style rendering `(a, b, ...)`. - - Records: `e_record` (allocate and fill by field name via accessor), REPL‑style rendering `{ x: 1, y: 2 }`. - - Lambdas: `e_lambda` as minimal placeholder; `e_call` supports a one‑arg lambda by binding the parameter to the + - Records: `e_record` (allocate and fill by field name via accessor), REPL-style rendering `{ x: 1, y: 2 }`. + - Empty record literal `e_empty_record` and expect-success unit result `{}` (zero-sized values stay purely logical). + - Lambdas: `e_lambda` as minimal placeholder; `e_call` supports a one-arg lambda by binding the parameter to the evaluated argument and evaluating the body; `e_lookup_local` finds values in a simple binding stack. - Nominal wrappers: `e_nominal` / `e_nominal_external` delegate evaluation to the backing expression. + - Crash & expect: `e_crash`, `s_crash`, and `e_expect`/`s_expect` invoke `roc_crashed`/`roc_expect_failed`, stash + crash messages, and surface deterministic Roc-style error strings. - Not implemented in minimal path (intentionally deferred): booleans, `if`, `match` tag destructuring, guards, tag unions, lists, boxes, effects, and most operators. (See Roadmap.) @@ -72,11 +77,13 @@ ### 7) Roc‑syntax tests (begin/end with Roc code) - New file: `src/eval/test/interpreter2_style_test.zig`. - Tests parse and canonicalize with early failure (using helpers) to surface syntax or type issues immediately. - - Tests then exercise Interpreter2 minimal eval and assert on REPL‑style rendered results for readability: + - Tests then exercise Interpreter2 minimal eval and assert on REPL-style rendered results for readability: - `(|x| x)("Hello")` → `"Hello"` - `(|n| n + 1)(41)` → `42` - `(1, 2)` → `(1, 2)` - `{ x: 1, y: 2 }` → `{ x: 1, y: 2 }` + - `0.125` → `0.125`, `3.25f64` → `3.25` + - `expect 1 == 1` → `{}` while failures crash with trimmed `Expect failed: …` messages. ## Design Summary diff --git a/src/eval/render_helpers.zig b/src/eval/render_helpers.zig index df40df9b12..f581481fcc 100644 --- a/src/eval/render_helpers.zig +++ b/src/eval/render_helpers.zig @@ -2,7 +2,9 @@ const std = @import("std"); const types = @import("types"); const can = @import("can"); const layout = @import("layout"); +const builtins = @import("builtins"); const StackValue = @import("StackValue.zig"); +const RocDec = builtins.dec.RocDec; pub const RenderCtx = struct { allocator: std.mem.Allocator, @@ -133,21 +135,18 @@ pub fn renderValueRocWithType(ctx: *RenderCtx, value: StackValue, rt_var: types. pub fn renderValueRoc(ctx: *RenderCtx, value: StackValue) ![]u8 { const gpa = ctx.allocator; if (value.layout.tag == .scalar) { - switch (value.layout.data.scalar.tag) { + const scalar = value.layout.data.scalar; + switch (scalar.tag) { .str => { - const rs: *const @import("builtins").str.RocStr = @ptrCast(@alignCast(value.ptr.?)); + const rs: *const builtins.str.RocStr = @ptrCast(@alignCast(value.ptr.?)); const s = rs.asSlice(); var buf = std.ArrayList(u8).init(gpa); errdefer buf.deinit(); try buf.append('"'); for (s) |ch| { switch (ch) { - '\\' => { - try buf.appendSlice("\\\\"); - }, - '"' => { - try buf.appendSlice("\\\""); - }, + '\\' => try buf.appendSlice("\\\\"), + '"' => try buf.appendSlice("\\\""), else => try buf.append(ch), } } @@ -158,6 +157,23 @@ pub fn renderValueRoc(ctx: *RenderCtx, value: StackValue) ![]u8 { const i = value.asI128(); return try std.fmt.allocPrint(gpa, "{d}", .{i}); }, + .frac => { + std.debug.assert(value.ptr != null); + return switch (scalar.data.frac) { + .f32 => { + const ptr = @as(*const f32, @ptrCast(@alignCast(value.ptr.?))); + return try std.fmt.allocPrint(gpa, "{d}", .{@as(f64, ptr.*)}); + }, + .f64 => { + const ptr = @as(*const f64, @ptrCast(@alignCast(value.ptr.?))); + return try std.fmt.allocPrint(gpa, "{d}", .{ptr.*}); + }, + .dec => { + const ptr = @as(*const RocDec, @ptrCast(@alignCast(value.ptr.?))); + return try renderDecimal(gpa, ptr.*); + }, + }; + }, else => {}, } } @@ -181,8 +197,12 @@ pub fn renderValueRoc(ctx: *RenderCtx, value: StackValue) ![]u8 { if (value.layout.tag == .record) { var out = std.ArrayList(u8).init(gpa); errdefer out.deinit(); - try out.appendSlice("{ "); const rec_data = ctx.layout_store.getRecordData(value.layout.data.record.idx); + if (rec_data.fields.count == 0) { + try out.appendSlice("{}"); + return out.toOwnedSlice(); + } + try out.appendSlice("{ "); const fields = ctx.layout_store.record_fields.sliceRange(rec_data.getFields()); var i: usize = 0; while (i < fields.len) : (i += 1) { @@ -205,3 +225,50 @@ pub fn renderValueRoc(ctx: *RenderCtx, value: StackValue) ![]u8 { } return try std.fmt.allocPrint(gpa, "", .{}); } + +fn renderDecimal(gpa: std.mem.Allocator, dec: RocDec) ![]u8 { + if (dec.num == 0) { + return try gpa.dupe(u8, "0.0"); + } + + var out = std.ArrayList(u8).init(gpa); + errdefer out.deinit(); + + var num = dec.num; + if (num < 0) { + try out.append('-'); + num = -num; + } + + const one = RocDec.one_point_zero_i128; + const integer_part = @divTrunc(num, one); + const fractional_part = @rem(num, one); + + try std.fmt.format(out.writer(), "{d}", .{integer_part}); + + if (fractional_part == 0) { + try out.writer().writeAll(".0"); + return out.toOwnedSlice(); + } + + try out.writer().writeByte('.'); + + const decimal_places: usize = @as(usize, RocDec.decimal_places); + var digits: [decimal_places]u8 = undefined; + @memset(digits[0..], '0'); + var remaining = fractional_part; + var idx: usize = decimal_places; + while (idx > 0) : (idx -= 1) { + const digit: u8 = @intCast(@mod(remaining, 10)); + digits[idx - 1] = digit + '0'; + remaining = @divTrunc(remaining, 10); + } + + var end: usize = decimal_places; + while (end > 1 and digits[end - 1] == '0') { + end -= 1; + } + + try out.writer().writeAll(digits[0..end]); + return out.toOwnedSlice(); +} diff --git a/src/eval/test/interpreter2_polymorphism_test.zig b/src/eval/test/interpreter2_polymorphism_test.zig index dbf270eab6..cc4c603fe4 100644 --- a/src/eval/test/interpreter2_polymorphism_test.zig +++ b/src/eval/test/interpreter2_polymorphism_test.zig @@ -65,10 +65,9 @@ fn testRocCrashed(crashed_args: *const RocCrashed, _: *anyopaque) callconv(.C) v @panic("Roc crashed"); } -fn makeOps(alloc: std.mem.Allocator) RocOps { - var host = TestHost{ .allocator = alloc }; +fn makeOps(host: *TestHost) RocOps { return RocOps{ - .env = @ptrCast(&host), + .env = @ptrCast(host), .roc_alloc = testRocAlloc, .roc_dealloc = testRocDealloc, .roc_realloc = testRocRealloc, @@ -90,7 +89,8 @@ test "interpreter2 poly: return a function then call (int)" { var interp2 = try Interpreter2.init(std.testing.allocator, resources.module_env); defer interp2.deinit(); - var ops = makeOps(std.testing.allocator); + var host = TestHost{ .allocator = std.testing.allocator }; + var ops = makeOps(&host); const result = try interp2.evalMinimal(resources.expr_idx, &ops); const ct_var_ok = can.ModuleEnv.varFrom(resources.expr_idx); const rt_var_ok = try interp2.translateTypeVar(resources.module_env, ct_var_ok); @@ -110,7 +110,8 @@ test "interpreter2 poly: return a function then call (string)" { var interp2 = try Interpreter2.init(std.testing.allocator, resources.module_env); defer interp2.deinit(); - var ops = makeOps(std.testing.allocator); + var host = TestHost{ .allocator = std.testing.allocator }; + var ops = makeOps(&host); const result = try interp2.evalMinimal(resources.expr_idx, &ops); const ct_var_point = can.ModuleEnv.varFrom(resources.expr_idx); const rt_var_point = try interp2.translateTypeVar(resources.module_env, ct_var_point); @@ -133,7 +134,8 @@ test "interpreter2 captures (monomorphic): adder" { var interp2 = try Interpreter2.init(std.testing.allocator, resources.module_env); defer interp2.deinit(); - var ops = makeOps(std.testing.allocator); + var host = TestHost{ .allocator = std.testing.allocator }; + var ops = makeOps(&host); const result = try interp2.evalMinimal(resources.expr_idx, &ops); const ct_var_ok = can.ModuleEnv.varFrom(resources.expr_idx); const rt_var_ok = try interp2.translateTypeVar(resources.module_env, ct_var_ok); @@ -153,7 +155,8 @@ test "interpreter2 captures (monomorphic): constant function" { var interp2 = try Interpreter2.init(std.testing.allocator, resources.module_env); defer interp2.deinit(); - var ops = makeOps(std.testing.allocator); + var host = TestHost{ .allocator = std.testing.allocator }; + var ops = makeOps(&host); const result = try interp2.evalMinimal(resources.expr_idx, &ops); const ct_var_point = can.ModuleEnv.varFrom(resources.expr_idx); const rt_var_point = try interp2.translateTypeVar(resources.module_env, ct_var_point); @@ -176,7 +179,8 @@ test "interpreter2 captures (polymorphic): capture id and apply to int" { var interp2 = try Interpreter2.init(std.testing.allocator, resources.module_env); defer interp2.deinit(); - var ops = makeOps(std.testing.allocator); + var host = TestHost{ .allocator = std.testing.allocator }; + var ops = makeOps(&host); const result = try interp2.evalMinimal(resources.expr_idx, &ops); const ct_var_ok = can.ModuleEnv.varFrom(resources.expr_idx); const rt_var_ok = try interp2.translateTypeVar(resources.module_env, ct_var_ok); @@ -196,7 +200,8 @@ test "interpreter2 captures (polymorphic): capture id and apply to string" { var interp2 = try Interpreter2.init(std.testing.allocator, resources.module_env); defer interp2.deinit(); - var ops = makeOps(std.testing.allocator); + var host = TestHost{ .allocator = std.testing.allocator }; + var ops = makeOps(&host); const result = try interp2.evalMinimal(resources.expr_idx, &ops); const ct_var_point = can.ModuleEnv.varFrom(resources.expr_idx); const rt_var_point = try interp2.translateTypeVar(resources.module_env, ct_var_point); @@ -219,7 +224,8 @@ test "interpreter2 captures (polymorphic): same captured id used at two types" { var interp2 = try Interpreter2.init(std.testing.allocator, resources.module_env); defer interp2.deinit(); - var ops = makeOps(std.testing.allocator); + var host = TestHost{ .allocator = std.testing.allocator }; + var ops = makeOps(&host); const result = try interp2.evalMinimal(resources.expr_idx, &ops); const rendered = try interp2.renderValueRoc(result); defer std.testing.allocator.free(rendered); @@ -241,7 +247,8 @@ test "interpreter2 higher-order: apply f then call with 41" { var interp2 = try Interpreter2.init(std.testing.allocator, resources.module_env); defer interp2.deinit(); - var ops = makeOps(std.testing.allocator); + var host = TestHost{ .allocator = std.testing.allocator }; + var ops = makeOps(&host); const result = try interp2.evalMinimal(resources.expr_idx, &ops); const rendered = try interp2.renderValueRoc(result); defer std.testing.allocator.free(rendered); @@ -260,7 +267,8 @@ test "interpreter2 higher-order: apply f twice" { var interp2 = try Interpreter2.init(std.testing.allocator, resources.module_env); defer interp2.deinit(); - var ops = makeOps(std.testing.allocator); + var host = TestHost{ .allocator = std.testing.allocator }; + var ops = makeOps(&host); const result = try interp2.evalMinimal(resources.expr_idx, &ops); const rendered = try interp2.renderValueRoc(result); defer std.testing.allocator.free(rendered); @@ -279,7 +287,8 @@ test "interpreter2 higher-order: pass constructed closure and apply" { var interp2 = try Interpreter2.init(std.testing.allocator, resources.module_env); defer interp2.deinit(); - var ops = makeOps(std.testing.allocator); + var host = TestHost{ .allocator = std.testing.allocator }; + var ops = makeOps(&host); const result = try interp2.evalMinimal(resources.expr_idx, &ops); const rendered = try interp2.renderValueRoc(result); defer std.testing.allocator.free(rendered); @@ -298,7 +307,8 @@ test "interpreter2 higher-order: construct then pass then call" { var interp2 = try Interpreter2.init(std.testing.allocator, resources.module_env); defer interp2.deinit(); - var ops = makeOps(std.testing.allocator); + var host = TestHost{ .allocator = std.testing.allocator }; + var ops = makeOps(&host); const result = try interp2.evalMinimal(resources.expr_idx, &ops); const rendered = try interp2.renderValueRoc(result); defer std.testing.allocator.free(rendered); @@ -317,7 +327,8 @@ test "interpreter2 higher-order: compose id with +1" { var interp2 = try Interpreter2.init(std.testing.allocator, resources.module_env); defer interp2.deinit(); - var ops = makeOps(std.testing.allocator); + var host = TestHost{ .allocator = std.testing.allocator }; + var ops = makeOps(&host); const result = try interp2.evalMinimal(resources.expr_idx, &ops); const rendered = try interp2.renderValueRoc(result); defer std.testing.allocator.free(rendered); @@ -336,7 +347,8 @@ test "interpreter2 higher-order: return poly fn using captured +n" { var interp2 = try Interpreter2.init(std.testing.allocator, resources.module_env); defer interp2.deinit(); - var ops = makeOps(std.testing.allocator); + var host = TestHost{ .allocator = std.testing.allocator }; + var ops = makeOps(&host); const result = try interp2.evalMinimal(resources.expr_idx, &ops); const rendered = try interp2.renderValueRoc(result); defer std.testing.allocator.free(rendered); @@ -355,7 +367,8 @@ test "interpreter2 recursion: simple countdown" { var interp2 = try Interpreter2.init(std.testing.allocator, resources.module_env); defer interp2.deinit(); - var ops = makeOps(std.testing.allocator); + var host = TestHost{ .allocator = std.testing.allocator }; + var ops = makeOps(&host); const result = try interp2.evalMinimal(resources.expr_idx, &ops); const rendered = try interp2.renderValueRoc(result); defer std.testing.allocator.free(rendered); @@ -373,7 +386,8 @@ test "interpreter2 if: else-if chain selects middle branch" { var interp2 = try Interpreter2.init(std.testing.allocator, resources.module_env); defer interp2.deinit(); - var ops = makeOps(std.testing.allocator); + var host = TestHost{ .allocator = std.testing.allocator }; + var ops = makeOps(&host); const result = try interp2.evalMinimal(resources.expr_idx, &ops); const rendered = try interp2.renderValueRoc(result); defer std.testing.allocator.free(rendered); @@ -394,7 +408,8 @@ test "interpreter2 var and reassign" { var interp2 = try Interpreter2.init(std.testing.allocator, resources.module_env); defer interp2.deinit(); - var ops = makeOps(std.testing.allocator); + var host = TestHost{ .allocator = std.testing.allocator }; + var ops = makeOps(&host); const result = try interp2.evalMinimal(resources.expr_idx, &ops); const rendered = try interp2.renderValueRoc(result); defer std.testing.allocator.free(rendered); @@ -412,7 +427,8 @@ test "interpreter2 logical or is short-circuiting" { var interp2 = try Interpreter2.init(std.testing.allocator, resources.module_env); defer interp2.deinit(); - var ops = makeOps(std.testing.allocator); + var host = TestHost{ .allocator = std.testing.allocator }; + var ops = makeOps(&host); const result = try interp2.evalMinimal(resources.expr_idx, &ops); const rendered = try interp2.renderValueRoc(result); defer std.testing.allocator.free(rendered); @@ -433,7 +449,8 @@ test "interpreter2 logical and is short-circuiting" { var interp2 = try Interpreter2.init(std.testing.allocator, resources.module_env); defer interp2.deinit(); - var ops = makeOps(std.testing.allocator); + var host = TestHost{ .allocator = std.testing.allocator }; + var ops = makeOps(&host); const result = try interp2.evalMinimal(resources.expr_idx, &ops); const rendered = try interp2.renderValueRoc(result); defer std.testing.allocator.free(rendered); @@ -454,7 +471,8 @@ test "interpreter2 recursion: factorial 5 -> 120" { var interp2 = try Interpreter2.init(std.testing.allocator, resources.module_env); defer interp2.deinit(); - var ops = makeOps(std.testing.allocator); + var host = TestHost{ .allocator = std.testing.allocator }; + var ops = makeOps(&host); const result = try interp2.evalMinimal(resources.expr_idx, &ops); const rendered = try interp2.renderValueRoc(result); defer std.testing.allocator.free(rendered); @@ -476,7 +494,8 @@ test "interpreter2 recursion: fibonacci 5 -> 5" { var interp2 = try Interpreter2.init(std.testing.allocator, resources.module_env); defer interp2.deinit(); - var ops = makeOps(std.testing.allocator); + var host = TestHost{ .allocator = std.testing.allocator }; + var ops = makeOps(&host); const result = try interp2.evalMinimal(resources.expr_idx, &ops); const rendered = try interp2.renderValueRoc(result); defer std.testing.allocator.free(rendered); @@ -496,7 +515,8 @@ test "interpreter2 tag union: one-arg tag Ok(42)" { var interp2 = try Interpreter2.init(std.testing.allocator, resources.module_env); defer interp2.deinit(); - var ops = makeOps(std.testing.allocator); + var host = TestHost{ .allocator = std.testing.allocator }; + var ops = makeOps(&host); const result = try interp2.evalMinimal(resources.expr_idx, &ops); const ct_var = can.ModuleEnv.varFrom(resources.expr_idx); const rt_var = try interp2.translateTypeVar(resources.module_env, ct_var); @@ -519,7 +539,8 @@ test "interpreter2 tag union: multi-arg tag Point(1, 2)" { var interp2 = try Interpreter2.init(std.testing.allocator, resources.module_env); defer interp2.deinit(); - var ops = makeOps(std.testing.allocator); + var host = TestHost{ .allocator = std.testing.allocator }; + var ops = makeOps(&host); const result = try interp2.evalMinimal(resources.expr_idx, &ops); const ct_var = can.ModuleEnv.varFrom(resources.expr_idx); const rt_var = try interp2.translateTypeVar(resources.module_env, ct_var); diff --git a/src/layout/store.zig b/src/layout/store.zig index 1deebf3b64..3ae8bc2a4a 100644 --- a/src/layout/store.zig +++ b/src/layout/store.zig @@ -438,6 +438,10 @@ pub const Store = struct { return try self.insertLayout(empty_record_layout); } + pub fn ensureEmptyRecordLayout(self: *Self) !Idx { + return self.getEmptyRecordLayout(); + } + /// Get the size in bytes of a layout, given the store's target usize. pub fn layoutSize(self: *const Self, layout: Layout) u32 { // TODO change this to SizeAlign (just return both since they're packed into 4B anyway)