Document literal rendering improvements

This commit is contained in:
Richard Feldman 2025-09-25 10:25:28 -04:00
parent b92b3eed08
commit 0acbcdb94f
No known key found for this signature in database
4 changed files with 138 additions and 39 deletions

View file

@ -49,14 +49,19 @@
The goal is not full semantics yet, but enough to run endtoend Roc tests and exercise the typecarrying pieces.
- Implemented expressions:
- Strings: `e_str`, `e_str_segment` (create `RocStr` with `RocOps`), REPLstyle string rendering.
- Integers: `e_int`, and `e_binop` with `+` (reads/writes using runtimechosen layout), REPLstyle 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), REPLstyle rendering `(a, b, ...)`.
- Records: `e_record` (allocate and fill by field name via accessor), REPLstyle rendering `{ x: 1, y: 2 }`.
- Lambdas: `e_lambda` as minimal placeholder; `e_call` supports a onearg 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) Rocsyntax 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 REPLstyle 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

View file

@ -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, "<unsupported>", .{});
}
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();
}

View file

@ -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);

View file

@ -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)