Merge branch 'main' into builtin-str-impls2

This commit is contained in:
Luke Boswell 2025-11-26 14:39:21 +11:00 committed by GitHub
commit ac18944e72
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 745 additions and 323 deletions

View file

@ -1,80 +0,0 @@
module [Bool, Eq, true, false, not, is_eq, is_not_eq]
## Defines a type that can be compared for total equality.
##
## Total equality means that all values of the type can be compared to each
## other, and two values `a`, `b` are identical if and only if `is_eq(a, b)` is
## `Bool.true`.
##
## Not all types support total equality. For example, [`F32`](Num#F32) and [`F64`](Num#F64) can
## be a `NaN` ([Not a Number](https://en.wikipedia.org/wiki/NaN)), and the
## [IEEE-754](https://en.wikipedia.org/wiki/IEEE_754) floating point standard
## specifies that two `NaN`s are not equal.
Eq(a) : a
where
## Returns `Bool.true` if the input values are equal. This is
## equivalent to the logic
## [XNOR](https://en.wikipedia.org/wiki/Logical_equality) gate. The infix
## operator `==` can be used as shorthand for `Bool.is_eq`.
##
## **Note** that when `is_eq` is determined by the Roc compiler, values are
## compared using structural equality. The rules for this are as follows:
##
## 1. Tags are equal if their name and also contents are equal.
## 2. Records are equal if their fields are equal.
## 3. The collections [Str], [List], [Dict], and [Set] are equal iff they
## are the same length and their elements are equal.
## 4. [Num] values are equal if their numbers are equal. However, if both
## inputs are *NaN* then `is_eq` returns `Bool.false`. Refer to `Num.is_nan`
## for more detail.
## 5. Functions cannot be compared for structural equality, therefore Roc
## cannot derive `is_eq` for types that contain functions.
a.is_eq(a) -> Bool,
## Represents the boolean true and false using an nominal type.
## `Bool` implements the `Eq` ability.
Bool := [True, False]
## The boolean true value.
true : Bool
true = Bool.True
## The boolean false value.
false : Bool
false = Bool.False
## Satisfies the interface of `Eq`
is_eq : Bool, Bool -> Bool
is_eq = |b1, b2| match (b1, b2) {
(Bool.True, Bool.True) => true
(Bool.False, Bool.False) => true
_ => false
}
## Returns `Bool.false` when given `Bool.true`, and vice versa. This is
## equivalent to the logic [NOT](https://en.wikipedia.org/wiki/Negation)
## gate. The operator `!` can also be used as shorthand for `Bool.not`.
## ```roc
## expect Bool.not(Bool.false) == Bool.true
## expect Bool.false != Bool.true
## ```
not : Bool -> Bool
not = |b| match b {
Bool.True => false
Bool.False => true
}
## This will call the function `Bool.is_eq` on the inputs, and then `Bool.not`
## on the result. The is equivalent to the logic
## [XOR](https://en.wikipedia.org/wiki/Exclusive_or) gate. The infix operator
## `!=` can also be used as shorthand for `Bool.is_not_eq`.
##
## **Note** that `is_not_eq` does not accept arguments whose types contain
## functions.
## ```roc
## expect Bool.is_not_eq(Bool.false, Bool.true) == Bool.true
## expect (Bool.false != Bool.false) == Bool.false
## expect "Apples" != "Oranges"
## ```
is_not_eq : a, a -> Bool where a.Eq
is_not_eq = |a, b| not(a.is_eq(b))

View file

@ -1,177 +0,0 @@
module [
Try,
is_ok,
is_err,
is_eq,
map_ok,
map_err,
on_err,
on_err!,
map_both,
map2,
try,
with_default,
]
import Bool exposing [Bool.*]
## The result of an operation that could fail: either the operation went
## okay, or else there was an error of some sort.
Try(ok, err) := [Ok(ok), Err(err)]
## Returns `Bool.true` if the result indicates a success, else returns `Bool.false`.
## ```roc
## Ok(5).is_ok()
## ```
is_ok : Try(ok, err) -> Bool
is_ok = |result| match result {
Try.Ok(_) => Bool.true
Try.Err(_) => Bool.false
}
## Returns `Bool.true` if the result indicates a failure, else returns `Bool.false`.
## ```roc
## Err("uh oh").is_err()
## ```
is_err : Try(ok, err) -> Bool
is_err = |result| match result {
Try.Ok(_) => Bool.false
Try.Err(_) => Bool.true
}
## If the result is `Ok`, returns the value it holds. Otherwise, returns
## the given default value.
##
## Note: This function should be used sparingly, because it hides that an error
## happened, which will make debugging harder. Prefer using `?` to forward errors or
## handle them explicitly with `when`.
## ```roc
## Err("uh oh").with_default(42) # = 42
##
## Ok(7).with_default(42) # = 7
## ```
with_default : Try(ok, err), ok -> ok
with_default = |result, default| match result {
Try.Ok(value) => value
Try.Err(_) => default
}
## If the result is `Ok`, transforms the value it holds by running a conversion
## function on it. Then returns a new `Ok` holding the transformed value. If the
## result is `Err`, this has no effect. Use [map_err] to transform an `Err`.
## ```roc
## Ok(12).map_ok(Num.neg) # = Ok(-12)
##
## Err("yipes!").map_ok(Num.neg) # = Err("yipes!")
## ```
##
## Functions like `map` are common in Roc; see for example [List.map],
## `Set.map`, and `Dict.map`.
map_ok : Try(a, err), (a -> b) -> Try(b, err)
map_ok = |result, transform| match result {
Try.Ok(v) => Try.Ok(transform(v))
Try.Err(e) => Try.Err(e)
}
## If the result is `Err`, transforms the value it holds by running a conversion
## function on it. Then returns a new `Err` holding the transformed value. If
## the result is `Ok`, this has no effect. Use [map] to transform an `Ok`.
## ```roc
## [].last().map_err(|_| ProvidedListIsEmpty) # = Err(ProvidedListIsEmpty)
##
## [4].last().map_err(|_| ProvidedListIsEmpty) # = Ok(4)
## ```
map_err : Try(ok, a), (a -> b) -> Try(ok, b)
map_err = |result, transform| match result {
Try.Ok(v) => Try.Ok(v)
Try.Err(e) => Try.Err(transform(e))
}
## If the result is `Err`, transforms the entire result by running a conversion
## function on the value the `Err` holds. Then returns that new result. If the
## result is `Ok`, this has no effect. Use `?` or [try] to transform an `Ok`.
## ```roc
## Try.on_err(Ok(10), Str.to_u64) # = Ok(10)
##
## Try.on_err(Err("42"), Str.to_u64) # = Ok(42)
##
## Try.on_err(Err("string"), Str.to_u64) # = Err(InvalidNumStr)
## ```
on_err : Try(a, err), (err -> Try(a, other_err)) -> Try(a, other_err)
on_err = |result, transform| match result {
Try.Ok(v) => Try.Ok(v)
Try.Err(e) => transform(e)
}
expect Try.on_err(Ok(10), Str.to_u64) == Try.Ok(10)
expect Try.on_err(Err("42"), Str.to_u64) == Try.Ok(42)
expect Try.on_err(Err("string"), Str.to_u64) == Try.Err(InvalidNumStr)
## Like [on_err], but it allows the transformation function to produce effects.
##
## ```roc
## Err("missing user").on_err(|msg| {
## Stdout.line!("ERROR: ${msg}")?
## Err(msg)
## })
## ```
on_err! : Try(a, err), (err => Try(a, other_err)) => Try(a, other_err)
on_err! = |result, transform!| match result {
Try.Ok(v) => Try.Ok(v)
Try.Err(e) => transform!(e)
}
## Maps both the `Ok` and `Err` values of a `Try` to new values.
map_both : Try(ok1, err1), (ok1 -> ok2), (err1 -> err2) -> Try(ok2, err2)
map_both = |result, ok_transform, err_transform| match result {
Try. Ok(val) => Try.Ok(ok_transform(val))
Try. Err(err) => Try.Err(err_transform(err))
}
## Maps the `Ok` values of two `Try`s to a new value using a given transformation,
## or returns the first `Err` value encountered.
map2 : Try(a, err), Try(b, err), (a, b -> c) -> Try(c, err)
map2 = |first_result, second_result, transform| match (first_result, second_result) {
(Try.Ok(first), Try.Ok(second)) => Ok(transform(first, second))
(Try.Err(err), _) => Try.Err(err)
(_, Try.Err(err)) => Try.Err(err)
}
## If the result is `Ok`, transforms the entire result by running a conversion
## function on the value the `Ok` holds. Then returns that new result. If the
## result is `Err`, this has no effect. Use `on_err` to transform an `Err`.
##
## We recommend using `?` instead of `try`, it makes the code easier to read.
## ```roc
## Ok(-1).try(|num| if num < 0 then Err("negative!") else Ok(-num)) # = Err("negative!")
##
## Ok(1).try(|num| if num < 0 then Err("negative!") else Ok(-num)) # = Ok(-1)
##
## Err("yipes!").try(|num| if num < 0 then Err("negative!") else Ok(-num)) # = Err("yipes!")
## ```
try : Try(a, err), (a -> Try(b, err)) -> Try(b, err)
try = |result, transform| match result {
Try.Ok(v) => transform(v)
Try.Err(e) => Try.Err(e)
}
expect Ok(-1).try(|num| if num < 0 then Err("negative!") else Ok(-num)) == Try.Err("negative!")
expect Ok(1).try(|num| if num < 0 then Err("negative!") else Ok(-num)) == Try.Ok(-1)
expect Err("yipes!").try(|num| if num < 0 then Err("negative!") else Ok(-num)) == Try.Err("yipes!")
## Implementation of [Bool.Eq]. Checks if two results that have both `ok` and `err` types that are `Eq` are themselves equal.
##
## ```roc
## Ok("Hello").is_eq(Ok("Hello"))
## ```
is_eq : Try(ok, err), Try(ok, err) -> Bool where [ok.Eq, err.Eq]
is_eq = |r1, r2| match (r1, r2) {
(Try.Ok(ok1), Try.Ok(ok2)) => ok1 == ok2
(Try.Err(err1), Try.Err(err2)) => err1 == err2
}
expect Try.Ok(1) == Try.Ok(1)
expect Try.Ok(2) != Try.Ok(1)
expect Try.Err("Foo") == Try.Err("Foo")
expect Try.Err("Bar") != Try.Err("Foo")
expect Try.Ok("Foo") != Try.Err("Foo")

View file

@ -1,6 +0,0 @@
package
[
Bool,
Try,
]
{}

View file

@ -1700,6 +1700,10 @@ pub fn canonicalizeFile(
.platform => |h| {
self.env.module_kind = .platform;
try self.createExposedScope(h.exposes);
// Extract required type signatures for type checking
// This stores the types in env.requires_types without creating local definitions
// Pass requires_rigids so R1, R2, etc. are in scope when processing signatures
try self.processRequiresSignatures(h.requires_rigids, h.requires_signatures);
},
.hosted => |h| {
self.env.module_kind = .hosted;
@ -2589,6 +2593,82 @@ fn createExposedScope(
}
}
/// Process the requires_signatures from a platform header.
///
/// This extracts the required type signatures (like `main! : () => {}`) from the platform
/// header and stores them in `env.requires_types`. These are used during app type checking
/// to ensure the app's provided values match the platform's expected types.
///
/// The requires_rigids parameter contains the type variables declared in `requires { R1, R2 }`.
/// These are introduced into scope before processing the signatures so that references to
/// R1, R2, etc. in the signatures are properly resolved as type variables.
///
/// Note: This does NOT create local definitions for the required identifiers. The platform
/// body can reference these identifiers as forward references that will be resolved to
/// the app's exports at runtime.
fn processRequiresSignatures(self: *Self, requires_rigids_idx: AST.Collection.Idx, requires_signatures_idx: AST.TypeAnno.Idx) std.mem.Allocator.Error!void {
// First, process the requires_rigids to add them to the type variable scope
// This allows R1, R2, etc. to be recognized when processing the signatures
const rigids_collection = self.parse_ir.store.getCollection(requires_rigids_idx);
for (self.parse_ir.store.exposedItemSlice(.{ .span = rigids_collection.span })) |exposed_idx| {
const exposed_item = self.parse_ir.store.getExposedItem(exposed_idx);
switch (exposed_item) {
.upper_ident => |upper| {
// Get the identifier for this rigid type variable (e.g., "R1")
const rigid_name = self.parse_ir.tokens.resolveIdentifier(upper.ident) orelse continue;
const rigid_region = self.parse_ir.tokenizedRegionToRegion(upper.region);
// Create a type annotation for this rigid variable
const rigid_anno_idx = try self.env.addTypeAnno(.{ .rigid_var = .{
.name = rigid_name,
} }, rigid_region);
// Introduce it into the type variable scope
_ = try self.scopeIntroduceTypeVar(rigid_name, rigid_anno_idx);
},
else => {
// Skip lower_ident, upper_ident_star, malformed - these aren't valid for requires rigids
},
}
}
// Now process the requires_signatures with the rigids in scope
const requires_signatures = self.parse_ir.store.getTypeAnno(requires_signatures_idx);
// The requires_signatures should be a record type like { main! : () => {} }
switch (requires_signatures) {
.record => |record| {
for (self.parse_ir.store.annoRecordFieldSlice(record.fields)) |field_idx| {
const field = self.parse_ir.store.getAnnoRecordField(field_idx) catch |err| switch (err) {
error.MalformedNode => {
// Skip malformed fields
continue;
},
};
// Get the field name (e.g., "main!")
const field_name = self.parse_ir.tokens.resolveIdentifier(field.name) orelse continue;
const field_region = self.parse_ir.tokenizedRegionToRegion(field.region);
// Canonicalize the type annotation for this required identifier
var type_anno_ctx = TypeAnnoCtx.init(.inline_anno);
const type_anno_idx = try self.canonicalizeTypeAnnoHelp(field.ty, &type_anno_ctx);
// Store the required type in the module env
_ = try self.env.requires_types.append(self.env.gpa, .{
.ident = field_name,
.type_anno = type_anno_idx,
.region = field_region,
});
}
},
else => {
// requires_signatures should always be a record type from parsing
// If it's not, just skip processing (parser would have reported an error)
},
}
}
fn populateExports(self: *Self) std.mem.Allocator.Error!void {
// Start a new scratch space for exports
const scratch_exports_start = self.env.store.scratchDefTop();
@ -7111,6 +7191,17 @@ fn canonicalizeTypeAnnoBasicType(
}
}
// Check if this is a type variable in scope (e.g., R1, R2 from requires { R1, R2 })
switch (self.scopeLookupTypeVar(type_name_ident)) {
.found => |found_anno_idx| {
// Found a type variable with this name - create a reference to it
return try self.env.addTypeAnno(.{ .rigid_var_lookup = .{
.ref = found_anno_idx,
} }, region);
},
.not_found => {},
}
// Not found anywhere - undeclared type
return try self.env.pushMalformed(TypeAnno.Idx, Diagnostic{ .undeclared_type = .{
.name = type_name_ident,

View file

@ -108,6 +108,10 @@ all_defs: CIR.Def.Span,
all_statements: CIR.Statement.Span,
/// Definitions that are exported by this module (populated by canonicalization)
exports: CIR.Def.Span,
/// Required type signatures for platform modules (from `requires {} { main! : () => {} }`)
/// Maps identifier names to their expected type annotations.
/// Empty for non-platform modules.
requires_types: RequiredType.SafeList,
/// All builtin stmts (temporary until module imports are working)
builtin_statements: CIR.Statement.Span,
/// All external declarations referenced in this module
@ -185,6 +189,19 @@ pub const DeferredNumericLiteral = struct {
pub const SafeList = collections.SafeList(@This());
};
/// Required type for platform modules - maps an identifier to its expected type annotation.
/// Used to enforce that apps provide values matching the platform's required types.
pub const RequiredType = struct {
/// The identifier name (e.g., "main!")
ident: Ident.Idx,
/// The canonicalized type annotation for this required value
type_anno: CIR.TypeAnno.Idx,
/// Region of the requirement for error reporting
region: Region,
pub const SafeList = collections.SafeList(@This());
};
/// Relocate all pointers in the ModuleEnv by the given offset.
/// This is used when loading a ModuleEnv from shared memory at a different address.
pub fn relocate(self: *Self, offset: isize) void {
@ -192,6 +209,7 @@ pub fn relocate(self: *Self, offset: isize) void {
self.common.relocate(offset);
self.types.relocate(offset);
self.external_decls.relocate(offset);
self.requires_types.relocate(offset);
self.imports.relocate(offset);
self.store.relocate(offset);
self.deferred_numeric_literals.relocate(offset);
@ -276,6 +294,7 @@ pub fn init(gpa: std.mem.Allocator, source: []const u8) std.mem.Allocator.Error!
.all_defs = .{ .span = .{ .start = 0, .len = 0 } },
.all_statements = .{ .span = .{ .start = 0, .len = 0 } },
.exports = .{ .span = .{ .start = 0, .len = 0 } },
.requires_types = try RequiredType.SafeList.initCapacity(gpa, 4),
.builtin_statements = .{ .span = .{ .start = 0, .len = 0 } },
.external_decls = try CIR.ExternalDecl.SafeList.initCapacity(gpa, 16),
.imports = CIR.Import.Store.init(),
@ -312,6 +331,7 @@ pub fn deinit(self: *Self) void {
self.common.deinit(self.gpa);
self.types.deinit();
self.external_decls.deinit(self.gpa);
self.requires_types.deinit(self.gpa);
self.imports.deinit(self.gpa);
self.deferred_numeric_literals.deinit(self.gpa);
// diagnostics are stored in the NodeStore, no need to free separately
@ -1713,6 +1733,7 @@ pub const Serialized = extern struct {
all_defs: CIR.Def.Span,
all_statements: CIR.Statement.Span,
exports: CIR.Def.Span,
requires_types: RequiredType.SafeList.Serialized,
builtin_statements: CIR.Statement.Span,
external_decls: CIR.ExternalDecl.SafeList.Serialized,
imports: CIR.Import.Store.Serialized,
@ -1760,6 +1781,7 @@ pub const Serialized = extern struct {
self.exports = env.exports;
self.builtin_statements = env.builtin_statements;
try self.requires_types.serialize(&env.requires_types, allocator, writer);
try self.external_decls.serialize(&env.external_decls, allocator, writer);
try self.imports.serialize(&env.imports, allocator, writer);
@ -1825,6 +1847,7 @@ pub const Serialized = extern struct {
.all_defs = self.all_defs,
.all_statements = self.all_statements,
.exports = self.exports,
.requires_types = self.requires_types.deserialize(offset).*,
.builtin_statements = self.builtin_statements,
.external_decls = self.external_decls.deserialize(offset).*,
.imports = (try self.imports.deserialize(offset, gpa)).*,

View file

@ -1062,6 +1062,84 @@ pub fn checkFile(self: *Self) std.mem.Allocator.Error!void {
// Note that we can't use SCCs to determine the order to resolve defs
// because anonymous static dispatch makes function order not knowable
// before type inference
// Process requires_types annotations for platforms
// This ensures the type store has the actual types for platform requirements
try self.processRequiresTypes(&env);
}
/// Process the requires_types annotations for platform modules.
/// This generates the actual types from the type annotations stored in requires_types.
fn processRequiresTypes(self: *Self, env: *Env) std.mem.Allocator.Error!void {
const requires_types_slice = self.cir.requires_types.items.items;
for (requires_types_slice) |required_type| {
// Generate the type from the annotation
try self.generateAnnoTypeInPlace(required_type.type_anno, env, .annotation);
}
}
/// Check that the app's exported values match the platform's required types.
/// This should be called after checkFile() to verify that app exports conform
/// to the platform's requirements.
pub fn checkPlatformRequirements(self: *Self, platform_env: *const ModuleEnv) std.mem.Allocator.Error!void {
const trace = tracy.trace(@src());
defer trace.end();
// Create a solver env for type operations
var env = try self.env_pool.acquire(.generalized);
defer self.env_pool.release(env);
// Iterate over the platform's required types
const requires_types_slice = platform_env.requires_types.items.items;
for (requires_types_slice) |required_type| {
// Get the identifier name for this required type
const required_ident = required_type.ident;
const required_ident_text = platform_env.getIdent(required_ident);
// Find the matching export in the app
const app_exports_slice = self.cir.store.sliceDefs(self.cir.exports);
var found_export: ?CIR.Def.Idx = null;
for (app_exports_slice) |def_idx| {
const def = self.cir.store.getDef(def_idx);
const pattern = self.cir.store.getPattern(def.pattern);
if (pattern == .assign) {
const export_ident_text = self.cir.getIdent(pattern.assign.ident);
if (std.mem.eql(u8, export_ident_text, required_ident_text)) {
found_export = def_idx;
break;
}
}
}
if (found_export) |export_def_idx| {
// Get the app export's type variable
const export_def = self.cir.store.getDef(export_def_idx);
const export_var = ModuleEnv.varFrom(export_def.pattern);
// Copy the required type from the platform's type store into the app's type store
// First, convert the type annotation to a type variable in the platform's context
const required_type_var = ModuleEnv.varFrom(required_type.type_anno);
// Copy the type from the platform's type store
const copied_required_var = try self.copyVar(required_type_var, platform_env, required_type.region);
// Instantiate the copied variable before unifying (to avoid poisoning the cached copy)
const instantiated_required_var = try self.instantiateVar(copied_required_var, &env, .{ .explicit = required_type.region });
// Create a copy of the export's type for unification.
// This prevents unification failure from corrupting the app's actual types
// (which would cause the interpreter to fail when trying to get layouts).
const export_copy = try self.copyVar(export_var, self.cir, required_type.region);
const instantiated_export_copy = try self.instantiateVar(export_copy, &env, .{ .explicit = required_type.region });
// Unify the platform's required type with the COPY of the app's export type.
// The platform type is the "expected" type, app export copy is "actual".
_ = try self.unifyFromAnno(instantiated_required_var, instantiated_export_copy, &env);
}
// Note: If the export is not found, the canonicalizer should have already reported an error
}
}
// repl //
@ -3154,8 +3232,10 @@ fn checkExpr(self: *Self, expr_idx: CIR.Expr.Idx, env: *Env, expected: Expected)
// without doing any additional work
try self.unifyWith(expr_var, .err, env);
} else {
// From the base function type, extract the actual function info
const mb_func: ?types_mod.Func = inner_blk: {
// From the base function type, extract the actual function info
// and also track whether the function is effectful
const FuncInfo = struct { func: types_mod.Func, is_effectful: bool };
const mb_func_info: ?FuncInfo = inner_blk: {
// Here, we unwrap the function, following aliases, to get
// the actual function we want to check against
var var_ = func_var;
@ -3163,9 +3243,9 @@ fn checkExpr(self: *Self, expr_idx: CIR.Expr.Idx, env: *Env, expected: Expected)
switch (self.types.resolveVar(var_).desc.content) {
.structure => |flat_type| {
switch (flat_type) {
.fn_pure => |func| break :inner_blk func,
.fn_unbound => |func| break :inner_blk func,
.fn_effectful => |func| break :inner_blk func,
.fn_pure => |func| break :inner_blk FuncInfo{ .func = func, .is_effectful = false },
.fn_unbound => |func| break :inner_blk FuncInfo{ .func = func, .is_effectful = false },
.fn_effectful => |func| break :inner_blk FuncInfo{ .func = func, .is_effectful = true },
else => break :inner_blk null,
}
},
@ -3176,6 +3256,14 @@ fn checkExpr(self: *Self, expr_idx: CIR.Expr.Idx, env: *Env, expected: Expected)
}
}
};
const mb_func = if (mb_func_info) |info| info.func else null;
// If the function being called is effectful, mark this expression as effectful
if (mb_func_info) |info| {
if (info.is_effectful) {
does_fx = true;
}
}
// Get the name of the function (for error messages)
const func_name: ?Ident.Idx = inner_blk: {

View file

@ -77,6 +77,7 @@ fn loadCompiledModule(gpa: std.mem.Allocator, bin_data: []const u8, module_name:
.all_defs = serialized_ptr.all_defs,
.all_statements = serialized_ptr.all_statements,
.exports = serialized_ptr.exports,
.requires_types = serialized_ptr.requires_types.deserialize(@as(i64, @intCast(base_ptr))).*,
.builtin_statements = serialized_ptr.builtin_statements,
.external_decls = serialized_ptr.external_decls.deserialize(@as(i64, @intCast(base_ptr))).*,
.imports = (try serialized_ptr.imports.deserialize(@as(i64, @intCast(base_ptr)), gpa)).*,

View file

@ -136,6 +136,7 @@ pub const ExperimentalLspArgs = struct {
pub fn parse(alloc: mem.Allocator, args: []const []const u8) !CliArgs {
if (args.len == 0) return try parseRun(alloc, args);
if (mem.eql(u8, args[0], "run")) return try parseRun(alloc, args[1..]);
if (mem.eql(u8, args[0], "check")) return parseCheck(args[1..]);
if (mem.eql(u8, args[0], "build")) return parseBuild(args[1..]);
if (mem.eql(u8, args[0], "bundle")) return try parseBundle(alloc, args[1..]);

View file

@ -1364,11 +1364,16 @@ pub fn setupSharedMemoryWithModuleEnv(allocs: *Allocators, roc_file_path: []cons
var exposed_modules = std.ArrayList([]const u8).empty;
defer exposed_modules.deinit(allocs.gpa);
var has_platform = true;
extractExposedModulesFromPlatform(allocs, platform_main_path, &exposed_modules) catch {
// No platform found - that's fine, just continue with no platform modules
has_platform = false;
};
// Create header - use multi-module format
// IMPORTANT: Create header FIRST before any module compilation.
// The interpreter_shim expects the Header to be at FIRST_ALLOC_OFFSET (504).
// If we compile modules first, they would occupy that offset and break
// shared memory layout assumptions.
const Header = struct {
parent_base_addr: u64,
module_count: u32,
@ -1381,6 +1386,20 @@ pub fn setupSharedMemoryWithModuleEnv(allocs: *Allocators, roc_file_path: []cons
const shm_base_addr = @intFromPtr(shm.base_ptr);
header_ptr.parent_base_addr = shm_base_addr;
// Compile platform main.roc to get requires_types (if platform exists)
// This must come AFTER header allocation to preserve memory layout.
var platform_main_env: ?*ModuleEnv = null;
if (has_platform) {
platform_main_env = compileModuleToSharedMemory(
allocs,
platform_main_path,
"main.roc",
shm_allocator,
&builtin_modules,
&.{},
) catch null;
}
// Module count = 1 (app) + number of platform modules
const total_module_count: u32 = 1 + @as(u32, @intCast(exposed_modules.items.len));
header_ptr.module_count = total_module_count;
@ -1407,6 +1426,7 @@ pub fn setupSharedMemoryWithModuleEnv(allocs: *Allocators, roc_file_path: []cons
module_filename,
shm_allocator,
&builtin_modules,
&.{},
);
// Add exposed item aliases with "pf." prefix for import resolution
@ -1557,6 +1577,11 @@ pub fn setupSharedMemoryWithModuleEnv(allocs: *Allocators, roc_file_path: []cons
builtin_modules.builtin_indices,
);
for (platform_env_ptrs) |mod_env| {
const name = try app_env.insertIdent(base.Ident.for_text(mod_env.module_name));
try app_module_envs_map.put(name, .{ .env = mod_env });
}
// Add platform modules to the module envs map for canonicalization
// Two keys are needed for each platform module:
// 1. "pf.Stdout" - used during import validation (import pf.Stdout)
@ -1637,6 +1662,11 @@ pub fn setupSharedMemoryWithModuleEnv(allocs: *Allocators, roc_file_path: []cons
try app_checker.checkFile();
// Check that app exports match platform requirements (if platform exists)
if (platform_main_env) |penv| {
try app_checker.checkPlatformRequirements(penv);
}
app_env_ptr.* = app_env;
shm.updateHeader();
@ -1707,6 +1737,7 @@ fn compileModuleToSharedMemory(
module_name_arg: []const u8,
shm_allocator: std.mem.Allocator,
builtin_modules: *eval.BuiltinModules,
additional_modules: []const *ModuleEnv,
) !*ModuleEnv {
// Read file
const file = try std.fs.cwd().openFile(file_path, .{});
@ -1743,6 +1774,11 @@ fn compileModuleToSharedMemory(
builtin_modules.builtin_indices,
);
for (additional_modules) |mod_env| {
const name = try env.insertIdent(base.Ident.for_text(mod_env.module_name));
try module_envs_map.put(name, .{ .env = mod_env });
}
// Canonicalize (without root_is_platform - we'll run HostedCompiler separately)
var canonicalizer = try Can.init(&env, &parse_ast, &module_envs_map);
defer canonicalizer.deinit();

View file

@ -265,6 +265,46 @@ test "fx platform expect with numeric literal" {
try testing.expectEqualStrings("", run_result.stderr);
}
test "fx platform match returning string" {
const allocator = testing.allocator;
try ensureRocBinary(allocator);
// Run the app that has a match expression returning a string
// This tests that match expressions with string returns work correctly
const run_result = try std.process.Child.run(.{
.allocator = allocator,
.argv = &[_][]const u8{
"./zig-out/bin/roc",
"test/fx/match_str_return.roc",
},
});
defer allocator.free(run_result.stdout);
defer allocator.free(run_result.stderr);
switch (run_result.term) {
.Exited => |code| {
if (code != 0) {
std.debug.print("Run failed with exit code {}\n", .{code});
std.debug.print("STDOUT: {s}\n", .{run_result.stdout});
std.debug.print("STDERR: {s}\n", .{run_result.stderr});
return error.RunFailed;
}
},
else => {
std.debug.print("Run terminated abnormally: {}\n", .{run_result.term});
std.debug.print("STDOUT: {s}\n", .{run_result.stdout});
std.debug.print("STDERR: {s}\n", .{run_result.stderr});
return error.RunFailed;
},
}
// The app should run successfully and exit with code 0
// It outputs "0" from the match expression
try testing.expectEqualStrings("0\n", run_result.stdout);
try testing.expectEqualStrings("", run_result.stderr);
}
test "fx platform match with wildcard" {
const allocator = testing.allocator;

View file

@ -16,12 +16,16 @@ const builtin = @import("builtin");
const build_options = @import("build_options");
const reporting = @import("reporting");
const eval = @import("eval");
const check = @import("check");
const Report = reporting.Report;
const ReportBuilder = check.ReportBuilder;
const BuiltinModules = eval.BuiltinModules;
const Mode = @import("compile_package.zig").Mode;
const Allocator = std.mem.Allocator;
const ModuleEnv = can.ModuleEnv;
const Can = can.Can;
const Check = check.Check;
const PackageEnv = @import("compile_package.zig").PackageEnv;
const ModuleTimingInfo = @import("compile_package.zig").TimingInfo;
const ImportResolver = @import("compile_package.zig").ImportResolver;
@ -544,10 +548,125 @@ pub const BuildEnv = struct {
// Note: In single-threaded mode, buildRoot() runs synchronously and blocks
// until all modules are complete, so no additional waiting is needed.
// Check platform requirements for app modules
try self.checkPlatformRequirements();
// Deterministic emission: globally order reports by (min dependency depth from app, then module name)
try self.emitDeterministic();
}
/// Check that app exports match platform requirements.
/// This is called after all modules are compiled and type-checked.
fn checkPlatformRequirements(self: *BuildEnv) !void {
// Find the app and platform packages
var app_pkg: ?[]const u8 = null;
var platform_pkg: ?[]const u8 = null;
var pkg_it = self.packages.iterator();
while (pkg_it.next()) |entry| {
const pkg = entry.value_ptr.*;
if (pkg.kind == .app) {
app_pkg = entry.key_ptr.*;
} else if (pkg.kind == .platform) {
platform_pkg = entry.key_ptr.*;
}
}
// If we don't have both an app and a platform, nothing to check
const app_name = app_pkg orelse return;
const platform_name = platform_pkg orelse return;
// Get the schedulers for both packages
const app_sched = self.schedulers.get(app_name) orelse return;
const platform_sched = self.schedulers.get(platform_name) orelse return;
// Get the root module envs for both packages
const app_root_env = app_sched.getRootEnv() orelse return;
const platform_root_env = platform_sched.getRootEnv() orelse return;
// If the platform has no requires_types, nothing to check
if (platform_root_env.requires_types.items.items.len == 0) {
return;
}
// Get builtin indices and module
const builtin_indices = self.builtin_modules.builtin_indices;
const builtin_module_env = self.builtin_modules.builtin_module.env;
// Build module_envs_map for type resolution
var module_envs_map = std.AutoHashMap(base.Ident.Idx, Can.AutoImportedType).init(self.gpa);
defer module_envs_map.deinit();
// Add builtin types (Bool, Try, Str)
if (app_root_env.common.findIdent("Bool")) |bi| {
try module_envs_map.put(bi, .{
.env = builtin_module_env,
.statement_idx = builtin_indices.bool_type,
});
}
if (app_root_env.common.findIdent("Try")) |ti| {
try module_envs_map.put(ti, .{
.env = builtin_module_env,
.statement_idx = builtin_indices.try_type,
});
}
if (app_root_env.common.findIdent("Str")) |si| {
try module_envs_map.put(si, .{
.env = builtin_module_env,
.statement_idx = builtin_indices.str_type,
});
}
// Build common idents for the type checker
const common_idents = Check.CommonIdents{
.module_name = app_root_env.module_name_idx,
.list = app_root_env.common.findIdent("List") orelse return,
.box = app_root_env.common.findIdent("Box") orelse return,
.@"try" = app_root_env.common.findIdent("Try") orelse return,
.bool_stmt = builtin_indices.bool_type,
.try_stmt = builtin_indices.try_type,
.str_stmt = builtin_indices.str_type,
.builtin_module = builtin_module_env,
};
// Create type checker for the app module
var checker = try Check.init(
self.gpa,
&app_root_env.types,
app_root_env,
&.{}, // No imported modules needed for checking exports
&module_envs_map,
&app_root_env.store.regions,
common_idents,
);
defer checker.deinit();
// Check platform requirements against app exports
try checker.checkPlatformRequirements(platform_root_env);
// If there are type problems, convert them to reports and emit via sink
if (checker.problems.problems.items.len > 0) {
const app_root_module = app_sched.getRootModule() orelse return;
var rb = ReportBuilder.init(
self.gpa,
app_root_env,
app_root_env,
&checker.snapshots,
app_root_module.path,
&.{},
&checker.import_mapping,
);
defer rb.deinit();
for (checker.problems.problems.items) |prob| {
const rep = rb.build(prob) catch continue;
// Emit via sink with the module name (not path) to match other reports
self.sink.emitReport(app_name, app_root_module.name, rep);
}
}
}
// ------------------------
// Resolver implementation
// ------------------------

View file

@ -252,6 +252,18 @@ pub const PackageEnv = struct {
self.emitted.deinit(self.gpa);
}
/// Get the root module's env (first module added)
pub fn getRootEnv(self: *PackageEnv) ?*ModuleEnv {
if (self.modules.items.len == 0) return null;
return if (self.modules.items[0].env) |*env| env else null;
}
/// Get the root module state (first module added)
pub fn getRootModule(self: *PackageEnv) ?*ModuleState {
if (self.modules.items.len == 0) return null;
return &self.modules.items[0];
}
fn internModuleName(self: *PackageEnv, name: []const u8) !ModuleId {
const gop = try self.module_names.getOrPut(self.gpa, name);
if (!gop.found_existing) {

View file

@ -94,6 +94,7 @@ test "ModuleEnv.Serialized roundtrip" {
.all_defs = deserialized_ptr.all_defs,
.all_statements = deserialized_ptr.all_statements,
.exports = deserialized_ptr.exports,
.requires_types = deserialized_ptr.requires_types.deserialize(@as(i64, @intCast(@intFromPtr(buffer.ptr)))).*,
.builtin_statements = deserialized_ptr.builtin_statements,
.external_decls = deserialized_ptr.external_decls.deserialize(@as(i64, @intCast(@intFromPtr(buffer.ptr)))).*,
.imports = (try deserialized_ptr.imports.deserialize(@as(i64, @intCast(@intFromPtr(buffer.ptr))), deser_alloc)).*,

View file

@ -69,6 +69,7 @@ pub fn loadCompiledModule(gpa: std.mem.Allocator, bin_data: []const u8, module_n
.all_defs = serialized_ptr.all_defs,
.all_statements = serialized_ptr.all_statements,
.exports = serialized_ptr.exports,
.requires_types = serialized_ptr.requires_types.deserialize(@as(i64, @intCast(base_ptr))).*,
.builtin_statements = serialized_ptr.builtin_statements,
.external_decls = serialized_ptr.external_decls.deserialize(@as(i64, @intCast(base_ptr))).*,
.imports = (try serialized_ptr.imports.deserialize(@as(i64, @intCast(base_ptr)), gpa)).*,

View file

@ -234,6 +234,26 @@ pub const Interpreter = struct {
var next_id: u32 = 1; // Start at 1, reserve 0 for current module
var imported_modules = std.StringHashMap(*const can.ModuleEnv).init(allocator);
errdefer imported_modules.deinit();
if (other_envs.len > 0) {
// Populate imported_modules with platform modules and builtin module
// This allows dynamic lookup by name, which is needed for cross-module calls
// when imports are processed in different orders across modules
for (other_envs) |module_env| {
const module_name = module_env.module_name;
// Add full name "Stdout.roc"
try imported_modules.put(module_name, module_env);
// Add name without extension if present "Stdout"
if (std.mem.endsWith(u8, module_name, ".roc")) {
const short_name = module_name[0 .. module_name.len - 4];
try imported_modules.put(short_name, module_env);
}
}
}
// Safely access import count
const import_count = if (env.imports.imports.items.items.len > 0)
env.imports.imports.items.items.len
@ -320,7 +340,7 @@ pub const Interpreter = struct {
}
}
return initWithModuleEnvs(allocator, env, module_envs, module_ids, import_envs, next_id, builtin_types, builtin_module_env);
return initWithModuleEnvs(allocator, env, module_envs, module_ids, import_envs, imported_modules, next_id, builtin_types, builtin_module_env);
}
/// Deinit the interpreter and also free the module maps if they were allocated by init()
@ -334,6 +354,7 @@ pub const Interpreter = struct {
module_envs: std.AutoHashMapUnmanaged(base_pkg.Ident.Idx, *const can.ModuleEnv),
module_ids: std.AutoHashMapUnmanaged(base_pkg.Ident.Idx, u32),
import_envs: std.AutoHashMapUnmanaged(can.CIR.Import.Idx, *const can.ModuleEnv),
imported_modules: std.StringHashMap(*const can.ModuleEnv),
next_module_id: u32,
builtin_types: BuiltinTypes,
builtin_module_env: ?*const can.ModuleEnv,
@ -368,7 +389,7 @@ pub const Interpreter = struct {
.canonical_bool_rt_var = null,
.scratch_tags = try std.array_list.Managed(types.Tag).initCapacity(allocator, 8),
.builtins = builtin_types,
.imported_modules = std.StringHashMap(*const can.ModuleEnv).init(allocator),
.imported_modules = imported_modules,
.def_stack = try std.array_list.Managed(DefInProgress).initCapacity(allocator, 4),
.num_literal_target_type = null,
.last_error_message = null,
@ -416,7 +437,8 @@ pub const Interpreter = struct {
defer func_val.decref(&self.runtime_layout_store, roc_ops);
if (func_val.layout.tag != .closure) {
return error.NotImplemented;
self.triggerCrash("DEBUG: evaluateExpression func_val not closure", false, roc_ops);
return error.Crash;
}
const header: *const layout.Closure = @ptrCast(@alignCast(func_val.ptr.?));
@ -492,14 +514,45 @@ pub const Interpreter = struct {
const result_value = try self.evalExprMinimal(header.body_idx, roc_ops, null);
defer result_value.decref(&self.runtime_layout_store, roc_ops);
try result_value.copyToPtr(&self.runtime_layout_store, ret_ptr, roc_ops);
// Only copy result if the result type is compatible with ret_ptr
if (try self.shouldCopyResult(result_value, ret_ptr)) {
try result_value.copyToPtr(&self.runtime_layout_store, ret_ptr, roc_ops);
}
return;
}
const result = try self.evalMinimal(expr_idx, roc_ops);
defer result.decref(&self.runtime_layout_store, roc_ops);
try result.copyToPtr(&self.runtime_layout_store, ret_ptr, roc_ops);
// Only copy result if the result type is compatible with ret_ptr
if (try self.shouldCopyResult(result, ret_ptr)) {
try result.copyToPtr(&self.runtime_layout_store, ret_ptr, roc_ops);
}
}
/// Check if the result should be copied to ret_ptr based on the result's layout.
/// Returns false for zero-sized types (nothing to copy).
/// Validates that ret_ptr is properly aligned for the result type.
fn shouldCopyResult(self: *Interpreter, result: StackValue, ret_ptr: *anyopaque) !bool {
const result_size = self.runtime_layout_store.layoutSize(result.layout);
if (result_size == 0) {
// Zero-sized types don't need copying
return false;
}
// Validate alignment: ret_ptr must be properly aligned for the result type.
// A mismatch here indicates a type error between what the platform expects
// and what the Roc code returns. This should have been caught at compile
// time, but if the type checking didn't enforce the constraint, we catch
// it here at runtime.
const required_alignment = result.layout.alignment(self.runtime_layout_store.targetUsize());
const ret_addr = @intFromPtr(ret_ptr);
if (ret_addr % required_alignment.toByteUnits() != 0) {
// Type mismatch detected at runtime
return error.TypeMismatch;
}
return true;
}
fn evalExprMinimal(
@ -660,7 +713,10 @@ pub const Interpreter = struct {
},
.s_reassign => |r| {
const patt = self.env.store.getPattern(r.pattern_idx);
if (patt != .assign) return error.NotImplemented;
if (patt != .assign) {
self.triggerCrash("DEBUG: s_reassign pattern not assign", false, roc_ops);
return error.Crash;
}
const new_val = try self.evalExprMinimal(r.expr, roc_ops, null);
// Search through all bindings, not just current block scope
// This allows reassigning variables from outer scopes (e.g., in for loops)
@ -797,7 +853,10 @@ pub const Interpreter = struct {
// While loop completes and returns {} (implicitly)
},
else => return error.NotImplemented,
else => {
self.triggerCrash("DEBUG: stmt not implemented", false, roc_ops);
return error.Crash;
},
}
}
@ -1351,7 +1410,10 @@ pub const Interpreter = struct {
break :blk try self.translateTypeVar(self.env, ct_var);
};
const resolved = self.runtime_types.resolveVar(rt_var);
if (resolved.desc.content != .structure or resolved.desc.content.structure != .tag_union) return error.NotImplemented;
if (resolved.desc.content != .structure or resolved.desc.content.structure != .tag_union) {
self.triggerCrash("DEBUG: e_zero_argument_tag not tag union", false, roc_ops);
return error.Crash;
}
const tu = resolved.desc.content.structure.tag_union;
const tags = self.runtime_types.getTagsSlice(tu.tags);
// Find index by name
@ -1384,19 +1446,26 @@ pub const Interpreter = struct {
out.is_initialized = true;
return out;
}
return error.NotImplemented;
self.triggerCrash("DEBUG: e_zero_argument_tag scalar not int", false, roc_ops);
return error.Crash;
} else if (layout_val.tag == .record) {
// Record { tag: Discriminant, payload: ZST }
var dest = try self.pushRaw(layout_val, 0);
var acc = try dest.asRecord(&self.runtime_layout_store);
const tag_idx = acc.findFieldIndex(self.env, "tag") orelse return error.NotImplemented;
const tag_idx = acc.findFieldIndex(self.env, "tag") orelse {
self.triggerCrash("DEBUG: e_zero_argument_tag tag field not found", false, roc_ops);
return error.Crash;
};
const tag_field = try acc.getFieldByIndex(tag_idx);
// write tag as int
if (tag_field.layout.tag == .scalar and tag_field.layout.data.scalar.tag == .int) {
var tmp = tag_field;
tmp.is_initialized = false;
try tmp.setInt(@intCast(tag_index));
} else return error.NotImplemented;
} else {
self.triggerCrash("DEBUG: e_zero_argument_tag tag field not int", false, roc_ops);
return error.Crash;
}
return dest;
} else if (layout_val.tag == .tuple) {
// Tuple (payload, tag) - tag unions are now represented as tuples
@ -1409,10 +1478,14 @@ pub const Interpreter = struct {
var tmp = tag_field;
tmp.is_initialized = false;
try tmp.setInt(@intCast(tag_index));
} else return error.NotImplemented;
} else {
self.triggerCrash("DEBUG: e_zero_argument_tag tuple tag not int", false, roc_ops);
return error.Crash;
}
return dest;
}
return error.NotImplemented;
self.triggerCrash("DEBUG: e_zero_argument_tag layout not implemented", false, roc_ops);
return error.Crash;
},
.e_tag => |tag| {
// Construct a tag union value with payloads
@ -1422,7 +1495,10 @@ pub const Interpreter = struct {
};
// Unwrap nominal types and aliases to get the base tag union
const resolved = self.resolveBaseVar(rt_var);
if (resolved.desc.content != .structure or resolved.desc.content.structure != .tag_union) return error.NotImplemented;
if (resolved.desc.content != .structure or resolved.desc.content.structure != .tag_union) {
self.triggerCrash("DEBUG: e_tag not tag union", false, roc_ops);
return error.Crash;
}
const name_text = self.env.getIdent(tag.name);
var tag_list = std.array_list.AlignedManaged(types.Tag, null).init(self.allocator);
defer tag_list.deinit();
@ -1456,20 +1532,30 @@ pub const Interpreter = struct {
out.is_initialized = true;
return out;
}
return error.NotImplemented;
self.triggerCrash("DEBUG: e_tag scalar not int", false, roc_ops);
return error.Crash;
} else if (layout_val.tag == .record) {
// Has payload: record { tag, payload }
var dest = try self.pushRaw(layout_val, 0);
var acc = try dest.asRecord(&self.runtime_layout_store);
const tag_field_idx = acc.findFieldIndex(self.env, "tag") orelse return error.NotImplemented;
const payload_field_idx = acc.findFieldIndex(self.env, "payload") orelse return error.NotImplemented;
const tag_field_idx = acc.findFieldIndex(self.env, "tag") orelse {
self.triggerCrash("DEBUG: e_tag tag field not found", false, roc_ops);
return error.Crash;
};
const payload_field_idx = acc.findFieldIndex(self.env, "payload") orelse {
self.triggerCrash("DEBUG: e_tag payload field not found", false, roc_ops);
return error.Crash;
};
// write tag discriminant
const tag_field = try acc.getFieldByIndex(tag_field_idx);
if (tag_field.layout.tag == .scalar and tag_field.layout.data.scalar.tag == .int) {
var tmp = tag_field;
tmp.is_initialized = false;
try tmp.setInt(@intCast(tag_index));
} else return error.NotImplemented;
} else {
self.triggerCrash("DEBUG: e_tag tag field not int", false, roc_ops);
return error.Crash;
}
const args_exprs = self.env.store.sliceExpr(tag.args);
const arg_vars_range = tag_list.items[tag_index].args;
@ -1544,7 +1630,10 @@ pub const Interpreter = struct {
var tmp = tag_field;
tmp.is_initialized = false;
try tmp.setInt(@intCast(tag_index));
} else return error.NotImplemented;
} else {
self.triggerCrash("DEBUG: e_tag (nullable) tag field not int", false, roc_ops);
return error.Crash;
}
const args_exprs = self.env.store.sliceExpr(tag.args);
const arg_vars_range = tag_list.items[tag_index].args;
@ -1676,7 +1765,8 @@ pub const Interpreter = struct {
return dest;
}
}
return error.NotImplemented;
self.triggerCrash("DEBUG: e_tag layout not implemented", false, roc_ops);
return error.Crash;
},
.e_match => |m| {
// Evaluate scrutinee once and protect from stack corruption
@ -1777,7 +1867,32 @@ pub const Interpreter = struct {
};
const closure_layout = try self.getRuntimeLayout(rt_var);
// Expect a closure layout from type-to-layout translation
if (closure_layout.tag != .closure) return error.NotImplemented;
if (closure_layout.tag != .closure) {
// Debug: print what type we got instead
const resolved = self.runtime_types.resolveVar(rt_var);
const ct_var_debug = can.ModuleEnv.varFrom(expr_idx);
const ct_resolved = self.env.types.resolveVar(ct_var_debug);
// Build a message with the expression index
var msg_buf: [256]u8 = undefined;
const expr_idx_int = @intFromEnum(expr_idx);
const types_len = self.env.types.len();
const msg = switch (ct_resolved.desc.content) {
.flex => std.fmt.bufPrint(&msg_buf, "e_lambda: type is FLEX, expr_idx={}, types.len={}", .{ expr_idx_int, types_len }) catch "e_lambda: FLEX",
.rigid => std.fmt.bufPrint(&msg_buf, "e_lambda: type is RIGID, expr_idx={}, types.len={}", .{ expr_idx_int, types_len }) catch "e_lambda: RIGID",
.structure => |s| switch (s) {
.fn_pure => "e_lambda: type is fn_pure (should work!)",
.fn_effectful => "e_lambda: type is fn_effectful (should work!)",
.fn_unbound => "e_lambda: type is fn_unbound",
else => std.fmt.bufPrint(&msg_buf, "e_lambda: type is structure, expr_idx={}, types.len={}", .{ expr_idx_int, types_len }) catch "e_lambda: structure",
},
.alias => "e_lambda: type is alias",
.recursion_var => "e_lambda: type is recursion_var",
.err => std.fmt.bufPrint(&msg_buf, "e_lambda: type is ERROR, expr_idx={}, types.len={}", .{ expr_idx_int, types_len }) catch "e_lambda: ERROR",
};
_ = resolved;
self.triggerCrash(msg, false, roc_ops);
return error.Crash;
}
const value = try self.pushRaw(closure_layout, 0);
self.registerDefValue(expr_idx, value);
// Initialize the closure header
@ -1860,7 +1975,10 @@ pub const Interpreter = struct {
.e_closure => |cls| {
// Build a closure value with concrete captures. The closure references a lambda.
const lam_expr = self.env.store.getExpr(cls.lambda_idx);
if (lam_expr != .e_lambda) return error.NotImplemented;
if (lam_expr != .e_lambda) {
self.triggerCrash("DEBUG: e_closure expr not lambda", false, roc_ops);
return error.Crash;
}
const lam = lam_expr.e_lambda;
// Collect capture layouts and names from current bindings
@ -1975,8 +2093,12 @@ pub const Interpreter = struct {
var accessor = try rec_val.asRecord(&self.runtime_layout_store);
for (caps) |cap_idx2| {
const cap2 = self.env.store.getCapture(cap_idx2);
const cap_val2 = resolveCapture(self, cap2, roc_ops) orelse return error.NotImplemented;
const idx_opt = accessor.findFieldIndex(self.env, self.env.getIdent(cap2.name)) orelse return error.NotImplemented;
const cap_val2 = resolveCapture(self, cap2, roc_ops) orelse {
return error.NotImplemented;
};
const idx_opt = accessor.findFieldIndex(self.env, self.env.getIdent(cap2.name)) orelse {
return error.NotImplemented;
};
try accessor.setFieldByIndex(idx_opt, cap_val2, roc_ops);
}
}
@ -2231,7 +2353,8 @@ pub const Interpreter = struct {
return try self.evalExprMinimal(lambda.body, roc_ops, null);
}
return error.NotImplemented;
self.triggerCrash("DEBUG: e_call NotImplemented", false, roc_ops);
return error.Crash;
},
.e_dot_access => |dot_access| {
const receiver_ct_var = can.ModuleEnv.varFrom(dot_access.receiver);
@ -2553,12 +2676,55 @@ pub const Interpreter = struct {
}
}
return error.NotImplemented;
self.triggerCrash("DEBUG: e_lookup_local not found", false, roc_ops);
return error.Crash;
},
.e_lookup_external => |lookup| {
// Cross-module reference - look up in imported module
const other_env = self.import_envs.get(lookup.module_idx) orelse {
return error.NotImplemented;
const other_env = self.import_envs.get(lookup.module_idx) orelse blk: {
// Fallback: dynamic lookup by name
// This is needed when the current module (self.env) has imports in a different order
// than the root module, so the Import.Idx doesn't match what was populated in init().
// We need to get the module name from the import list using the Import.Idx.
if (self.env.imports.map.count() > @intFromEnum(lookup.module_idx)) {
// Retrieve the interned string index for this import
const import_list = self.env.imports.imports.items.items;
if (@intFromEnum(lookup.module_idx) < import_list.len) {
const str_idx = import_list[@intFromEnum(lookup.module_idx)];
const import_name = self.env.common.getString(str_idx);
// Try to find it in imported_modules
// First try exact match
if (self.imported_modules.get(import_name)) |env| {
break :blk env;
}
// Try stripping .roc if present
if (std.mem.endsWith(u8, import_name, ".roc")) {
const short = import_name[0 .. import_name.len - 4];
if (self.imported_modules.get(short)) |env| {
break :blk env;
}
}
// Try extracting module name from "pf.Module"
if (std.mem.lastIndexOf(u8, import_name, ".")) |dot_idx| {
const short = import_name[dot_idx + 1 ..];
if (self.imported_modules.get(short)) |env| {
break :blk env;
}
}
self.triggerCrash("DEBUG: Failed to resolve import in imported_modules", false, roc_ops);
return error.Crash;
} else {
self.triggerCrash("DEBUG: lookup.module_idx >= import_list.len", false, roc_ops);
return error.Crash;
}
} else {
self.triggerCrash("DEBUG: lookup.module_idx >= map.count", false, roc_ops);
return error.Crash;
}
};
// The target_node_idx is a Def.Idx in the other module
@ -2588,7 +2754,10 @@ pub const Interpreter = struct {
},
// no if handling in minimal evaluator
// no second e_binop case; handled above
else => return error.NotImplemented,
else => {
self.triggerCrash("DEBUG: evalExprMinimal catch-all NotImplemented", false, roc_ops);
return error.Crash;
},
}
}
@ -3828,8 +3997,12 @@ pub const Interpreter = struct {
// Record { tag, payload }
var dest = try self.pushRaw(result_layout, 0);
var acc = try dest.asRecord(&self.runtime_layout_store);
const tag_field_idx = acc.findFieldIndex(self.env, "tag") orelse return error.NotImplemented;
const payload_field_idx = acc.findFieldIndex(self.env, "payload") orelse return error.NotImplemented;
const tag_field_idx = acc.findFieldIndex(self.env, "tag") orelse {
return error.NotImplemented;
};
const payload_field_idx = acc.findFieldIndex(self.env, "payload") orelse {
return error.NotImplemented;
};
// Write tag discriminant
const tag_field = try acc.getFieldByIndex(tag_field_idx);
@ -3838,7 +4011,10 @@ pub const Interpreter = struct {
tmp.is_initialized = false;
const tag_idx: usize = if (in_range) ok_index orelse 0 else err_index orelse 1;
try tmp.setInt(@intCast(tag_idx));
} else return error.NotImplemented;
} else {
self.triggerCrash("DEBUG: callLowLevelBuiltin tag not int (1)", false, roc_ops);
return error.Crash;
}
// Clear payload area
const payload_field = try acc.getFieldByIndex(payload_field_idx);
@ -4154,8 +4330,12 @@ pub const Interpreter = struct {
var dest = try self.pushRaw(result_layout, 0);
var result_acc = try dest.asRecord(&self.runtime_layout_store);
// Use layout_env for field lookups since record fields use layout store's env idents
const tag_field_idx = result_acc.findFieldIndex(layout_env, "tag") orelse return error.NotImplemented;
const payload_field_idx = result_acc.findFieldIndex(layout_env, "payload") orelse return error.NotImplemented;
const tag_field_idx = result_acc.findFieldIndex(layout_env, "tag") orelse {
return error.NotImplemented;
};
const payload_field_idx = result_acc.findFieldIndex(layout_env, "payload") orelse {
return error.NotImplemented;
};
// Write tag discriminant
const tag_field = try result_acc.getFieldByIndex(tag_field_idx);
@ -4164,7 +4344,10 @@ pub const Interpreter = struct {
tmp.is_initialized = false;
const tag_idx: usize = if (in_range) ok_index orelse 0 else err_index orelse 1;
try tmp.setInt(@intCast(tag_idx));
} else return error.NotImplemented;
} else {
self.triggerCrash("DEBUG: callLowLevelBuiltin tag not int (2)", false, roc_ops);
return error.Crash;
}
// Clear payload area
const payload_field = try result_acc.getFieldByIndex(payload_field_idx);
@ -4399,7 +4582,10 @@ pub const Interpreter = struct {
tmp.is_initialized = false;
const tag_idx: usize = if (in_range) ok_index orelse 0 else err_index orelse 1;
try tmp.setInt(@intCast(tag_idx));
} else return error.NotImplemented;
} else {
self.triggerCrash("DEBUG: callLowLevelBuiltin tag not int (3)", false, roc_ops);
return error.Crash;
}
// Clear payload area (element 0)
const payload_field = try result_acc.getElement(0);
@ -4779,7 +4965,9 @@ pub const Interpreter = struct {
if (rhs_dec.num == 0) return error.DivisionByZero;
break :blk RocDec{ .num = @rem(lhs_dec.num, rhs_dec.num) };
},
else => return error.NotImplemented,
else => {
return error.NotImplemented;
},
};
var out = try self.pushRaw(result_layout, 0);
@ -4819,7 +5007,9 @@ pub const Interpreter = struct {
if (rhs_float == 0) return error.DivisionByZero;
break :blk @rem(lhs_float, rhs_float);
},
else => return error.NotImplemented,
else => {
return error.NotImplemented;
},
};
var out = try self.pushRaw(result_layout, 0);
@ -5033,14 +5223,18 @@ pub const Interpreter = struct {
const rhs_str: *const RocStr = @ptrCast(@alignCast(rhs.ptr.?));
return std.mem.eql(u8, lhs_str.asSlice(), rhs_str.asSlice());
},
else => return error.NotImplemented,
else => {
return error.NotImplemented;
},
}
}
// Ensure runtime vars resolve to the same descriptor before structural comparison.
const lhs_resolved = self.resolveBaseVar(lhs_var);
const lhs_content = lhs_resolved.desc.content;
if (lhs_content != .structure) return error.NotImplemented;
if (lhs_content != .structure) {
return error.NotImplemented;
}
return switch (lhs_content.structure) {
.tuple => |tuple| {
@ -5059,7 +5253,9 @@ pub const Interpreter = struct {
// For nominal types, dispatch to their is_eq method
return try self.dispatchNominalIsEq(lhs, rhs, nom, lhs_var);
},
.record_unbound, .fn_pure, .fn_effectful, .fn_unbound => error.NotImplemented,
.record_unbound, .fn_pure, .fn_effectful, .fn_unbound => {
return error.NotImplemented;
},
};
}
@ -5266,13 +5462,16 @@ pub const Interpreter = struct {
// For other cases, fall back to attempting scalar comparison
// This handles cases like Bool which wraps a tag union but is represented as a scalar
if (lhs.layout.tag == .scalar and rhs.layout.tag == .scalar) {
const order = self.compareNumericScalars(lhs, rhs) catch return error.NotImplemented;
const order = self.compareNumericScalars(lhs, rhs) catch {
return error.NotImplemented;
};
return order == .eq;
}
// Can't compare - likely a user-defined nominal type that needs is_eq dispatch
// TODO: Implement proper method dispatch by looking up is_eq in the nominal type's module
_ = lhs_var;
return error.NotImplemented;
}
@ -6115,7 +6314,8 @@ pub const Interpreter = struct {
const result = self.valuesStructurallyEqual(lhs, lhs_rt_var, rhs, rhs_rt_var) catch |err| {
// If structural equality is not implemented for this type, return false
if (err == error.NotImplemented) {
return try self.makeBoolValue(false);
self.triggerCrash("DEBUG: dispatchBinaryOpMethod NotImplemented", false, roc_ops);
return error.Crash;
}
return err;
};

View file

@ -971,6 +971,7 @@ fn compileSource(source: []const u8) !CompilerStageData {
.all_defs = serialized_ptr.all_defs,
.all_statements = serialized_ptr.all_statements,
.exports = serialized_ptr.exports,
.requires_types = serialized_ptr.requires_types.deserialize(@as(i64, @intCast(base_ptr))).*,
.builtin_statements = serialized_ptr.builtin_statements,
.external_decls = serialized_ptr.external_decls.deserialize(@as(i64, @intCast(base_ptr))).*,
.imports = (try serialized_ptr.imports.deserialize(@as(i64, @intCast(base_ptr)), gpa)).*,

View file

@ -80,6 +80,7 @@ fn loadCompiledModule(gpa: std.mem.Allocator, bin_data: []const u8, module_name:
.all_defs = serialized_ptr.all_defs,
.all_statements = serialized_ptr.all_statements,
.exports = serialized_ptr.exports,
.requires_types = serialized_ptr.requires_types.deserialize(@as(i64, @intCast(base_ptr))).*,
.builtin_statements = serialized_ptr.builtin_statements,
.external_decls = serialized_ptr.external_decls.deserialize(@as(i64, @intCast(base_ptr))).*,
.imports = (try serialized_ptr.imports.deserialize(@as(i64, @intCast(base_ptr)), gpa)).*,

View file

@ -3,10 +3,13 @@ app [main!] { pf: platform "./platform/main.roc" }
import pf.Stdout
import pf.Stderr
str : Str -> Str
str = |s| s
main! = || {
Stdout.line!("Hello from stdout!")
Stdout.line!("Line 1 to stdout")
Stderr.line!("Line 2 to stderr")
Stdout.line!("Line 3 to stdout")
Stderr.line!("Error from stderr!")
Stdout.line!(str("Hello from stdout!"))
Stdout.line!(str("Line 1 to stdout"))
Stderr.line!(str("Line 2 to stderr"))
Stdout.line!(str("Line 3 to stdout"))
Stderr.line!(str("Error from stderr!"))
}

View file

@ -0,0 +1,13 @@
app [main!] { pf: platform "./platform/main.roc" }
import pf.Stdout
str : Str -> Str
str = |s| s
main! = || {
x = match 0 {
_ => str("0")
}
Stdout.line!(x)
}

View file

@ -3,7 +3,10 @@ app [main!] { pf: platform "./platform/main.roc" }
import pf.Stdin
import pf.Stdout
str : Str -> Str
str = |s| s
main! = || {
line = Stdin.line!()
Stdout.line!(line)
Stdout.line!(str(line))
}

View file

@ -3,7 +3,10 @@ app [main!] { pf: platform "./platform/main.roc" }
import pf.Stdin
import pf.Stderr
str : Str -> Str
str = |s| s
main! = || {
line = Stdin.line!()
Stderr.line!(line)
Stderr.line!(str(line))
}

View file

@ -3,8 +3,11 @@ app [main!] { pf: platform "./platform/main.roc" }
import pf.Stdout
import pf.Stdin
str : Str -> Str
str = |s| s
main! = || {
Stdout.line!("Before stdin")
Stdout.line!(str("Before stdin"))
Stdin.line!()
Stdout.line!("After stdin")
Stdout.line!(str("After stdin"))
}

View file

@ -3,4 +3,10 @@ app [main!] { pf: platform "./platform/main.roc" }
import pf.Stdin
import pf.Stdout
main! = || Stdout.line!(Stdin.line!())
str : Str -> Str
str = |s| s
main! = || {
line = Stdin.line!()
Stdout.line!(str(line))
}

View file

@ -0,0 +1,7 @@
app [main!] { pf: platform "./platform/main.roc" }
import pf.Stdout
main! = || {
Stdout.line!("Hello")
}

View file

@ -0,0 +1,6 @@
app [main!] { pf: platform "./platform/main.roc" }
main! : () => {}
main! = || {
"hello"
}

11
test/fx/test_one_call.roc Normal file
View file

@ -0,0 +1,11 @@
app [main!] { pf: platform "./platform/main.roc" }
import pf.Stdout
identity : a -> a
identity = |x| x
main! = || {
str = identity("Hello")
Stdout.line!(str)
}

View file

@ -0,0 +1,5 @@
app [main!] { pf: platform "./platform/main.roc" }
main! = || {
"hello"
}

View file

@ -0,0 +1,10 @@
app [main!] { pf: platform "./platform/main.roc" }
import pf.Stdout
str : Str -> Str
str = |s| s
main! = || {
Stdout.line!(str("Hello"))
}

View file

@ -31,7 +31,7 @@ const expected_safelist_u8_size = 24;
const expected_safelist_u32_size = 24;
const expected_safemultilist_teststruct_size = 24;
const expected_safemultilist_node_size = 24;
const expected_moduleenv_size = 712; // Platform-independent size
const expected_moduleenv_size = 736; // Platform-independent size
const expected_nodestore_size = 96; // Platform-independent size
// Compile-time assertions - build will fail if sizes don't match expected values