Fix some missing module-not-found checks

This commit is contained in:
Richard Feldman 2025-11-26 09:02:21 -05:00
parent f753b29c12
commit 2cbe9f0cd1
No known key found for this signature in database
9 changed files with 106 additions and 19 deletions

View file

@ -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 = .{

View file

@ -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);
},

View file

@ -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");

View file

@ -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,

View file

@ -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;

View file

@ -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() },

View file

@ -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();

View file

@ -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,
};

View file

@ -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 => {