diff --git a/src/canonicalize/Can.zig b/src/canonicalize/Can.zig index d46ee445dd..0b48ada0f4 100644 --- a/src/canonicalize/Can.zig +++ b/src/canonicalize/Can.zig @@ -2865,18 +2865,18 @@ fn importAliased( _ = try current_scope.introduceImportedModule(self.env.gpa, module_name_text, module_import_idx); // 9. Check that this module actually exists, and if not report an error + // Only check if module_envs is provided - when it's null, we don't know what modules + // exist yet (e.g., during standalone module canonicalization without full project context) + // Also skip the check for platform modules (which have requires_types) since they can + // import sibling modules that may not be in module_envs yet. + const is_platform = self.env.requires_types.items.items.len > 0; if (self.module_envs) |envs_map| { - if (!envs_map.contains(module_name)) { + if (!is_platform and !envs_map.contains(module_name)) { try self.env.pushDiagnostic(Diagnostic{ .module_not_found = .{ .module_name = module_name, .region = import_region, } }); } - } else { - try self.env.pushDiagnostic(Diagnostic{ .module_not_found = .{ - .module_name = module_name, - .region = import_region, - } }); } // If this import satisfies an exposed type requirement (e.g., platform re-exporting @@ -2934,18 +2934,18 @@ fn importWithAlias( _ = try current_scope.introduceImportedModule(self.env.gpa, module_name_text, module_import_idx); // 8. Check that this module actually exists, and if not report an error + // Only check if module_envs is provided - when it's null, we don't know what modules + // exist yet (e.g., during standalone module canonicalization without full project context) + // Also skip the check for platform modules (which have requires_types) since they can + // import sibling modules that may not be in module_envs yet. + const is_platform = self.env.requires_types.items.items.len > 0; if (self.module_envs) |envs_map| { - if (!envs_map.contains(module_name)) { + if (!is_platform and !envs_map.contains(module_name)) { try self.env.pushDiagnostic(Diagnostic{ .module_not_found = .{ .module_name = module_name, .region = import_region, } }); } - } else { - try self.env.pushDiagnostic(Diagnostic{ .module_not_found = .{ - .module_name = module_name, - .region = import_region, - } }); } // If this import satisfies an exposed type requirement (e.g., platform re-exporting @@ -2996,18 +2996,18 @@ fn importUnaliased( _ = try current_scope.introduceImportedModule(self.env.gpa, module_name_text, module_import_idx); // 6. Check that this module actually exists, and if not report an error + // Only check if module_envs is provided - when it's null, we don't know what modules + // exist yet (e.g., during standalone module canonicalization without full project context) + // Also skip the check for platform modules (which have requires_types) since they can + // import sibling modules that may not be in module_envs yet. + const is_platform = self.env.requires_types.items.items.len > 0; if (self.module_envs) |envs_map| { - if (!envs_map.contains(module_name)) { + if (!is_platform and !envs_map.contains(module_name)) { try self.env.pushDiagnostic(Diagnostic{ .module_not_found = .{ .module_name = module_name, .region = import_region, } }); } - } else { - try self.env.pushDiagnostic(Diagnostic{ .module_not_found = .{ - .module_name = module_name, - .region = import_region, - } }); } // If this import satisfies an exposed type requirement (e.g., platform re-exporting @@ -4061,6 +4061,18 @@ pub fn canonicalizeExpr( return CanonicalizedExpr{ .idx = expr_idx, .free_vars = if (free_vars_span.len > 0) free_vars_span else null }; } + // Check if this is a required identifier from the platform's `requires` clause + const requires_items = self.env.requires_types.items.items; + for (requires_items, 0..) |req, idx| { + if (req.ident == ident) { + // Found a required identifier - create a lookup expression for it + const expr_idx = try self.env.addExpr(CIR.Expr{ .e_lookup_required = .{ + .requires_idx = @intCast(idx), + } }, region); + return CanonicalizedExpr{ .idx = expr_idx, .free_vars = null }; + } + } + // We did not find the ident in scope or as an exposed item, and forward refs not allowed return CanonicalizedExpr{ .idx = try self.env.pushMalformed(Expr.Idx, Diagnostic{ .ident_not_in_scope = .{ diff --git a/src/canonicalize/DependencyGraph.zig b/src/canonicalize/DependencyGraph.zig index 24ba712d6d..47a723c57f 100644 --- a/src/canonicalize/DependencyGraph.zig +++ b/src/canonicalize/DependencyGraph.zig @@ -255,6 +255,9 @@ fn collectExprDependencies( // External lookups reference other modules - skip for now .e_lookup_external => {}, + // Required lookups reference app-provided values - skip for dependency analysis + .e_lookup_required => {}, + .e_nominal_external => |nominal| { try collectExprDependencies(cir, nominal.backing_expr, dependencies, allocator); }, diff --git a/src/canonicalize/Expression.zig b/src/canonicalize/Expression.zig index b4e1f813f8..9f9530705f 100644 --- a/src/canonicalize/Expression.zig +++ b/src/canonicalize/Expression.zig @@ -128,6 +128,18 @@ pub const Expr = union(enum) { target_node_idx: u16, region: Region, }, + /// Lookup of a required identifier from the platform's `requires` clause. + /// This represents a value that the app provides to the platform. + /// ```roc + /// platform "..." + /// requires {} { main! : () => {} } + /// ... + /// main_for_host! = main! # "main!" here is a required lookup + /// ``` + e_lookup_required: struct { + /// Index into env.requires_types for this required identifier + requires_idx: u32, + }, /// A sequence of zero or more elements of the same type /// ```roc /// ["one", "two", "three"] @@ -781,6 +793,22 @@ pub const Expr = union(enum) { try tree.endNode(begin, attrs); }, + .e_lookup_required => |e| { + const begin = tree.beginNode(); + try tree.pushStaticAtom("e-lookup-required"); + const region = ir.store.getExprRegion(expr_idx); + try ir.appendRegionInfoToSExprTreeFromRegion(tree, region); + const attrs = tree.beginNode(); + + const requires_items = ir.requires_types.items.items; + if (e.requires_idx < requires_items.len) { + const required_type = requires_items[e.requires_idx]; + const ident_name = ir.getIdent(required_type.ident); + try tree.pushStringPair("required-ident", ident_name); + } + + try tree.endNode(begin, attrs); + }, .e_match => |e| { const begin = tree.beginNode(); try tree.pushStaticAtom("e-match"); diff --git a/src/canonicalize/Node.zig b/src/canonicalize/Node.zig index e92260f9ae..35b86b55b1 100644 --- a/src/canonicalize/Node.zig +++ b/src/canonicalize/Node.zig @@ -52,6 +52,7 @@ pub const Tag = enum { expr_field_access, expr_static_dispatch, expr_external_lookup, + expr_required_lookup, expr_dot_access, expr_apply, expr_string, diff --git a/src/canonicalize/NodeStore.zig b/src/canonicalize/NodeStore.zig index bd47e1fa14..c84f24e5b9 100644 --- a/src/canonicalize/NodeStore.zig +++ b/src/canonicalize/NodeStore.zig @@ -144,7 +144,7 @@ pub fn relocate(store: *NodeStore, offset: isize) void { /// Count of the diagnostic nodes in the ModuleEnv pub const MODULEENV_DIAGNOSTIC_NODE_COUNT = 59; /// Count of the expression nodes in the ModuleEnv -pub const MODULEENV_EXPR_NODE_COUNT = 36; +pub const MODULEENV_EXPR_NODE_COUNT = 37; /// Count of the statement nodes in the ModuleEnv pub const MODULEENV_STATEMENT_NODE_COUNT = 16; /// Count of the type annotation nodes in the ModuleEnv @@ -385,6 +385,12 @@ pub fn getExpr(store: *const NodeStore, expr: CIR.Expr.Idx) CIR.Expr { .region = store.getRegionAt(node_idx), } }; }, + .expr_required_lookup => { + // Handle required lookups (platform requires clause) + return CIR.Expr{ .e_lookup_required = .{ + .requires_idx = node.data_1, + } }; + }, .expr_num => { // Get requirements const kind: CIR.NumKind = @enumFromInt(node.data_1); @@ -1470,6 +1476,11 @@ pub fn addExpr(store: *NodeStore, expr: CIR.Expr, region: base.Region) Allocator node.data_1 = @intFromEnum(e.module_idx); node.data_2 = e.target_node_idx; }, + .e_lookup_required => |e| { + // For required lookups (platform requires clause), store the index + node.tag = .expr_required_lookup; + node.data_1 = e.requires_idx; + }, .e_num => |e| { node.tag = .expr_num; diff --git a/src/canonicalize/test/node_store_test.zig b/src/canonicalize/test/node_store_test.zig index 9ed0e9bcb7..3135f26eb8 100644 --- a/src/canonicalize/test/node_store_test.zig +++ b/src/canonicalize/test/node_store_test.zig @@ -241,6 +241,11 @@ test "NodeStore round trip - Expressions" { .region = rand_region(), }, }); + try expressions.append(gpa, CIR.Expr{ + .e_lookup_required = .{ + .requires_idx = rand.random().int(u32), + }, + }); try expressions.append(gpa, CIR.Expr{ .e_list = .{ .elems = CIR.Expr.Span{ .span = rand_span() }, diff --git a/src/check/Check.zig b/src/check/Check.zig index 4d665df481..c0d64aa548 100644 --- a/src/check/Check.zig +++ b/src/check/Check.zig @@ -3021,6 +3021,22 @@ fn checkExpr(self: *Self, expr_idx: CIR.Expr.Idx, env: *Env, expected: Expected) try self.unifyWith(expr_var, .err, env); } }, + .e_lookup_required => |req| { + // Look up the type from the platform's requires clause + const requires_items = self.cir.requires_types.items.items; + if (req.requires_idx < requires_items.len) { + const required_type = requires_items[req.requires_idx]; + const type_var = ModuleEnv.varFrom(required_type.type_anno); + const instantiated_var = try self.instantiateVar( + type_var, + env, + .{ .explicit = expr_region }, + ); + _ = try self.unify(expr_var, instantiated_var, env); + } else { + try self.unifyWith(expr_var, .err, env); + } + }, // block // .e_block => |block| { const anno_free_vars_top = self.anno_free_vars.top(); diff --git a/src/eval/comptime_evaluator.zig b/src/eval/comptime_evaluator.zig index c862e23099..a1c069de1a 100644 --- a/src/eval/comptime_evaluator.zig +++ b/src/eval/comptime_evaluator.zig @@ -267,6 +267,10 @@ pub const ComptimeEvaluator = struct { // Nothing to evaluate at the declaration site for these; // by design, they cause crashes when lookups happen on them .e_anno_only => return EvalResult{ .success = null }, + // Required lookups reference values from the app's `main` that provides + // values to the platform's `requires` clause. These values are not available + // during compile-time evaluation of the platform - they will be linked at runtime. + .e_lookup_required => return EvalResult{ .success = null }, else => false, }; diff --git a/src/eval/interpreter.zig b/src/eval/interpreter.zig index 7195e572ea..84d6956be1 100644 --- a/src/eval/interpreter.zig +++ b/src/eval/interpreter.zig @@ -2752,6 +2752,13 @@ pub const Interpreter = struct { self.triggerCrash("runtime error", false, roc_ops); return error.Crash; }, + .e_lookup_required => { + // Required lookups reference values from the app that provides values to the + // platform's `requires` clause. These are not available during compile-time + // evaluation - they will be linked at runtime. Return TypeMismatch to signal + // that this expression cannot be evaluated at compile time. + return error.TypeMismatch; + }, // no if handling in minimal evaluator // no second e_binop case; handled above else => {