Skip to content

Commit 7f0ce09

Browse files
authored
Merge pull request #8280 from roc-lang/bool-builtin
Add initial Bool.roc and Result.roc builtin modules
2 parents 045ca9b + 135a9e7 commit 7f0ce09

File tree

24 files changed

+189
-69
lines changed

24 files changed

+189
-69
lines changed

build.zig

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,19 @@ pub fn build(b: *std.Build) void {
2222
const playground_test_step = b.step("test-playground", "Build the integration test suite for the WASM playground");
2323

2424
// general configuration
25-
const target = b.standardTargetOptions(.{ .default_target = .{
26-
.abi = if (builtin.target.os.tag == .linux) .musl else null,
27-
} });
25+
const target = blk: {
26+
var default_target_query: std.Target.Query = .{
27+
.abi = if (builtin.target.os.tag == .linux) .musl else null,
28+
};
29+
30+
// Use baseline x86_64 CPU for Valgrind compatibility on CI (Valgrind 3.18.1 doesn't support AVX-512)
31+
const is_ci = std.process.getEnvVarOwned(b.allocator, "CI") catch null;
32+
if (is_ci != null and builtin.target.cpu.arch == .x86_64 and builtin.target.os.tag == .linux) {
33+
default_target_query.cpu_model = .{ .explicit = &std.Target.x86.cpu.x86_64 };
34+
}
35+
36+
break :blk b.standardTargetOptions(.{ .default_target = default_target_query });
37+
};
2838
const optimize = b.standardOptimizeOption(.{});
2939
const strip = b.option(bool, "strip", "Omit debug information");
3040
const no_bin = b.option(bool, "no-bin", "Skip emitting binaries (important for fast incremental compilation)") orelse false;
@@ -91,7 +101,7 @@ pub fn build(b: *std.Build) void {
91101
//
92102
// We cache the builtin compiler executable to avoid ~doubling normal build times.
93103
// CI always rebuilds from scratch, so it's not affected by this caching.
94-
//
104+
95105
// Discover all .roc files in src/build/roc/
96106
const roc_files = discoverBuiltinRocFiles(b) catch |err| {
97107
std.debug.print("Failed to discover builtin .roc files: {}\n", .{err});

src/build/builtin_compiler/main.zig

Lines changed: 57 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
//! Build-time compiler for Roc builtin modules (Dict.roc and Set.roc).
1+
//! Build-time compiler for Roc builtin modules (Bool.roc, Result.roc, Dict.roc, and Set.roc).
22
//!
33
//! This executable runs during `zig build` on the host machine to:
4-
//! 1. Parse and type-check Dict.roc and Set.roc
4+
//! 1. Parse and type-check the builtin .roc modules
55
//! 2. Serialize the resulting ModuleEnvs to binary files
66
//! 3. Output .bin files to zig-out/builtins/ (which get embedded in the roc binary)
77

@@ -21,6 +21,9 @@ const Allocator = std.mem.Allocator;
2121
/// Build-time compiler that compiles builtin .roc sources into serialized ModuleEnvs.
2222
/// This runs during `zig build` on the host machine to generate .bin files
2323
/// that get embedded into the final roc executable.
24+
///
25+
/// Note: Command-line arguments are ignored. The .roc files are read from fixed paths.
26+
/// The build system may pass file paths as arguments for cache tracking, but we don't use them.
2427
pub fn main() !void {
2528
var gpa_impl = std.heap.GeneralPurposeAllocator(.{}){};
2629
defer {
@@ -31,33 +34,78 @@ pub fn main() !void {
3134
}
3235
const gpa = gpa_impl.allocator();
3336

37+
// Ignore command-line arguments - they're only used by Zig's build system for cache tracking
38+
3439
// Read the .roc source files at runtime
40+
const bool_roc_source = try std.fs.cwd().readFileAlloc(gpa, "src/build/roc/Bool.roc", 1024 * 1024);
41+
defer gpa.free(bool_roc_source);
42+
43+
const result_roc_source = try std.fs.cwd().readFileAlloc(gpa, "src/build/roc/Result.roc", 1024 * 1024);
44+
defer gpa.free(result_roc_source);
45+
3546
const dict_roc_source = try std.fs.cwd().readFileAlloc(gpa, "src/build/roc/Dict.roc", 1024 * 1024);
3647
defer gpa.free(dict_roc_source);
3748

3849
const set_roc_source = try std.fs.cwd().readFileAlloc(gpa, "src/build/roc/Set.roc", 1024 * 1024);
3950
defer gpa.free(set_roc_source);
4051

41-
// Compile Dict.roc first (it has no dependencies)
52+
// Compile Bool.roc without injecting anything (it's completely self-contained)
53+
const bool_env = try compileModule(
54+
gpa,
55+
"Bool",
56+
bool_roc_source,
57+
&.{}, // No module dependencies
58+
.{ .inject_bool = false, .inject_result = false },
59+
);
60+
defer {
61+
bool_env.deinit();
62+
gpa.destroy(bool_env);
63+
}
64+
65+
// Verify that Bool's type declaration is at the expected index (2)
66+
// This is critical for the compiler's hardcoded BUILTIN_BOOL constant
67+
const bool_type_idx = bool_env.all_statements.span.start;
68+
if (bool_type_idx != 2) {
69+
const stderr = std.io.getStdErr().writer();
70+
try stderr.print("WARNING: Expected Bool at index 2, but got {}!\n", .{bool_type_idx});
71+
return error.UnexpectedBoolIndex;
72+
}
73+
74+
// Compile Result.roc (injects Bool since Result might use if expressions)
75+
const result_env = try compileModule(
76+
gpa,
77+
"Result",
78+
result_roc_source,
79+
&.{}, // No module dependencies
80+
.{ .inject_bool = true, .inject_result = false },
81+
);
82+
defer {
83+
result_env.deinit();
84+
gpa.destroy(result_env);
85+
}
86+
87+
// Compile Dict.roc (needs Bool injected for if expressions, and Result for error handling)
4288
const dict_env = try compileModule(
4389
gpa,
4490
"Dict",
4591
dict_roc_source,
4692
&.{}, // No module dependencies
93+
.{}, // Inject Bool and Result (defaults)
4794
);
4895
defer {
4996
dict_env.deinit();
5097
gpa.destroy(dict_env);
5198
}
5299

53-
// Compile Set.roc (it imports Dict)
100+
// Compile Set.roc (imports Dict, needs Bool and Result injected)
54101
const set_env = try compileModule(
55102
gpa,
56103
"Set",
57104
set_roc_source,
58105
&[_]ModuleDep{
59106
.{ .name = "Dict", .env = dict_env },
60107
},
108+
.{}, // Inject Bool and Result (defaults)
61109
);
62110
defer {
63111
set_env.deinit();
@@ -67,10 +115,10 @@ pub fn main() !void {
67115
// Create output directory
68116
try std.fs.cwd().makePath("zig-out/builtins");
69117

70-
// Serialize Dict module
118+
// Serialize modules
119+
try serializeModuleEnv(gpa, bool_env, "zig-out/builtins/Bool.bin");
120+
try serializeModuleEnv(gpa, result_env, "zig-out/builtins/Result.bin");
71121
try serializeModuleEnv(gpa, dict_env, "zig-out/builtins/Dict.bin");
72-
73-
// Serialize Set module
74122
try serializeModuleEnv(gpa, set_env, "zig-out/builtins/Set.bin");
75123
}
76124

@@ -84,6 +132,7 @@ fn compileModule(
84132
module_name: []const u8,
85133
source: []const u8,
86134
deps: []const ModuleDep,
135+
can_options: Can.InitOptions,
87136
) !*ModuleEnv {
88137
// This follows the pattern from TestEnv.init() in src/check/test/TestEnv.zig
89138

@@ -149,7 +198,7 @@ fn compileModule(
149198
gpa.destroy(can_result);
150199
}
151200

152-
can_result.* = try Can.init(module_env, parse_ast, &module_envs);
201+
can_result.* = try Can.init(module_env, parse_ast, &module_envs, can_options);
153202

154203
try can_result.canonicalizeFile();
155204
try can_result.validateForChecking();

src/build/roc/Bool.roc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Bool := [True, False].{}

src/build/roc/Result.roc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Result(ok, err) := [Ok(ok), Err(err)].{}

src/canonicalize/Can.zig

Lines changed: 47 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ const RecordField = CIR.RecordField;
133133
const SeenRecordField = struct { ident: base.Ident.Idx, region: base.Region };
134134

135135
/// The idx of the builtin Bool
136-
pub const BUILTIN_BOOL: Statement.Idx = @enumFromInt(4);
136+
pub const BUILTIN_BOOL: Statement.Idx = @enumFromInt(2);
137137
/// The idx of the builtin Result
138138
pub const BUILTIN_RESULT: Statement.Idx = @enumFromInt(13);
139139

@@ -171,10 +171,22 @@ pub fn deinit(
171171
self.scratch_free_vars.deinit(gpa);
172172
}
173173

174+
/// Options for initializing the canonicalizer.
175+
/// Controls which built-in types are injected into the module's scope.
176+
pub const InitOptions = struct {
177+
/// Whether to inject the Bool type declaration (`Bool := [True, False]`).
178+
/// Set to false when compiling Bool.roc itself to avoid duplication.
179+
inject_bool: bool = true,
180+
/// Whether to inject the Result type declaration (`Result(ok, err) := [Ok(ok), Err(err)]`).
181+
/// Set to false when compiling Result.roc itself (if it exists).
182+
inject_result: bool = true,
183+
};
184+
174185
pub fn init(
175186
env: *ModuleEnv,
176187
parse_ir: *AST,
177188
module_envs: ?*const std.StringHashMap(*const ModuleEnv),
189+
options: InitOptions,
178190
) std.mem.Allocator.Error!Self {
179191
const gpa = env.gpa;
180192

@@ -207,16 +219,28 @@ pub fn init(
207219

208220
const scratch_statements_start = result.env.store.scratch_statements.top();
209221

210-
// Simulate the builtins by add type declarations
222+
// Inject built-in type declarations that aren't defined in this module's source
211223
// TODO: These should ultimately come from the platform/builtin files rather than being hardcoded
212-
try result.addBuiltinTypeBool(env);
213-
try result.addBuiltinTypeResult(env);
224+
if (options.inject_bool) {
225+
const bool_idx = try result.addBuiltinTypeBool(env);
226+
try result.env.store.addScratchStatement(bool_idx);
227+
}
228+
if (options.inject_result) {
229+
const result_idx = try result.addBuiltinTypeResult(env);
230+
try result.env.store.addScratchStatement(result_idx);
231+
}
214232

215-
// Add builtins to builtin stmts
216-
try result.env.store.addScratchStatement(BUILTIN_BOOL);
217-
try result.env.store.addScratchStatement(BUILTIN_RESULT);
218233
result.env.builtin_statements = try result.env.store.statementSpanFrom(scratch_statements_start);
219234

235+
// Debug assertion: When Bool is injected, it must be the first builtin statement
236+
if (std.debug.runtime_safety and options.inject_bool) {
237+
const builtin_stmts = result.env.store.sliceStatements(result.env.builtin_statements);
238+
std.debug.assert(builtin_stmts.len >= 1); // Must have at least Bool
239+
// Verify first builtin is Bool by checking it's a nominal_decl
240+
const first_stmt = result.env.store.getStatement(builtin_stmts[0]);
241+
std.debug.assert(first_stmt == .s_nominal_decl);
242+
}
243+
220244
// Assert that the node store is completely empty
221245
env.debugAssertArraysInSync();
222246

@@ -226,7 +250,8 @@ pub fn init(
226250
// builtins //
227251

228252
/// Creates `Bool := [True, False]`
229-
fn addBuiltinTypeBool(self: *Self, ir: *ModuleEnv) std.mem.Allocator.Error!void {
253+
/// Returns the statement index where Bool was created
254+
fn addBuiltinTypeBool(self: *Self, ir: *ModuleEnv) std.mem.Allocator.Error!Statement.Idx {
230255
const gpa = ir.gpa;
231256
const type_ident = try ir.insertIdent(base.Ident.for_text("Bool"));
232257
const true_ident = try ir.insertIdent(base.Ident.for_text("True"));
@@ -268,8 +293,10 @@ fn addBuiltinTypeBool(self: *Self, ir: *ModuleEnv) std.mem.Allocator.Error!void
268293
.s_nominal_decl = .{ .header = header_idx, .anno = tag_union_anno_idx },
269294
}, .err, Region.zero());
270295

271-
// Assert that this is the first stmt in the file
272-
std.debug.assert(type_decl_idx == BUILTIN_BOOL);
296+
// Note: When Bool.roc is compiled without injecting builtins, Bool is at absolute index 2 (BUILTIN_BOOL).
297+
// This is verified at build time by the builtin_compiler.
298+
// When builtins are injected into other modules, Bool is always the FIRST builtin (builtin_statements[0]),
299+
// though its absolute statement index may differ from BUILTIN_BOOL.
273300

274301
// Introduce to scope
275302
const current_scope = &self.scopes.items[self.scopes.items.len - 1];
@@ -279,10 +306,13 @@ fn addBuiltinTypeBool(self: *Self, ir: *ModuleEnv) std.mem.Allocator.Error!void
279306
// TODO: in the future, we should have hardcoded constants for these.
280307
try self.unqualified_nominal_tags.put(gpa, "True", type_decl_idx);
281308
try self.unqualified_nominal_tags.put(gpa, "False", type_decl_idx);
309+
310+
return type_decl_idx;
282311
}
283312

284313
/// Creates `Result(ok, err) := [Ok(ok), Err(err)]`
285-
fn addBuiltinTypeResult(self: *Self, ir: *ModuleEnv) std.mem.Allocator.Error!void {
314+
/// Returns the statement index where Result was created
315+
fn addBuiltinTypeResult(self: *Self, ir: *ModuleEnv) std.mem.Allocator.Error!Statement.Idx {
286316
const gpa = ir.gpa;
287317
const type_ident = try ir.insertIdent(base.Ident.for_text("Result"));
288318
const ok_tag_ident = try ir.insertIdent(base.Ident.for_text("Ok"));
@@ -357,17 +387,19 @@ fn addBuiltinTypeResult(self: *Self, ir: *ModuleEnv) std.mem.Allocator.Error!voi
357387
Region.zero(),
358388
);
359389

360-
// Assert that this is the first stmt in the file
361-
std.debug.assert(type_decl_idx == BUILTIN_RESULT);
390+
// Note: When Result.roc is compiled without injecting builtins, Result ends up at index 13 (BUILTIN_RESULT)
391+
// This is verified during build time.
392+
// When builtins are injected into other modules (Dict, Set), Result can be at any index.
362393

363394
// Add to scope
364395
const current_scope = &self.scopes.items[self.scopes.items.len - 1];
365396
try current_scope.put(gpa, .type_decl, type_ident, type_decl_idx);
366397

367-
// Add True and False to unqualified_nominal_tags
368-
// TODO: in the future, we should have hardcoded constants for these.
398+
// Add Ok and Err to unqualified_nominal_tags
369399
try self.unqualified_nominal_tags.put(gpa, "Ok", type_decl_idx);
370400
try self.unqualified_nominal_tags.put(gpa, "Err", type_decl_idx);
401+
402+
return type_decl_idx;
371403
}
372404

373405
// canonicalize //

src/canonicalize/test/TestEnv.zig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ pub fn init(source: []const u8) !TestEnv {
5050

5151
try module_env.initCIRFields(gpa, "test");
5252

53-
can.* = try Can.init(module_env, parse_ast, null);
53+
can.* = try Can.init(module_env, parse_ast, null, .{});
5454

5555
return TestEnv{
5656
.gpa = gpa,

0 commit comments

Comments
 (0)