Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
76 commits
Select commit Hold shift + click to select a range
221ef89
Add desugaring for minus
rtfeldman Nov 10, 2025
64f2657
Desugar other arithmetic operators
rtfeldman Nov 10, 2025
63aa24d
Improve a test
rtfeldman Nov 10, 2025
924727f
Fix some operator precedence
rtfeldman Nov 10, 2025
843f150
Consolidate some operator string stuff
rtfeldman Nov 10, 2025
cdeef94
Use a hash map for operator names
rtfeldman Nov 10, 2025
c56d31d
Update a comment
rtfeldman Nov 10, 2025
1279c22
rem and pow
rtfeldman Nov 10, 2025
6b76350
Fix size expectation
rtfeldman Nov 10, 2025
4fbfe53
s/div_by/div
rtfeldman Nov 10, 2025
dc64579
Fix some debug stuff for wasm
rtfeldman Nov 10, 2025
32800bf
Improve unification for numbers
rtfeldman Nov 10, 2025
aec008d
Remove unnecessary addVarToRank
rtfeldman Nov 10, 2025
4b9c2ee
Add a test
rtfeldman Nov 11, 2025
7525844
Merge remote-tracking branch 'origin/main' into desugar-arithmetic
rtfeldman Nov 11, 2025
39a70c8
Use desc in panic
rtfeldman Nov 11, 2025
afb4fc4
Eliminate Num
rtfeldman Nov 11, 2025
e1d3cc7
Fix placeholder behavior
rtfeldman Nov 11, 2025
18b90c4
Fix a type checker bug
rtfeldman Nov 12, 2025
10914d9
Add some fallbacks
rtfeldman Nov 12, 2025
4f50c91
panic with a descriptive error on bug
rtfeldman Nov 12, 2025
ec0d249
Introduce more debug panicking
rtfeldman Nov 12, 2025
285216e
Reproduce type-checking bug
rtfeldman Nov 12, 2025
b40e647
describe bug and fix
rtfeldman Nov 12, 2025
ad1aca3
Fix literals
rtfeldman Nov 12, 2025
317d4b5
Fix more type checking stuff
rtfeldman Nov 12, 2025
1e7d58a
More type checking fixes
rtfeldman Nov 12, 2025
5ea1338
Unskip some tests
rtfeldman Nov 12, 2025
4e65b87
Fix infinite recursion
rtfeldman Nov 12, 2025
7515cbf
More type checker fixes
rtfeldman Nov 12, 2025
3f04beb
In a surprise twist, more type-checking fixes
rtfeldman Nov 12, 2025
808b582
Update .rules
rtfeldman Nov 12, 2025
c541f43
Allow builtin shadowing in Builtin.roc
rtfeldman Nov 13, 2025
18d165a
Update type printing for numbers
rtfeldman Nov 13, 2025
501d373
Re-skip some tests that origin/main skips
rtfeldman Nov 13, 2025
bb94a11
Update tests
rtfeldman Nov 13, 2025
3e984fb
wip
rtfeldman Nov 13, 2025
2f54fb8
Revise builtin stuff more
rtfeldman Nov 13, 2025
0f39cab
Remove evalArithmeticBinop
rtfeldman Nov 13, 2025
29ce2a6
Fix desugaring bug
rtfeldman Nov 13, 2025
84aee27
Revert "Fix desugaring bug"
rtfeldman Nov 13, 2025
205d7d9
Remove incorrect special-casing
rtfeldman Nov 13, 2025
8c25ade
Fix a constraint bug
rtfeldman Nov 13, 2025
4b3ecb7
Revert "Fix a constraint bug"
rtfeldman Nov 14, 2025
2ddd072
Partial fix
rtfeldman Nov 14, 2025
58ef395
Revert "Partial fix"
rtfeldman Nov 14, 2025
928f7a9
Skip some tests
rtfeldman Nov 14, 2025
cbf6f29
Update snapshots
rtfeldman Nov 14, 2025
e498d1e
Merge remote-tracking branch 'origin/main' into desugar-arithmetic
rtfeldman Nov 14, 2025
4014e31
Merge remote-tracking branch 'origin/main' into desugar-arithmetic
rtfeldman Nov 14, 2025
60791cf
Fix missing .list unification
rtfeldman Nov 14, 2025
06f4207
Clean up stuff
rtfeldman Nov 14, 2025
ddd7c7e
Add a new test
rtfeldman Nov 14, 2025
2984b54
Add .num_unbound_if_builtin
rtfeldman Nov 14, 2025
41e308d
Fix eval for .plus
rtfeldman Nov 14, 2025
bdbebab
Fix `+` desugaring
rtfeldman Nov 15, 2025
cfab46f
Update some tests
rtfeldman Nov 15, 2025
51658cf
Clean up some type checking tests
rtfeldman Nov 15, 2025
7adb4fd
wip trying stuff
rtfeldman Nov 15, 2025
2ff418a
Get number tests passing
rtfeldman Nov 15, 2025
ce4e14f
Delete obsolete test
rtfeldman Nov 15, 2025
18afe73
Get rid of the runtime type store
rtfeldman Nov 15, 2025
3b6551b
Wire in Builtin.roc into more tests
rtfeldman Nov 15, 2025
4761d4e
Don't skip low level tests anymore
rtfeldman Nov 15, 2025
37e09b4
wip
rtfeldman Nov 15, 2025
0b86661
Remove int_unbound
rtfeldman Nov 15, 2025
cc6d869
Fix some tests
rtfeldman Nov 15, 2025
f32a6f6
Fix a unification regression
rtfeldman Nov 16, 2025
398d71b
wip
rtfeldman Nov 16, 2025
8321038
Fix some constraint unification
rtfeldman Nov 16, 2025
fbf61f8
Before eliminating frac_unbound
rtfeldman Nov 16, 2025
6d5972a
Remove frac_unbound and frac_poly
rtfeldman Nov 16, 2025
97b36eb
Eliminate the *_poly numeric primitives
rtfeldman Nov 16, 2025
e077179
Fix some tests
rtfeldman Nov 16, 2025
5f1c48b
Remove "Num" from some tests
rtfeldman Nov 16, 2025
131b4b8
Fix missing from_dec_digits constraints
rtfeldman Nov 16, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
10 changes: 9 additions & 1 deletion .rules
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@ When writing Roc code in tests or examples, remember these syntax rules:

- **Naming Convention**: Roc uses `snake_case` for identifiers, not `camelCase`
- **Boolean Operators**: Use `and` and `or` keywords, not `&&` or `||`
- **Lambda Syntax**: Anonymous functions use bars: `my_fn = |arg1, arg2| other_fn(arg1, arg2)`
- **Function Syntax**: Functions in Roc are called with parentheses and commas, not whitespace: `my_fn = |arg1, arg2| other_fn(arg1, arg2)` NOT `my_fn = \arg1, arg2 -> other_fn arg1 arg2`
- **Lambda Syntax**: Functions in Roc are written with pipes (never backslashes) and are called with parentheses and commas (never whitespace). This is valid: `my_fn = |arg1, arg2| other_fn(arg1, arg2)` and this is NOT valid: `my_fn = \arg1, arg2 -> other_fn arg1 arg2`
- **If Expressions**: The syntax is `if condition then_branch else else_branch` with no `then` keyword
- **Blocks**: Use curly braces to define inline variables. The last expression in the block is its return value:
```
Expand All @@ -92,3 +93,10 @@ When writing Roc code in tests or examples, remember these syntax rules:
answer
}
```

## Development Priorities

1. **No std.debug.print or std.debug.panic in production** - This code base compiles to WebAssembly. That means if you ever leave std.debug.print or std.debug.panic calls in production code (both are fine in tests), CI will fail on the WebAssembly build. It's fine to use these for debugging, but if you want a production panic, use `@panic`, and if you want to write to stderr in production, use a normal stderr writer.
1. **No excuses** - It is always incorrect to think "this is a pre-existing test failure, so I don't need to address it." All projects in this repository begin with completely passing tests, and this is strictly enforced by CI. If there is a test failure, you have not completed your task until it has been addressed. No excuses!
2. **Ignore pass rate** - Test pass rate is something you should ignore completely. It doesn't matter how many tests are passing or how many are failing. All that matters is the binary "are all tests passing?" If you make a change and more tests pass than before, that does NOT necessarily mean you made progress. Likewise, if you make a change and fewer tests pass than before, that does NOT necessarily mean you regressed progress. You need to look at feedback like compiler output, debug logging, and individual test failure messages to determine whether you are making progress or not; completely disregard the number of passing or failing tests as a measure of incremental progress. The only way the number of failing tests is relevant is that you know you are not done with the project until the number of failures has reached zero.
3. **No hacks, workarounds, placeholders, or fallbacks** - This is a production code base with an emphasis on high quality. Never give up and write code like "// For now, ..." or which silently swallows errors, or which uses fallbacks like "// If that didn't work, try..."
29 changes: 0 additions & 29 deletions ci/check_test_wiring.zig
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,6 @@ const test_exclusions = [_][]const u8{
"src/snapshot_tool",
};

const test_file_exclusions = [_][]const u8{
// TODO: This test got out of sync and is not straightforward to fix
"src/eval/test/low_level_interp_test.zig",
};

const TermColor = struct {
pub const red = "\x1b[0;31m";
pub const green = "\x1b[0;32m";
Expand Down Expand Up @@ -155,16 +150,6 @@ fn handleFile(
return;
}

if (shouldSkipTestPath(path)) {
allocator.free(path);
return;
}

if (shouldSkipTestFile(path)) {
allocator.free(path);
return;
}

if (try fileHasTestDecl(allocator, path)) {
try test_files.append(allocator, path);
return;
Expand All @@ -173,20 +158,6 @@ fn handleFile(
allocator.free(path);
}

fn shouldSkipTestPath(path: []const u8) bool {
for (test_exclusions) |prefix| {
if (hasDirPrefix(path, prefix)) return true;
}
return false;
}

fn shouldSkipTestFile(path: []const u8) bool {
for (test_file_exclusions) |excluded| {
if (std.mem.eql(u8, path, excluded)) return true;
}
return false;
}

fn hasDirPrefix(path: []const u8, prefix: []const u8) bool {
if (!std.mem.startsWith(u8, path, prefix)) return false;
return path.len == prefix.len or path[prefix.len] == '/';
Expand Down
10 changes: 10 additions & 0 deletions src/base/Ident.zig
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,16 @@ pub const FROM_INT_DIGITS_METHOD_NAME = "from_int_digits";
pub const FROM_DEC_DIGITS_METHOD_NAME = "from_dec_digits";
/// Method name for addition - used by + operator desugaring
pub const PLUS_METHOD_NAME = "plus";
/// Method name for subtraction - used by - operator desugaring
pub const MINUS_METHOD_NAME = "minus";
/// Method name for multiplication - used by * operator desugaring
pub const TIMES_METHOD_NAME = "times";
/// Method name for division - used by / operator desugaring
pub const DIV_METHOD_NAME = "div";
/// Method name for truncating division - used by // operator desugaring
pub const DIV_TRUNC_METHOD_NAME = "div_trunc";
/// Method name for remainder - used by % operator desugaring
pub const REM_METHOD_NAME = "rem";

/// The original text of the identifier.
raw_text: []const u8,
Expand Down
165 changes: 152 additions & 13 deletions src/build/builtin_compiler/main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,81 @@ fn transformStrNominalToPrimitive(env: *ModuleEnv) !void {
}
}

/// Transform all numeric nominal types (U8, I8, ..., I128, Dec, F32, F64) to their compact Num representations.
/// This ensures that numeric types in method signatures use num_compact instead of staying as nominal types,
/// which is necessary for proper unification and serialization.
fn transformNumericNominalToPrimitive(env: *ModuleEnv) !void {
const Num = types.Num;

// Map of numeric type names to their compact representations
const NumericTypeMapping = struct {
name: []const u8,
num_type: Num,
};

const numeric_mappings = [_]NumericTypeMapping{
.{ .name = "U8", .num_type = .{ .num_compact = .{ .int = .u8 } } },
.{ .name = "I8", .num_type = .{ .num_compact = .{ .int = .i8 } } },
.{ .name = "U16", .num_type = .{ .num_compact = .{ .int = .u16 } } },
.{ .name = "I16", .num_type = .{ .num_compact = .{ .int = .i16 } } },
.{ .name = "U32", .num_type = .{ .num_compact = .{ .int = .u32 } } },
.{ .name = "I32", .num_type = .{ .num_compact = .{ .int = .i32 } } },
.{ .name = "U64", .num_type = .{ .num_compact = .{ .int = .u64 } } },
.{ .name = "I64", .num_type = .{ .num_compact = .{ .int = .i64 } } },
.{ .name = "U128", .num_type = .{ .num_compact = .{ .int = .u128 } } },
.{ .name = "I128", .num_type = .{ .num_compact = .{ .int = .i128 } } },
.{ .name = "Dec", .num_type = .{ .num_compact = .{ .frac = .dec } } },
.{ .name = "F32", .num_type = .{ .num_compact = .{ .frac = .f32 } } },
.{ .name = "F64", .num_type = .{ .num_compact = .{ .frac = .f64 } } },
};

// Build a map of identifier -> num type for fast lookup
var ident_to_num_map = std.AutoHashMap(base.Ident.Idx, Num).init(env.gpa);
defer ident_to_num_map.deinit();

for (numeric_mappings) |mapping| {
if (env.common.findIdent(mapping.name)) |ident| {
try ident_to_num_map.put(ident, mapping.num_type);
}
}

// Iterate through all slots in the type store
for (0..env.types.len()) |i| {
const var_idx = @as(Var, @enumFromInt(i));

// Skip redirects, only process roots
if (!env.types.resolveVar(var_idx).is_root) {
continue;
}

const resolved = env.types.resolveVar(var_idx);
const desc = resolved.desc;

// Check if this descriptor contains a nominal type
switch (desc.content) {
.structure => |structure| {
switch (structure) {
.nominal_type => |nominal| {
// Check if this is a numeric nominal type
if (ident_to_num_map.get(nominal.ident.ident_idx)) |num_type| {
// Replace with compact num type
const new_content = Content{ .structure = .{ .num = num_type } };
const new_desc = types.Descriptor{
.content = new_content,
.rank = desc.rank,
.mark = desc.mark,
};
try env.types.setVarDesc(var_idx, new_desc);
}
},
else => {},
}
},
else => {},
}
}
}

/// Replace specific e_anno_only expressions with e_low_level_lambda operations.
/// This transforms standalone annotations into low-level builtin lambda operations
/// that will be recognized by the compiler backend.
Expand Down Expand Up @@ -270,7 +345,7 @@ fn replaceStrIsEmptyWithLowLevel(env: *ModuleEnv) !std.ArrayList(CIR.Def.Idx) {
}
}

// Numeric arithmetic operations (all numeric types have plus, minus, times, div_by, rem_by)
// Numeric arithmetic operations (all numeric types have plus, minus, times, div, rem)
for (numeric_types) |num_type| {
var buf: [256]u8 = undefined;

Expand All @@ -292,15 +367,15 @@ fn replaceStrIsEmptyWithLowLevel(env: *ModuleEnv) !std.ArrayList(CIR.Def.Idx) {
try low_level_map.put(ident, .num_times);
}

// div_by
const div_by = try std.fmt.bufPrint(&buf, "Builtin.Num.{s}.div_by", .{num_type});
if (env.common.findIdent(div_by)) |ident| {
// div
const div = try std.fmt.bufPrint(&buf, "Builtin.Num.{s}.div", .{num_type});
if (env.common.findIdent(div)) |ident| {
try low_level_map.put(ident, .num_div_by);
}

// rem_by
const rem_by = try std.fmt.bufPrint(&buf, "Builtin.Num.{s}.rem_by", .{num_type});
if (env.common.findIdent(rem_by)) |ident| {
// rem
const rem = try std.fmt.bufPrint(&buf, "Builtin.Num.{s}.rem", .{num_type});
if (env.common.findIdent(rem)) |ident| {
try low_level_map.put(ident, .num_rem_by);
}
}
Expand All @@ -316,22 +391,57 @@ fn replaceStrIsEmptyWithLowLevel(env: *ModuleEnv) !std.ArrayList(CIR.Def.Idx) {
}
}

// DEBUG: Show what we're looking for
std.debug.print("\n=== DEBUG: Builtin Compiler Method Transformation ===\n", .{});
std.debug.print("Looking for {d} low-level operations to transform:\n", .{low_level_map.count()});
{
var iter = low_level_map.iterator();
var count: usize = 0;
while (iter.next()) |entry| {
const ident_text = env.getIdentText(entry.key_ptr.*);
const op_name = @tagName(entry.value_ptr.*);
if (count < 10 or std.mem.indexOf(u8, ident_text, "I128") != null) {
std.debug.print(" - {s} -> .{s}\n", .{ ident_text, op_name });
}
count += 1;
}
if (count > 10) {
std.debug.print(" ... and {d} more\n", .{count - 10});
}
}
std.debug.print("\n", .{});

// Iterate through all defs and replace matching anno-only defs with low-level implementations
const all_defs = env.store.sliceDefs(env.all_defs);
std.debug.print("Examining {d} total defs for .e_anno_only expressions...\n", .{all_defs.len});

var anno_only_count: usize = 0;
var matched_count: usize = 0;

for (all_defs) |def_idx| {
const def = env.store.getDef(def_idx);
const expr = env.store.getExpr(def.expr);

// Check if this is an anno-only def (e_anno_only expression)
if (expr == .e_anno_only and def.annotation != null) {
anno_only_count += 1;

// Get the identifier from the pattern
const pattern = env.store.getPattern(def.pattern);
if (pattern == .assign) {
const ident = pattern.assign.ident;
const ident_text = env.getIdentText(ident);

// DEBUG: Show first few anno_only defs and all I128 ones
if (anno_only_count <= 5 or std.mem.indexOf(u8, ident_text, "I128") != null) {
std.debug.print(" Found .e_anno_only def: {s}\n", .{ident_text});
}

// Check if this identifier matches a low-level operation
if (low_level_map.fetchRemove(ident)) |entry| {
const low_level_op = entry.value;
matched_count += 1;
std.debug.print(" -> MATCHED! Transforming to .{s}\n", .{@tagName(low_level_op)});

// Create a dummy parameter pattern for the lambda
// Use the identifier "_arg" for the parameter
Expand Down Expand Up @@ -359,12 +469,28 @@ fn replaceStrIsEmptyWithLowLevel(env: *ModuleEnv) !std.ArrayList(CIR.Def.Idx) {
} }, base.Region.zero());

// Now replace the e_anno_only expression with the e_low_level_lambda
// We need to modify the def's expr field to point to our new expression
// CIR.Def.Idx and Node.Idx have the same underlying representation
// The def stores its fields in extra_data:
// extra_data[0] = pattern
// extra_data[1] = expr <- This is what we need to modify!
// extra_data[2..3] = kind
// extra_data[4] = annotation
const def_node_idx = @as(@TypeOf(env.store.nodes).Idx, @enumFromInt(@intFromEnum(def_idx)));
var def_node = env.store.nodes.get(def_node_idx);
def_node.data_2 = @intFromEnum(expr_idx);
env.store.nodes.set(def_node_idx, def_node);
const def_node = env.store.nodes.get(def_node_idx);
const extra_start = def_node.data_1;
// Modify the expr field (at offset 1 from extra_start)
env.store.extra_data.items.items[extra_start + 1] = @intFromEnum(expr_idx);

// DEBUG STEP 1: Verify the transformation actually happened
if (std.mem.indexOf(u8, ident_text, "I128.plus") != null) {
const updated_def = env.store.getDef(def_idx);
const updated_expr = env.store.getExpr(updated_def.expr);
std.debug.print(" [STEP 1 VERIFY] After transformation, expr tag for {s}: {s}\n", .{ ident_text, @tagName(updated_expr) });
if (updated_expr == .e_low_level_lambda) {
std.debug.print(" [STEP 1 VERIFY] ✓ Transformation succeeded in memory!\n", .{});
} else {
std.debug.print(" [STEP 1 VERIFY] ✗ PROBLEM: Still {s}, transformation failed!\n", .{@tagName(updated_expr)});
}
}

// Track this replaced def index
try new_def_indices.append(gpa, def_idx);
Expand All @@ -373,6 +499,13 @@ fn replaceStrIsEmptyWithLowLevel(env: *ModuleEnv) !std.ArrayList(CIR.Def.Idx) {
}
}

// DEBUG: Show summary
std.debug.print("\n=== Transformation Summary ===\n", .{});
std.debug.print("Total .e_anno_only defs found: {d}\n", .{anno_only_count});
std.debug.print("Successfully transformed: {d}\n", .{matched_count});
std.debug.print("Operations still not found: {d}\n", .{low_level_map.count()});
std.debug.print("==========================================\n\n", .{});

// Verify all low-level operations were found in the builtins
if (low_level_map.count() > 0) {
std.debug.print("\n" ++ "=" ** 80 ++ "\n", .{});
Expand Down Expand Up @@ -581,9 +714,10 @@ pub fn main() !void {

// Transform nominal types to primitive types as necessary.
// This must happen BEFORE serialization to ensure the .bin file contains
// methods associated with the .str primitive, not a nominal type
// methods associated with primitives/compact types, not nominal types
try transformStrNominalToPrimitive(builtin_env);
try transformListNominalToPrimitive(builtin_env);
try transformNumericNominalToPrimitive(builtin_env);

// Create output directory
try std.fs.cwd().makePath("zig-out/builtins");
Expand Down Expand Up @@ -729,6 +863,11 @@ fn compileModule(
const nested = module_env.getIdentText(d.nested_name);
std.debug.print(" - Nested value not found: {s}.{s}\n", .{ parent, nested });
},
.malformed_type_annotation => |d| {
const region = d.region;
const text = source[region.start.offset..region.end.offset];
std.debug.print(" - Malformed type annotation at offset {d}-{d}: '{s}'\n", .{ region.start.offset, region.end.offset, text });
},
else => {
std.debug.print(" - Diagnostic: {any}\n", .{diag});
},
Expand Down
Loading
Loading