Add to_str to other number types

This commit is contained in:
Richard Feldman 2025-11-24 00:01:50 -05:00
parent 53d31c8003
commit c92babb47d
No known key found for this signature in database
5 changed files with 291 additions and 14 deletions

View file

@ -167,12 +167,44 @@ fn replaceStrIsEmptyWithLowLevel(env: *ModuleEnv) !std.ArrayList(CIR.Def.Idx) {
try low_level_map.put(ident, .num_is_ne);
}
// Dec to_str operation
// Note: Dec is nested under Num in Builtin.roc, so the canonical identifier is
// Numeric to_str operations (all numeric types)
// Note: Types like Dec are nested under Num in Builtin.roc, so the canonical identifier is
// "Builtin.Num.Dec.to_str". But Dec is auto-imported as "Dec", so user code
// calling Dec.to_str looks up "Builtin.Dec.to_str". We need the canonical name here.
if (env.common.findIdent("Builtin.Num.Dec.to_str")) |ident| {
try low_level_map.put(ident, .dec_to_str);
for (numeric_types) |num_type| {
var buf: [256]u8 = undefined;
const to_str_name = try std.fmt.bufPrint(&buf, "Builtin.Num.{s}.to_str", .{num_type});
if (env.common.findIdent(to_str_name)) |ident| {
const low_level_op: CIR.Expr.LowLevel = if (std.mem.eql(u8, num_type, "U8"))
.u8_to_str
else if (std.mem.eql(u8, num_type, "I8"))
.i8_to_str
else if (std.mem.eql(u8, num_type, "U16"))
.u16_to_str
else if (std.mem.eql(u8, num_type, "I16"))
.i16_to_str
else if (std.mem.eql(u8, num_type, "U32"))
.u32_to_str
else if (std.mem.eql(u8, num_type, "I32"))
.i32_to_str
else if (std.mem.eql(u8, num_type, "U64"))
.u64_to_str
else if (std.mem.eql(u8, num_type, "I64"))
.i64_to_str
else if (std.mem.eql(u8, num_type, "U128"))
.u128_to_str
else if (std.mem.eql(u8, num_type, "I128"))
.i128_to_str
else if (std.mem.eql(u8, num_type, "Dec"))
.dec_to_str
else if (std.mem.eql(u8, num_type, "F32"))
.f32_to_str
else if (std.mem.eql(u8, num_type, "F64"))
.f64_to_str
else
continue;
try low_level_map.put(ident, low_level_op);
}
}
// Numeric comparison operations (all numeric types)
@ -309,7 +341,7 @@ fn replaceStrIsEmptyWithLowLevel(env: *ModuleEnv) !std.ArrayList(CIR.Def.Idx) {
// Create parameter patterns for the lambda
// Binary operations need 2 parameters, unary operations need 1
const num_params: u32 = switch (low_level_op) {
.num_negate, .num_is_zero, .num_is_negative, .num_is_positive, .num_from_numeral, .num_from_int_digits, .dec_to_str => 1,
.num_negate, .num_is_zero, .num_is_negative, .num_is_positive, .num_from_numeral, .num_from_int_digits, .u8_to_str, .i8_to_str, .u16_to_str, .i16_to_str, .u32_to_str, .i32_to_str, .u64_to_str, .i64_to_str, .u128_to_str, .i128_to_str, .dec_to_str, .f32_to_str, .f64_to_str => 1,
else => 2, // Most numeric operations are binary
};
@ -357,15 +389,22 @@ fn replaceStrIsEmptyWithLowLevel(env: *ModuleEnv) !std.ArrayList(CIR.Def.Idx) {
}
}
// Expose Dec.to_str under the alias "Builtin.Dec.to_str" for user code lookups.
// The canonical name is "Builtin.Num.Dec.to_str" (since Dec is nested under Num),
// Expose to_str under aliases like "Builtin.Dec.to_str" for user code lookups.
// The canonical names are like "Builtin.Num.Dec.to_str" (since numeric types are nested under Num),
// but user code calling Dec.to_str will look for "Builtin.Dec.to_str".
if (env.common.findIdent("Builtin.Num.Dec.to_str")) |canonical_ident| {
if (env.getExposedNodeIndexById(canonical_ident)) |node_idx| {
// Insert the alias identifier
const alias_ident = try env.common.insertIdent(gpa, base.Ident.for_text("Builtin.Dec.to_str"));
// Expose the same node under the alias
try env.common.setNodeIndexById(gpa, alias_ident, node_idx);
for (numeric_types) |num_type| {
var canonical_buf: [256]u8 = undefined;
var alias_buf: [256]u8 = undefined;
const canonical_name = try std.fmt.bufPrint(&canonical_buf, "Builtin.Num.{s}.to_str", .{num_type});
const alias_name = try std.fmt.bufPrint(&alias_buf, "Builtin.{s}.to_str", .{num_type});
if (env.common.findIdent(canonical_name)) |canonical_ident| {
if (env.getExposedNodeIndexById(canonical_ident)) |node_idx| {
// Insert the alias identifier
const alias_ident = try env.common.insertIdent(gpa, base.Ident.for_text(alias_name));
// Expose the same node under the alias
try env.common.setNodeIndexById(gpa, alias_ident, node_idx);
}
}
}

View file

@ -139,6 +139,7 @@ Builtin :: [].{
}
U8 :: [].{
to_str : U8 -> Str
is_zero : U8 -> Bool
is_eq : U8, U8 -> Bool
is_gt : U8, U8 -> Bool
@ -158,6 +159,7 @@ Builtin :: [].{
}
I8 :: [].{
to_str : I8 -> Str
is_zero : I8 -> Bool
is_negative : I8 -> Bool
is_positive : I8 -> Bool
@ -180,6 +182,7 @@ Builtin :: [].{
}
U16 :: [].{
to_str : U16 -> Str
is_zero : U16 -> Bool
is_eq : U16, U16 -> Bool
is_gt : U16, U16 -> Bool
@ -199,6 +202,7 @@ Builtin :: [].{
}
I16 :: [].{
to_str : I16 -> Str
is_zero : I16 -> Bool
is_negative : I16 -> Bool
is_positive : I16 -> Bool
@ -221,6 +225,7 @@ Builtin :: [].{
}
U32 :: [].{
to_str : U32 -> Str
is_zero : U32 -> Bool
is_eq : U32, U32 -> Bool
is_gt : U32, U32 -> Bool
@ -240,6 +245,7 @@ Builtin :: [].{
}
I32 :: [].{
to_str : I32 -> Str
is_zero : I32 -> Bool
is_negative : I32 -> Bool
is_positive : I32 -> Bool
@ -262,6 +268,7 @@ Builtin :: [].{
}
U64 :: [].{
to_str : U64 -> Str
is_zero : U64 -> Bool
is_eq : U64, U64 -> Bool
is_gt : U64, U64 -> Bool
@ -281,6 +288,7 @@ Builtin :: [].{
}
I64 :: [].{
to_str : I64 -> Str
is_zero : I64 -> Bool
is_negative : I64 -> Bool
is_positive : I64 -> Bool
@ -303,6 +311,7 @@ Builtin :: [].{
}
U128 :: [].{
to_str : U128 -> Str
is_zero : U128 -> Bool
is_eq : U128, U128 -> Bool
is_gt : U128, U128 -> Bool
@ -322,6 +331,7 @@ Builtin :: [].{
}
I128 :: [].{
to_str : I128 -> Str
is_zero : I128 -> Bool
is_negative : I128 -> Bool
is_positive : I128 -> Bool
@ -369,6 +379,7 @@ Builtin :: [].{
}
F32 :: [].{
to_str : F32 -> Str
is_zero : F32 -> Bool
is_negative : F32 -> Bool
is_positive : F32 -> Bool
@ -391,6 +402,7 @@ Builtin :: [].{
}
F64 :: [].{
to_str : F64 -> Str
is_zero : F64 -> Bool
is_negative : F64 -> Bool
is_positive : F64 -> Bool

View file

@ -402,8 +402,20 @@ pub const Expr = union(enum) {
// String operations
str_is_empty,
// Dec operations
// Numeric to_str operations
u8_to_str,
i8_to_str,
u16_to_str,
i16_to_str,
u32_to_str,
i32_to_str,
u64_to_str,
i64_to_str,
u128_to_str,
i128_to_str,
dec_to_str,
f32_to_str,
f64_to_str,
// List operations
list_len,

View file

@ -4034,6 +4034,18 @@ pub const Interpreter = struct {
roc_str_ptr.* = result_str;
return value;
},
.u8_to_str => return self.intToStr(u8, args, roc_ops),
.i8_to_str => return self.intToStr(i8, args, roc_ops),
.u16_to_str => return self.intToStr(u16, args, roc_ops),
.i16_to_str => return self.intToStr(i16, args, roc_ops),
.u32_to_str => return self.intToStr(u32, args, roc_ops),
.i32_to_str => return self.intToStr(i32, args, roc_ops),
.u64_to_str => return self.intToStr(u64, args, roc_ops),
.i64_to_str => return self.intToStr(i64, args, roc_ops),
.u128_to_str => return self.intToStr(u128, args, roc_ops),
.i128_to_str => return self.intToStr(i128, args, roc_ops),
.f32_to_str => return self.floatToStr(f32, args, roc_ops),
.f64_to_str => return self.floatToStr(f64, args, roc_ops),
}
}
@ -4049,6 +4061,48 @@ pub const Interpreter = struct {
return bool_value;
}
/// Helper for integer to_str operations
fn intToStr(self: *Interpreter, comptime T: type, args: []const StackValue, roc_ops: *RocOps) !StackValue {
std.debug.assert(args.len == 1);
const int_arg = args[0];
if (int_arg.ptr == null) {
self.triggerCrash("int_to_str: null argument", false, roc_ops);
return error.Crash;
}
const int_value: T = @as(*const T, @ptrCast(@alignCast(int_arg.ptr.?))).*;
// Use std.fmt to format the integer
var buf: [40]u8 = undefined; // 40 is enough for i128
const result = std.fmt.bufPrint(&buf, "{}", .{int_value}) catch unreachable;
const value = try self.pushStr("");
const roc_str_ptr: *RocStr = @ptrCast(@alignCast(value.ptr.?));
roc_str_ptr.* = RocStr.init(&buf, result.len, roc_ops);
return value;
}
/// Helper for float to_str operations
fn floatToStr(self: *Interpreter, comptime T: type, args: []const StackValue, roc_ops: *RocOps) !StackValue {
std.debug.assert(args.len == 1);
const float_arg = args[0];
if (float_arg.ptr == null) {
self.triggerCrash("float_to_str: null argument", false, roc_ops);
return error.Crash;
}
const float_value: T = @as(*const T, @ptrCast(@alignCast(float_arg.ptr.?))).*;
// Use std.fmt to format the float
var buf: [400]u8 = undefined;
const result = std.fmt.bufPrint(&buf, "{d}", .{float_value}) catch unreachable;
const value = try self.pushStr("");
const roc_str_ptr: *RocStr = @ptrCast(@alignCast(value.ptr.?));
roc_str_ptr.* = RocStr.init(&buf, result.len, roc_ops);
return value;
}
fn triggerCrash(self: *Interpreter, message: []const u8, owned: bool, roc_ops: *RocOps) void {
defer if (owned) self.allocator.free(@constCast(message));
roc_ops.crash(message);

View file

@ -327,3 +327,163 @@ test "e_low_level_lambda - Dec.to_str with zero" {
defer test_allocator.free(value);
try testing.expectEqualStrings("\"0.0\"", value);
}
// Integer to_str tests
test "e_low_level_lambda - U8.to_str" {
const src =
\\a : U8
\\a = 42u8
\\x = U8.to_str(a)
;
const value = try evalModuleAndGetString(src, 1, test_allocator);
defer test_allocator.free(value);
try testing.expectEqualStrings("\"42\"", value);
}
test "e_low_level_lambda - I8.to_str with negative" {
const src =
\\a : I8
\\a = -42i8
\\x = I8.to_str(a)
;
const value = try evalModuleAndGetString(src, 1, test_allocator);
defer test_allocator.free(value);
try testing.expectEqualStrings("\"-42\"", value);
}
test "e_low_level_lambda - U16.to_str" {
const src =
\\a : U16
\\a = 1000u16
\\x = U16.to_str(a)
;
const value = try evalModuleAndGetString(src, 1, test_allocator);
defer test_allocator.free(value);
try testing.expectEqualStrings("\"1000\"", value);
}
test "e_low_level_lambda - I16.to_str with negative" {
const src =
\\a : I16
\\a = -500i16
\\x = I16.to_str(a)
;
const value = try evalModuleAndGetString(src, 1, test_allocator);
defer test_allocator.free(value);
try testing.expectEqualStrings("\"-500\"", value);
}
test "e_low_level_lambda - U32.to_str" {
const src =
\\a : U32
\\a = 100000u32
\\x = U32.to_str(a)
;
const value = try evalModuleAndGetString(src, 1, test_allocator);
defer test_allocator.free(value);
try testing.expectEqualStrings("\"100000\"", value);
}
test "e_low_level_lambda - I32.to_str with negative" {
const src =
\\a : I32
\\a = -12345i32
\\x = I32.to_str(a)
;
const value = try evalModuleAndGetString(src, 1, test_allocator);
defer test_allocator.free(value);
try testing.expectEqualStrings("\"-12345\"", value);
}
test "e_low_level_lambda - U64.to_str" {
const src =
\\a : U64
\\a = 9876543210u64
\\x = U64.to_str(a)
;
const value = try evalModuleAndGetString(src, 1, test_allocator);
defer test_allocator.free(value);
try testing.expectEqualStrings("\"9876543210\"", value);
}
test "e_low_level_lambda - I64.to_str with negative" {
const src =
\\a : I64
\\a = -9876543210i64
\\x = I64.to_str(a)
;
const value = try evalModuleAndGetString(src, 1, test_allocator);
defer test_allocator.free(value);
try testing.expectEqualStrings("\"-9876543210\"", value);
}
test "e_low_level_lambda - U128.to_str" {
const src =
\\a : U128
\\a = 12345678901234567890u128
\\x = U128.to_str(a)
;
const value = try evalModuleAndGetString(src, 1, test_allocator);
defer test_allocator.free(value);
try testing.expectEqualStrings("\"12345678901234567890\"", value);
}
test "e_low_level_lambda - I128.to_str with negative" {
const src =
\\a : I128
\\a = -12345678901234567890i128
\\x = I128.to_str(a)
;
const value = try evalModuleAndGetString(src, 1, test_allocator);
defer test_allocator.free(value);
try testing.expectEqualStrings("\"-12345678901234567890\"", value);
}
// Float to_str tests
test "e_low_level_lambda - F32.to_str" {
const src =
\\a : F32
\\a = 3.14f32
\\x = F32.to_str(a)
;
const value = try evalModuleAndGetString(src, 1, test_allocator);
defer test_allocator.free(value);
// F32 has limited precision, so we just check it starts correctly
try testing.expect(std.mem.startsWith(u8, value, "\"3.14"));
}
test "e_low_level_lambda - F64.to_str" {
const src =
\\a : F64
\\a = 3.14159265359f64
\\x = F64.to_str(a)
;
const value = try evalModuleAndGetString(src, 1, test_allocator);
defer test_allocator.free(value);
// F64 has more precision than F32
try testing.expect(std.mem.startsWith(u8, value, "\"3.141592"));
}
test "e_low_level_lambda - F32.to_str with negative" {
const src =
\\a : F32
\\a = -2.5f32
\\x = F32.to_str(a)
;
const value = try evalModuleAndGetString(src, 1, test_allocator);
defer test_allocator.free(value);
try testing.expect(std.mem.startsWith(u8, value, "\"-2.5"));
}
test "e_low_level_lambda - F64.to_str with negative" {
const src =
\\a : F64
\\a = -123.456f64
\\x = F64.to_str(a)
;
const value = try evalModuleAndGetString(src, 1, test_allocator);
defer test_allocator.free(value);
try testing.expect(std.mem.startsWith(u8, value, "\"-123.456"));
}