diff --git a/src/build/builtin_compiler/main.zig b/src/build/builtin_compiler/main.zig index 9927e97db1..4c1a81d02b 100644 --- a/src/build/builtin_compiler/main.zig +++ b/src/build/builtin_compiler/main.zig @@ -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); + } } } diff --git a/src/build/roc/Builtin.roc b/src/build/roc/Builtin.roc index 4ef43ec02a..104fa1035d 100644 --- a/src/build/roc/Builtin.roc +++ b/src/build/roc/Builtin.roc @@ -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 diff --git a/src/canonicalize/Expression.zig b/src/canonicalize/Expression.zig index be168e806f..08e4cabd28 100644 --- a/src/canonicalize/Expression.zig +++ b/src/canonicalize/Expression.zig @@ -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, diff --git a/src/eval/interpreter.zig b/src/eval/interpreter.zig index 7619dca653..80525cb5f1 100644 --- a/src/eval/interpreter.zig +++ b/src/eval/interpreter.zig @@ -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); diff --git a/src/eval/test/low_level_interp_test.zig b/src/eval/test/low_level_interp_test.zig index 8ca2dacff4..1f196973f9 100644 --- a/src/eval/test/low_level_interp_test.zig +++ b/src/eval/test/low_level_interp_test.zig @@ -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")); +}