mirror of
https://github.com/roc-lang/roc.git
synced 2025-12-23 08:48:03 +00:00
Merge branch 'main' into builtin-str-impls2
This commit is contained in:
commit
ac18944e72
29 changed files with 745 additions and 323 deletions
|
|
@ -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))
|
||||
|
|
@ -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")
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
package
|
||||
[
|
||||
Bool,
|
||||
Try,
|
||||
]
|
||||
{}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)).*,
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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)).*,
|
||||
|
|
|
|||
|
|
@ -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..]);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// ------------------------
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)).*,
|
||||
|
|
|
|||
|
|
@ -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)).*,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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)).*,
|
||||
|
|
|
|||
|
|
@ -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)).*,
|
||||
|
|
|
|||
|
|
@ -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!"))
|
||||
}
|
||||
|
|
|
|||
13
test/fx/match_str_return.roc
Normal file
13
test/fx/match_str_return.roc
Normal 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)
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
7
test/fx/test_direct_string.roc
Normal file
7
test/fx/test_direct_string.roc
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
app [main!] { pf: platform "./platform/main.roc" }
|
||||
|
||||
import pf.Stdout
|
||||
|
||||
main! = || {
|
||||
Stdout.line!("Hello")
|
||||
}
|
||||
6
test/fx/test_explicit.roc
Normal file
6
test/fx/test_explicit.roc
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
app [main!] { pf: platform "./platform/main.roc" }
|
||||
|
||||
main! : () => {}
|
||||
main! = || {
|
||||
"hello"
|
||||
}
|
||||
11
test/fx/test_one_call.roc
Normal file
11
test/fx/test_one_call.roc
Normal 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)
|
||||
}
|
||||
5
test/fx/test_type_mismatch.roc
Normal file
5
test/fx/test_type_mismatch.roc
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
app [main!] { pf: platform "./platform/main.roc" }
|
||||
|
||||
main! = || {
|
||||
"hello"
|
||||
}
|
||||
10
test/fx/test_with_wrapper.roc
Normal file
10
test/fx/test_with_wrapper.roc
Normal 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"))
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue