roc/src/canonicalize/ModuleEnv.zig

2930 lines
136 KiB
Zig

//! The common state for a module: any data useful over the full lifetime of its compilation that lives beyond individual IR's.
//!
//! Stores all interned data like idents, strings, and problems.
//!
//! This reduces the size of this module's IRs as they can store references to this
//! interned (and deduplicated) data instead of storing the values themselves.
const std = @import("std");
const builtin = @import("builtin");
const types_mod = @import("types");
const collections = @import("collections");
const base = @import("base");
const Node = @import("Node.zig");
const NodeStore = @import("NodeStore.zig");
const CIR = @import("CIR.zig");
const DependencyGraph = @import("DependencyGraph.zig");
const TypeWriter = types_mod.TypeWriter;
const CompactWriter = collections.CompactWriter;
const SortedArrayBuilder = collections.SortedArrayBuilder;
const CommonEnv = base.CommonEnv;
const Ident = base.Ident;
const StringLiteral = base.StringLiteral;
const RegionInfo = base.RegionInfo;
const Region = base.Region;
const SExprTree = base.SExprTree;
const SExpr = base.SExpr;
const TypeVar = types_mod.Var;
const TypeStore = types_mod.Store;
const Self = @This();
/// The kind of module being canonicalized, set during header processing
pub const ModuleKind = union(enum) {
type_module: Ident.Idx, // Holds the main type identifier for type modules
default_app,
app,
package,
platform,
hosted,
deprecated_module,
malformed,
/// Extern-compatible tag for serialization
pub const Tag = enum(u32) {
type_module,
default_app,
app,
package,
platform,
hosted,
deprecated_module,
malformed,
};
/// Extern-compatible payload union for serialization
pub const Payload = extern union {
type_module_ident: Ident.Idx,
none: u32,
};
/// Extern-compatible serialized form
pub const Serialized = extern struct {
tag: Tag,
payload: Payload,
pub fn encode(kind: ModuleKind) @This() {
return switch (kind) {
.type_module => |idx| .{ .tag = .type_module, .payload = .{ .type_module_ident = idx } },
.default_app => .{ .tag = .default_app, .payload = .{ .none = 0 } },
.app => .{ .tag = .app, .payload = .{ .none = 0 } },
.package => .{ .tag = .package, .payload = .{ .none = 0 } },
.platform => .{ .tag = .platform, .payload = .{ .none = 0 } },
.hosted => .{ .tag = .hosted, .payload = .{ .none = 0 } },
.deprecated_module => .{ .tag = .deprecated_module, .payload = .{ .none = 0 } },
.malformed => .{ .tag = .malformed, .payload = .{ .none = 0 } },
};
}
pub fn decode(self: @This()) ModuleKind {
return switch (self.tag) {
.type_module => .{ .type_module = self.payload.type_module_ident },
.default_app => .default_app,
.app => .app,
.package => .package,
.platform => .platform,
.hosted => .hosted,
.deprecated_module => .deprecated_module,
.malformed => .malformed,
};
}
};
};
/// Well-known identifiers that are interned once and reused throughout compilation.
/// These are needed for type checking, operator desugaring, and layout generation.
/// This is an extern struct so it can be embedded in serialized ModuleEnv.
pub const CommonIdents = extern struct {
// Method names for operator desugaring
from_int_digits: Ident.Idx,
from_dec_digits: Ident.Idx,
plus: Ident.Idx,
minus: Ident.Idx,
times: Ident.Idx,
div_by: Ident.Idx,
div_trunc_by: Ident.Idx,
rem_by: Ident.Idx,
negate: Ident.Idx,
abs: Ident.Idx,
abs_diff: Ident.Idx,
not: Ident.Idx,
is_lt: Ident.Idx,
is_lte: Ident.Idx,
is_gt: Ident.Idx,
is_gte: Ident.Idx,
is_eq: Ident.Idx,
// Type/module names
@"try": Ident.Idx,
out_of_range: Ident.Idx,
builtin_module: Ident.Idx,
str: Ident.Idx,
list: Ident.Idx,
box: Ident.Idx,
// Unqualified builtin type names (for checking if a type name shadows a builtin)
num: Ident.Idx,
u8: Ident.Idx,
u16: Ident.Idx,
u32: Ident.Idx,
u64: Ident.Idx,
u128: Ident.Idx,
i8: Ident.Idx,
i16: Ident.Idx,
i32: Ident.Idx,
i64: Ident.Idx,
i128: Ident.Idx,
f32: Ident.Idx,
f64: Ident.Idx,
dec: Ident.Idx,
// Fully-qualified type identifiers for type checking and layout generation
builtin_try: Ident.Idx,
builtin_numeral: Ident.Idx,
builtin_str: Ident.Idx,
u8_type: Ident.Idx,
i8_type: Ident.Idx,
u16_type: Ident.Idx,
i16_type: Ident.Idx,
u32_type: Ident.Idx,
i32_type: Ident.Idx,
u64_type: Ident.Idx,
i64_type: Ident.Idx,
u128_type: Ident.Idx,
i128_type: Ident.Idx,
f32_type: Ident.Idx,
f64_type: Ident.Idx,
dec_type: Ident.Idx,
// Field/tag names used during type checking and evaluation
before_dot: Ident.Idx,
after_dot: Ident.Idx,
provided_by_compiler: Ident.Idx,
tag: Ident.Idx,
payload: Ident.Idx,
is_negative: Ident.Idx,
digits_before_pt: Ident.Idx,
digits_after_pt: Ident.Idx,
box_method: Ident.Idx,
unbox_method: Ident.Idx,
// Fully qualified Box intrinsic method names
builtin_box_box: Ident.Idx,
builtin_box_unbox: Ident.Idx,
to_inspect: Ident.Idx,
ok: Ident.Idx,
err: Ident.Idx,
from_numeral: Ident.Idx,
true_tag: Ident.Idx,
false_tag: Ident.Idx,
// from_utf8 result fields
byte_index: Ident.Idx,
string: Ident.Idx,
is_ok: Ident.Idx,
problem_code: Ident.Idx,
// from_utf8 error payload fields (BadUtf8 record)
problem: Ident.Idx,
index: Ident.Idx,
// Synthetic identifiers for ? operator desugaring
question_ok: Ident.Idx,
question_err: Ident.Idx,
/// Insert all well-known identifiers into a CommonEnv.
/// Use this when creating a fresh ModuleEnv from scratch.
pub fn insert(gpa: std.mem.Allocator, common: *CommonEnv) std.mem.Allocator.Error!CommonIdents {
return .{
.from_int_digits = try common.insertIdent(gpa, Ident.for_text(Ident.FROM_INT_DIGITS_METHOD_NAME)),
.from_dec_digits = try common.insertIdent(gpa, Ident.for_text(Ident.FROM_DEC_DIGITS_METHOD_NAME)),
.plus = try common.insertIdent(gpa, Ident.for_text(Ident.PLUS_METHOD_NAME)),
.minus = try common.insertIdent(gpa, Ident.for_text("minus")),
.times = try common.insertIdent(gpa, Ident.for_text("times")),
.div_by = try common.insertIdent(gpa, Ident.for_text("div_by")),
.div_trunc_by = try common.insertIdent(gpa, Ident.for_text("div_trunc_by")),
.rem_by = try common.insertIdent(gpa, Ident.for_text("rem_by")),
.negate = try common.insertIdent(gpa, Ident.for_text(Ident.NEGATE_METHOD_NAME)),
.abs = try common.insertIdent(gpa, Ident.for_text("abs")),
.abs_diff = try common.insertIdent(gpa, Ident.for_text("abs_diff")),
.not = try common.insertIdent(gpa, Ident.for_text("not")),
.is_lt = try common.insertIdent(gpa, Ident.for_text("is_lt")),
.is_lte = try common.insertIdent(gpa, Ident.for_text("is_lte")),
.is_gt = try common.insertIdent(gpa, Ident.for_text("is_gt")),
.is_gte = try common.insertIdent(gpa, Ident.for_text("is_gte")),
.is_eq = try common.insertIdent(gpa, Ident.for_text("is_eq")),
.@"try" = try common.insertIdent(gpa, Ident.for_text("Try")),
.out_of_range = try common.insertIdent(gpa, Ident.for_text("OutOfRange")),
.builtin_module = try common.insertIdent(gpa, Ident.for_text("Builtin")),
.str = try common.insertIdent(gpa, Ident.for_text("Str")),
.list = try common.insertIdent(gpa, Ident.for_text("List")),
.box = try common.insertIdent(gpa, Ident.for_text("Box")),
// Unqualified builtin type names
.num = try common.insertIdent(gpa, Ident.for_text("Num")),
.u8 = try common.insertIdent(gpa, Ident.for_text("U8")),
.u16 = try common.insertIdent(gpa, Ident.for_text("U16")),
.u32 = try common.insertIdent(gpa, Ident.for_text("U32")),
.u64 = try common.insertIdent(gpa, Ident.for_text("U64")),
.u128 = try common.insertIdent(gpa, Ident.for_text("U128")),
.i8 = try common.insertIdent(gpa, Ident.for_text("I8")),
.i16 = try common.insertIdent(gpa, Ident.for_text("I16")),
.i32 = try common.insertIdent(gpa, Ident.for_text("I32")),
.i64 = try common.insertIdent(gpa, Ident.for_text("I64")),
.i128 = try common.insertIdent(gpa, Ident.for_text("I128")),
.f32 = try common.insertIdent(gpa, Ident.for_text("F32")),
.f64 = try common.insertIdent(gpa, Ident.for_text("F64")),
.dec = try common.insertIdent(gpa, Ident.for_text("Dec")),
.builtin_try = try common.insertIdent(gpa, Ident.for_text("Try")),
.builtin_numeral = try common.insertIdent(gpa, Ident.for_text("Num.Numeral")),
.builtin_str = try common.insertIdent(gpa, Ident.for_text("Builtin.Str")),
.u8_type = try common.insertIdent(gpa, Ident.for_text("Builtin.Num.U8")),
.i8_type = try common.insertIdent(gpa, Ident.for_text("Builtin.Num.I8")),
.u16_type = try common.insertIdent(gpa, Ident.for_text("Builtin.Num.U16")),
.i16_type = try common.insertIdent(gpa, Ident.for_text("Builtin.Num.I16")),
.u32_type = try common.insertIdent(gpa, Ident.for_text("Builtin.Num.U32")),
.i32_type = try common.insertIdent(gpa, Ident.for_text("Builtin.Num.I32")),
.u64_type = try common.insertIdent(gpa, Ident.for_text("Builtin.Num.U64")),
.i64_type = try common.insertIdent(gpa, Ident.for_text("Builtin.Num.I64")),
.u128_type = try common.insertIdent(gpa, Ident.for_text("Builtin.Num.U128")),
.i128_type = try common.insertIdent(gpa, Ident.for_text("Builtin.Num.I128")),
.f32_type = try common.insertIdent(gpa, Ident.for_text("Builtin.Num.F32")),
.f64_type = try common.insertIdent(gpa, Ident.for_text("Builtin.Num.F64")),
.dec_type = try common.insertIdent(gpa, Ident.for_text("Builtin.Num.Dec")),
.before_dot = try common.insertIdent(gpa, Ident.for_text("before_dot")),
.after_dot = try common.insertIdent(gpa, Ident.for_text("after_dot")),
.provided_by_compiler = try common.insertIdent(gpa, Ident.for_text("ProvidedByCompiler")),
.tag = try common.insertIdent(gpa, Ident.for_text("tag")),
.payload = try common.insertIdent(gpa, Ident.for_text("payload")),
.is_negative = try common.insertIdent(gpa, Ident.for_text("is_negative")),
.digits_before_pt = try common.insertIdent(gpa, Ident.for_text("digits_before_pt")),
.digits_after_pt = try common.insertIdent(gpa, Ident.for_text("digits_after_pt")),
.box_method = try common.insertIdent(gpa, Ident.for_text("box")),
.unbox_method = try common.insertIdent(gpa, Ident.for_text("unbox")),
// Fully qualified Box intrinsic method names
.builtin_box_box = try common.insertIdent(gpa, Ident.for_text("Builtin.Box.box")),
.builtin_box_unbox = try common.insertIdent(gpa, Ident.for_text("Builtin.Box.unbox")),
.to_inspect = try common.insertIdent(gpa, Ident.for_text("to_inspect")),
.ok = try common.insertIdent(gpa, Ident.for_text("Ok")),
.err = try common.insertIdent(gpa, Ident.for_text("Err")),
.from_numeral = try common.insertIdent(gpa, Ident.for_text("from_numeral")),
.true_tag = try common.insertIdent(gpa, Ident.for_text("True")),
.false_tag = try common.insertIdent(gpa, Ident.for_text("False")),
// from_utf8 result fields
.byte_index = try common.insertIdent(gpa, Ident.for_text("byte_index")),
.string = try common.insertIdent(gpa, Ident.for_text("string")),
.is_ok = try common.insertIdent(gpa, Ident.for_text("is_ok")),
.problem_code = try common.insertIdent(gpa, Ident.for_text("problem_code")),
// from_utf8 error payload fields (BadUtf8 record)
.problem = try common.insertIdent(gpa, Ident.for_text("problem")),
.index = try common.insertIdent(gpa, Ident.for_text("index")),
// Synthetic identifiers for ? operator desugaring
.question_ok = try common.insertIdent(gpa, Ident.for_text("#ok")),
.question_err = try common.insertIdent(gpa, Ident.for_text("#err")),
};
}
/// Find all well-known identifiers in a CommonEnv that has already interned them.
/// Use this when loading a pre-compiled module where identifiers are already present.
/// Panics if any identifier is not found (indicates corrupted/incompatible pre-compiled data).
pub fn find(common: *const CommonEnv) CommonIdents {
return .{
.from_int_digits = common.findIdent(Ident.FROM_INT_DIGITS_METHOD_NAME) orelse unreachable,
.from_dec_digits = common.findIdent(Ident.FROM_DEC_DIGITS_METHOD_NAME) orelse unreachable,
.plus = common.findIdent(Ident.PLUS_METHOD_NAME) orelse unreachable,
.minus = common.findIdent("minus") orelse unreachable,
.times = common.findIdent("times") orelse unreachable,
.div_by = common.findIdent("div_by") orelse unreachable,
.div_trunc_by = common.findIdent("div_trunc_by") orelse unreachable,
.rem_by = common.findIdent("rem_by") orelse unreachable,
.negate = common.findIdent(Ident.NEGATE_METHOD_NAME) orelse unreachable,
.abs = common.findIdent("abs") orelse unreachable,
.abs_diff = common.findIdent("abs_diff") orelse unreachable,
.not = common.findIdent("not") orelse unreachable,
.is_lt = common.findIdent("is_lt") orelse unreachable,
.is_lte = common.findIdent("is_lte") orelse unreachable,
.is_gt = common.findIdent("is_gt") orelse unreachable,
.is_gte = common.findIdent("is_gte") orelse unreachable,
.is_eq = common.findIdent("is_eq") orelse unreachable,
.@"try" = common.findIdent("Try") orelse unreachable,
.out_of_range = common.findIdent("OutOfRange") orelse unreachable,
.builtin_module = common.findIdent("Builtin") orelse unreachable,
.str = common.findIdent("Str") orelse unreachable,
.list = common.findIdent("List") orelse unreachable,
.box = common.findIdent("Box") orelse unreachable,
// Unqualified builtin type names
.num = common.findIdent("Num") orelse unreachable,
.u8 = common.findIdent("U8") orelse unreachable,
.u16 = common.findIdent("U16") orelse unreachable,
.u32 = common.findIdent("U32") orelse unreachable,
.u64 = common.findIdent("U64") orelse unreachable,
.u128 = common.findIdent("U128") orelse unreachable,
.i8 = common.findIdent("I8") orelse unreachable,
.i16 = common.findIdent("I16") orelse unreachable,
.i32 = common.findIdent("I32") orelse unreachable,
.i64 = common.findIdent("I64") orelse unreachable,
.i128 = common.findIdent("I128") orelse unreachable,
.f32 = common.findIdent("F32") orelse unreachable,
.f64 = common.findIdent("F64") orelse unreachable,
.dec = common.findIdent("Dec") orelse unreachable,
.builtin_try = common.findIdent("Try") orelse unreachable,
.builtin_numeral = common.findIdent("Num.Numeral") orelse unreachable,
.builtin_str = common.findIdent("Builtin.Str") orelse unreachable,
.u8_type = common.findIdent("Builtin.Num.U8") orelse unreachable,
.i8_type = common.findIdent("Builtin.Num.I8") orelse unreachable,
.u16_type = common.findIdent("Builtin.Num.U16") orelse unreachable,
.i16_type = common.findIdent("Builtin.Num.I16") orelse unreachable,
.u32_type = common.findIdent("Builtin.Num.U32") orelse unreachable,
.i32_type = common.findIdent("Builtin.Num.I32") orelse unreachable,
.u64_type = common.findIdent("Builtin.Num.U64") orelse unreachable,
.i64_type = common.findIdent("Builtin.Num.I64") orelse unreachable,
.u128_type = common.findIdent("Builtin.Num.U128") orelse unreachable,
.i128_type = common.findIdent("Builtin.Num.I128") orelse unreachable,
.f32_type = common.findIdent("Builtin.Num.F32") orelse unreachable,
.f64_type = common.findIdent("Builtin.Num.F64") orelse unreachable,
.dec_type = common.findIdent("Builtin.Num.Dec") orelse unreachable,
.before_dot = common.findIdent("before_dot") orelse unreachable,
.after_dot = common.findIdent("after_dot") orelse unreachable,
.provided_by_compiler = common.findIdent("ProvidedByCompiler") orelse unreachable,
.tag = common.findIdent("tag") orelse unreachable,
.payload = common.findIdent("payload") orelse unreachable,
.is_negative = common.findIdent("is_negative") orelse unreachable,
.digits_before_pt = common.findIdent("digits_before_pt") orelse unreachable,
.digits_after_pt = common.findIdent("digits_after_pt") orelse unreachable,
.box_method = common.findIdent("box") orelse unreachable,
.unbox_method = common.findIdent("unbox") orelse unreachable,
// Fully qualified Box intrinsic method names
.builtin_box_box = common.findIdent("Builtin.Box.box") orelse unreachable,
.builtin_box_unbox = common.findIdent("Builtin.Box.unbox") orelse unreachable,
.to_inspect = common.findIdent("to_inspect") orelse unreachable,
.ok = common.findIdent("Ok") orelse unreachable,
.err = common.findIdent("Err") orelse unreachable,
.from_numeral = common.findIdent("from_numeral") orelse unreachable,
.true_tag = common.findIdent("True") orelse unreachable,
.false_tag = common.findIdent("False") orelse unreachable,
// from_utf8 result fields
.byte_index = common.findIdent("byte_index") orelse unreachable,
.string = common.findIdent("string") orelse unreachable,
.is_ok = common.findIdent("is_ok") orelse unreachable,
.problem_code = common.findIdent("problem_code") orelse unreachable,
// from_utf8 error payload fields (BadUtf8 record)
.problem = common.findIdent("problem") orelse unreachable,
.index = common.findIdent("index") orelse unreachable,
// Synthetic identifiers for ? operator desugaring
.question_ok = common.findIdent("#ok") orelse unreachable,
.question_err = common.findIdent("#err") orelse unreachable,
};
}
};
/// Key for method identifier lookup: (type_ident, method_ident) pair.
pub const MethodKey = packed struct(u64) {
type_ident: Ident.Idx,
method_ident: Ident.Idx,
};
/// Mapping from (type_ident, method_ident) pairs to their qualified method ident.
/// This enables O(log n) index-based method lookup instead of O(n) string comparison.
/// The value is the qualified method ident (e.g., "Bool.is_eq" for type "Bool" and method "is_eq").
///
/// This is populated during canonicalization when methods are defined in associated blocks.
pub const MethodIdents = SortedArrayBuilder(MethodKey, Ident.Idx);
gpa: std.mem.Allocator,
common: CommonEnv,
types: TypeStore,
// Module compilation fields
// NOTE: These fields are populated during canonicalization and preserved for later use
/// The kind of module (type_module, app, etc.) - set during canonicalization
module_kind: ModuleKind,
/// All the definitions in the module (populated by canonicalization)
all_defs: CIR.Def.Span,
/// All the top-level statements in the module (populated by canonicalization)
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,
/// Type alias mappings from for-clauses in requires declarations.
/// Stores (alias_name, rigid_name) pairs like (Model, model).
for_clause_aliases: ForClauseAlias.SafeList,
/// Rigid type variable mappings from platform for-clause after unification.
/// Maps rigid names (e.g., "model") to their resolved type variables in the app's type store.
/// Populated during checkPlatformRequirements when the platform has a for-clause.
rigid_vars: std.AutoHashMapUnmanaged(Ident.Idx, TypeVar),
/// All builtin stmts (temporary until module imports are working)
builtin_statements: CIR.Statement.Span,
/// Statements for for-clause type aliases (like Model : model) created during header processing.
/// These need to be processed by the type checker separately from all_statements.
for_clause_alias_statements: CIR.Statement.Span,
/// All external declarations referenced in this module
external_decls: CIR.ExternalDecl.SafeList,
/// Store for interned module imports
imports: CIR.Import.Store,
/// The module's name as a string
/// This is needed for import resolution to match import names to modules
module_name: []const u8,
/// The module's name as an interned identifier (for fast comparisons)
module_name_idx: Ident.Idx,
/// Diagnostics collected during canonicalization (optional)
diagnostics: CIR.Diagnostic.Span,
/// Stores the raw nodes which represent the intermediate representation
/// Uses an efficient data structure, and provides helpers for storing and retrieving nodes.
store: NodeStore,
/// Dependency analysis results (evaluation order for defs)
/// Set after canonicalization completes. Must not be accessed before then.
evaluation_order: ?*DependencyGraph.EvaluationOrder,
/// Well-known identifiers for type checking, operator desugaring, and layout generation.
/// Interned once during init to avoid repeated string comparisons.
idents: CommonIdents,
/// Deferred numeric literals collected during type checking
/// These will be validated during comptime evaluation
deferred_numeric_literals: DeferredNumericLiteral.SafeList,
/// Import mapping for type display names in error messages.
/// Maps fully-qualified type identifiers to their shortest display names based on imports.
/// Built during canonicalization when processing import statements.
/// Example: "MyModule.Foo" -> "F" if user has `import MyModule exposing [Foo as F]`
import_mapping: types_mod.import_mapping.ImportMapping,
/// Mapping from (type_ident, method_ident) pairs to qualified method idents.
/// Enables O(1) index-based method lookup during type checking and evaluation.
/// Populated during canonicalization when methods are defined in associated blocks.
method_idents: MethodIdents,
/// Deferred numeric literal for compile-time validation
pub const DeferredNumericLiteral = struct {
expr_idx: CIR.Expr.Idx,
type_var: TypeVar,
constraint: types_mod.StaticDispatchConstraint,
region: Region,
pub const SafeList = collections.SafeList(@This());
};
/// A type alias mapping from a for-clause: [Model : model]
/// Maps an alias name (Model) to a rigid variable name (model)
pub const ForClauseAlias = struct {
/// The alias name (e.g., "Model") - to be looked up in the app
alias_name: Ident.Idx,
/// The rigid variable name (e.g., "model") - the rigid in the required type
rigid_name: Ident.Idx,
/// The type annotation index of the rigid_var for this alias
rigid_anno_idx: CIR.TypeAnno.Idx,
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,
/// Type alias mappings from the for-clause (e.g., [Model : model])
/// These specify which app type aliases should be substituted for which rigids
type_aliases: ForClauseAlias.SafeList.Range,
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 {
// Relocate all sub-structures that contain pointers
self.common.relocate(offset);
self.types.relocate(offset);
self.external_decls.relocate(offset);
self.requires_types.relocate(offset);
self.for_clause_aliases.relocate(offset);
self.imports.relocate(offset);
self.store.relocate(offset);
self.deferred_numeric_literals.relocate(offset);
self.method_idents.relocate(offset);
// Relocate the module_name pointer if it's not empty
if (self.module_name.len > 0) {
const old_ptr = @intFromPtr(self.module_name.ptr);
const new_ptr = @as(isize, @intCast(old_ptr)) + offset;
self.module_name.ptr = @ptrFromInt(@as(usize, @intCast(new_ptr)));
}
}
/// Initialize the compilation fields in an existing ModuleEnv
pub fn initCIRFields(self: *Self, module_name: []const u8) !void {
self.module_kind = .deprecated_module; // Placeholder - set to actual kind during header canonicalization
self.all_defs = .{ .span = .{ .start = 0, .len = 0 } };
self.all_statements = .{ .span = .{ .start = 0, .len = 0 } };
self.exports = .{ .span = .{ .start = 0, .len = 0 } };
self.builtin_statements = .{ .span = .{ .start = 0, .len = 0 } };
self.for_clause_alias_statements = .{ .span = .{ .start = 0, .len = 0 } };
// Note: external_decls already exists from ModuleEnv.init(), so we don't create a new one
self.imports = CIR.Import.Store.init();
self.module_name = module_name;
self.module_name_idx = try self.insertIdent(Ident.for_text(module_name));
self.diagnostics = CIR.Diagnostic.Span{ .span = base.DataSpan{ .start = 0, .len = 0 } };
// Note: self.store already exists from ModuleEnv.init(), so we don't create a new one
self.evaluation_order = null; // Will be set after canonicalization completes
}
/// Alias for initCIRFields for backwards compatibility with tests
pub fn initModuleEnvFields(self: *Self, module_name: []const u8) !void {
return self.initCIRFields(module_name);
}
/// Initialize the module environment with capacity heuristics based on source size.
pub fn init(gpa: std.mem.Allocator, source: []const u8) std.mem.Allocator.Error!Self {
var common = try CommonEnv.init(gpa, source);
const idents = try CommonIdents.insert(gpa, &common);
// Use source-based heuristics for initial capacities
// Typical Roc code generates ~1 node per 20 bytes, ~1 type per 50 bytes
// Use generous minimums to avoid too many reallocations for small files
const source_len = source.len;
const node_capacity = @max(1024, @min(100_000, source_len / 20));
const type_capacity = @max(2048, @min(50_000, source_len / 50));
const var_capacity = @max(512, @min(10_000, source_len / 100));
return Self{
.gpa = gpa,
.common = common,
.types = try TypeStore.initCapacity(gpa, type_capacity, var_capacity),
.module_kind = .deprecated_module, // Placeholder - set to actual kind during header canonicalization
.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),
.for_clause_aliases = try ForClauseAlias.SafeList.initCapacity(gpa, 4),
.rigid_vars = std.AutoHashMapUnmanaged(Ident.Idx, TypeVar){},
.builtin_statements = .{ .span = .{ .start = 0, .len = 0 } },
.for_clause_alias_statements = .{ .span = .{ .start = 0, .len = 0 } },
.external_decls = try CIR.ExternalDecl.SafeList.initCapacity(gpa, 16),
.imports = CIR.Import.Store.init(),
.module_name = undefined, // Will be set later during canonicalization
.module_name_idx = Ident.Idx.NONE, // Will be set later during canonicalization
.diagnostics = CIR.Diagnostic.Span{ .span = base.DataSpan{ .start = 0, .len = 0 } },
.store = try NodeStore.initCapacity(gpa, node_capacity),
.evaluation_order = null, // Will be set after canonicalization completes
.idents = idents,
.deferred_numeric_literals = try DeferredNumericLiteral.SafeList.initCapacity(gpa, 32),
.import_mapping = types_mod.import_mapping.ImportMapping.init(gpa),
.method_idents = MethodIdents.init(),
};
}
/// Deinitialize the module environment.
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.for_clause_aliases.deinit(self.gpa);
self.rigid_vars.deinit(self.gpa);
self.imports.deinit(self.gpa);
self.deferred_numeric_literals.deinit(self.gpa);
self.import_mapping.deinit();
self.method_idents.deinit(self.gpa);
// diagnostics are stored in the NodeStore, no need to free separately
self.store.deinit();
if (self.evaluation_order) |eval_order| {
eval_order.deinit();
self.gpa.destroy(eval_order);
}
}
// Module compilation functionality
/// Records a diagnostic error during canonicalization without blocking compilation.
pub fn pushDiagnostic(self: *Self, reason: CIR.Diagnostic) std.mem.Allocator.Error!void {
_ = try self.addDiagnostic(reason);
}
/// Creates a malformed node that represents a runtime error in the IR.
pub fn pushMalformed(self: *Self, comptime RetIdx: type, reason: CIR.Diagnostic) std.mem.Allocator.Error!RetIdx {
comptime if (!isCastable(RetIdx)) @compileError("Idx type " ++ @typeName(RetIdx) ++ " is not castable");
const diag_idx = try self.addDiagnostic(reason);
const region = getDiagnosticRegion(reason);
const malformed_idx = try self.addMalformed(diag_idx, region);
return castIdx(Node.Idx, RetIdx, malformed_idx);
}
/// Extract the region from any diagnostic variant
fn getDiagnosticRegion(diagnostic: CIR.Diagnostic) Region {
return switch (diagnostic) {
.type_redeclared => |data| data.redeclared_region,
.type_alias_redeclared => |data| data.redeclared_region,
.nominal_type_redeclared => |data| data.redeclared_region,
.duplicate_record_field => |data| data.duplicate_region,
inline else => |data| data.region,
};
}
/// Import helper functions from CIR
const isCastable = CIR.isCastable;
/// Cast function for safely converting between compatible index types
pub const castIdx = CIR.castIdx;
// Module compilation functions
/// Retrieve all diagnostics collected during canonicalization.
pub fn getDiagnostics(self: *Self) std.mem.Allocator.Error![]CIR.Diagnostic {
// Get all diagnostics from the store, not just the ones in self.diagnostics span
const all_diagnostics = try self.store.diagnosticSpanFrom(0);
const diagnostic_indices = self.store.sliceDiagnostics(all_diagnostics);
const diagnostics = try self.gpa.alloc(CIR.Diagnostic, diagnostic_indices.len);
for (diagnostic_indices, 0..) |diagnostic_idx, i| {
diagnostics[i] = self.store.getDiagnostic(diagnostic_idx);
}
return diagnostics;
}
/// Compilation error report type for user-friendly error messages
pub const Report = CIR.Report;
/// Convert a canonicalization diagnostic to a Report for rendering.
pub fn diagnosticToReport(self: *Self, diagnostic: CIR.Diagnostic, allocator: std.mem.Allocator, filename: []const u8) !Report {
return switch (diagnostic) {
.invalid_num_literal => |data| blk: {
const region_info = self.calcRegionInfo(data.region);
// Extract the literal text from the source
const literal_text = self.getSource(data.region);
var report = Report.init(allocator, "INVALID NUMBER", .runtime_error);
const owned_literal = try report.addOwnedString(literal_text);
try report.document.addReflowingText("This number literal is not valid: ");
try report.document.addInlineCode(owned_literal);
try report.document.addLineBreak();
try report.document.addLineBreak();
const owned_filename = try report.addOwnedString(filename);
try report.document.addSourceRegion(
region_info,
.error_highlight,
owned_filename,
self.getSourceAll(),
self.getLineStartsAll(),
);
try report.document.addLineBreak();
try report.document.addReflowingText("Check that the number is correctly formatted. Valid examples include: ");
try report.document.addInlineCode("42");
try report.document.addReflowingText(", ");
try report.document.addInlineCode("3.14");
try report.document.addReflowingText(", ");
try report.document.addInlineCode("0x1A");
try report.document.addReflowingText(", or ");
try report.document.addInlineCode("1_000_000");
try report.document.addReflowingText(".");
break :blk report;
},
.ident_not_in_scope => |data| blk: {
const region_info = self.calcRegionInfo(data.region);
const ident_name = self.getIdent(data.ident);
var report = Report.init(allocator, "UNDEFINED VARIABLE", .runtime_error);
const owned_ident = try report.addOwnedString(ident_name);
try report.document.addReflowingText("Nothing is named ");
try report.document.addUnqualifiedSymbol(owned_ident);
try report.document.addReflowingText(" in this scope.");
try report.document.addLineBreak();
try report.document.addReflowingText("Is there an ");
try report.document.addKeyword("import");
try report.document.addReflowingText(" or ");
try report.document.addKeyword("exposing");
try report.document.addReflowingText(" missing up-top?");
try report.document.addLineBreak();
try report.document.addLineBreak();
const owned_filename = try report.addOwnedString(filename);
try report.document.addSourceRegion(
region_info,
.error_highlight,
owned_filename,
self.getSourceAll(),
self.getLineStartsAll(),
);
break :blk report;
},
.qualified_ident_does_not_exist => |data| blk: {
const region_info = self.calcRegionInfo(data.region);
const ident_name = self.getIdent(data.ident);
var report = Report.init(allocator, "DOES NOT EXIST", .runtime_error);
const owned_ident = try report.addOwnedString(ident_name);
try report.document.addUnqualifiedSymbol(owned_ident);
try report.document.addReflowingText(" does not exist.");
try report.document.addLineBreak();
try report.document.addLineBreak();
const owned_filename = try report.addOwnedString(filename);
try report.document.addSourceRegion(
region_info,
.error_highlight,
owned_filename,
self.getSourceAll(),
self.getLineStartsAll(),
);
break :blk report;
},
.exposed_but_not_implemented => |data| blk: {
const region_info = self.calcRegionInfo(data.region);
var report = Report.init(allocator, "EXPOSED BUT NOT DEFINED", .runtime_error);
const ident_name = self.getIdent(data.ident);
const owned_ident = try report.addOwnedString(ident_name);
try report.document.addReflowingText("The module header says that ");
try report.document.addUnqualifiedSymbol(owned_ident);
try report.document.addReflowingText(" is exposed, but it is not defined anywhere in this module.");
try report.document.addLineBreak();
try report.document.addLineBreak();
// Add source context with location
const owned_filename = try report.addOwnedString(filename);
try report.addSourceContext(region_info, owned_filename, self.getSourceAll(), self.getLineStartsAll());
try report.document.addReflowingText("You can fix this by either defining ");
try report.document.addUnqualifiedSymbol(owned_ident);
try report.document.addReflowingText(" in this module, or by removing it from the list of exposed values.");
break :blk report;
},
.unused_variable => |data| blk: {
const region_info = self.calcRegionInfo(data.region);
const ident_name = self.getIdent(data.ident);
var report = Report.init(allocator, "UNUSED VARIABLE", .warning);
const owned_ident = try report.addOwnedString(ident_name);
try report.document.addReflowingText("Variable ");
try report.document.addUnqualifiedSymbol(owned_ident);
try report.document.addReflowingText(" is not used anywhere in your code.");
try report.document.addLineBreak();
try report.document.addLineBreak();
const MAX_IDENT_FIXED_BUFFER = 100;
if (owned_ident.len > MAX_IDENT_FIXED_BUFFER - 1) {
try report.document.addReflowingText("If you don't need this variable, prefix it with an underscore to suppress this warning.");
} else {
// format the identifier with an underscore
try report.document.addReflowingText("If you don't need this variable, prefix it with an underscore like ");
var buf: [MAX_IDENT_FIXED_BUFFER]u8 = undefined;
const owned_ident_with_underscore = try std.fmt.bufPrint(&buf, "_{s}", .{owned_ident});
try report.document.addUnqualifiedSymbol(owned_ident_with_underscore);
try report.document.addReflowingText(" to suppress this warning.");
}
try report.document.addLineBreak();
try report.document.addReflowingText("The unused variable is declared here:");
try report.document.addLineBreak();
const owned_filename = try report.addOwnedString(filename);
try report.document.addSourceRegion(
region_info,
.error_highlight,
owned_filename,
self.getSourceAll(),
self.getLineStartsAll(),
);
break :blk report;
},
.underscore_in_type_declaration => |data| blk: {
const region_info = self.calcRegionInfo(data.region);
var report = Report.init(allocator, "UNDERSCORE IN TYPE ALIAS", .runtime_error);
const kind = if (data.is_alias) "alias" else "opaque type";
const message = try std.fmt.allocPrint(allocator, "Underscores are not allowed in type {s} declarations.", .{kind});
defer allocator.free(message);
const owned_message = try report.addOwnedString(message);
try report.document.addReflowingText(owned_message);
try report.document.addLineBreak();
try report.document.addLineBreak();
// Add source context with location
const owned_filename = try report.addOwnedString(filename);
try report.addSourceContext(region_info, owned_filename, self.getSourceAll(), self.getLineStartsAll());
try report.document.addLineBreak();
const explanation = try std.fmt.allocPrint(allocator, "Underscores in type annotations mean \"I don't care about this type\", which doesn't make sense when declaring a type. If you need a placeholder type variable, use a named type variable like `a` instead.", .{});
defer allocator.free(explanation);
const owned_explanation = try report.addOwnedString(explanation);
try report.document.addReflowingText(owned_explanation);
break :blk report;
},
.undeclared_type => |data| blk: {
const type_name = self.getIdent(data.name);
const region_info = self.calcRegionInfo(data.region);
var report = Report.init(allocator, "UNDECLARED TYPE", .runtime_error);
const owned_type_name = try report.addOwnedString(type_name);
try report.document.addReflowingText("The type ");
try report.document.addType(owned_type_name);
try report.document.addReflowingText(" is not declared in this scope.");
try report.document.addLineBreak();
try report.document.addLineBreak();
try report.document.addReflowingText("This type is referenced here:");
try report.document.addLineBreak();
const owned_filename = try report.addOwnedString(filename);
try report.document.addSourceRegion(
region_info,
.error_highlight,
owned_filename,
self.getSourceAll(),
self.getLineStartsAll(),
);
break :blk report;
},
.type_alias_but_needed_nominal => |data| blk: {
const type_name = self.getIdent(data.name);
const region_info = self.calcRegionInfo(data.region);
var report = Report.init(allocator, "EXPECTED NOMINAL TYPE", .runtime_error);
const owned_type_name = try report.addOwnedString(type_name);
try report.document.addReflowingText("You are using the type ");
try report.document.addType(owned_type_name);
try report.document.addReflowingText(" like a nominal type, but it is an alias.");
try report.document.addLineBreak();
try report.document.addLineBreak();
try report.document.addReflowingText("This type is referenced here:");
try report.document.addLineBreak();
const owned_filename = try report.addOwnedString(filename);
try report.document.addSourceRegion(
region_info,
.error_highlight,
owned_filename,
self.getSourceAll(),
self.getLineStartsAll(),
);
try report.document.addLineBreak();
try report.document.addLineBreak();
try report.document.addAnnotated("Hint:", .emphasized);
try report.document.addReflowingText(" You can declare this type with ");
try report.document.addInlineCode(":=");
try report.document.addReflowingText(" to make it nominal.");
break :blk report;
},
.type_redeclared => |data| blk: {
const type_name = self.getIdent(data.name);
const original_region_info = self.calcRegionInfo(data.original_region);
const redeclared_region_info = self.calcRegionInfo(data.redeclared_region);
var report = Report.init(allocator, "TYPE REDECLARED", .runtime_error);
const owned_type_name = try report.addOwnedString(type_name);
try report.document.addReflowingText("The type ");
try report.document.addType(owned_type_name);
try report.document.addReflowingText(" is being redeclared.");
try report.document.addLineBreak();
try report.document.addLineBreak();
// Show where the redeclaration is
try report.document.addReflowingText("The redeclaration is here:");
try report.document.addLineBreak();
const owned_filename = try report.addOwnedString(filename);
try report.document.addSourceRegion(
redeclared_region_info,
.error_highlight,
owned_filename,
self.getSourceAll(),
self.getLineStartsAll(),
);
try report.document.addLineBreak();
try report.document.addReflowingText("But ");
try report.document.addType(owned_type_name);
try report.document.addReflowingText(" was already declared here:");
try report.document.addLineBreak();
try report.document.addSourceRegion(
original_region_info,
.dimmed,
owned_filename,
self.getSourceAll(),
self.getLineStartsAll(),
);
break :blk report;
},
.invalid_top_level_statement => |data| blk: {
const stmt_name = self.getString(data.stmt);
const region_info = self.calcRegionInfo(data.region);
var report = Report.init(allocator, "INVALID STATEMENT", .runtime_error);
const owned_stmt = try report.addOwnedString(stmt_name);
try report.document.addReflowingText("The statement ");
try report.document.addInlineCode(owned_stmt);
try report.document.addReflowingText(" is not allowed at the top level.");
try report.document.addLineBreak();
try report.document.addReflowingText("Only definitions, type annotations, and imports are allowed at the top level.");
try report.document.addLineBreak();
try report.document.addLineBreak();
const owned_filename = try report.addOwnedString(filename);
try report.document.addSourceRegion(
region_info,
.error_highlight,
owned_filename,
self.getSourceAll(),
self.getLineStartsAll(),
);
break :blk report;
},
.used_underscore_variable => |data| blk: {
const ident_name = self.getIdent(data.ident);
const region_info = self.calcRegionInfo(data.region);
var report = Report.init(allocator, "UNDERSCORE VARIABLE USED", .warning);
const owned_ident = try report.addOwnedString(ident_name);
try report.document.addReflowingText("Variable ");
try report.document.addUnqualifiedSymbol(owned_ident);
try report.document.addReflowingText(" is prefixed with an underscore but is actually used.");
try report.document.addLineBreak();
try report.document.addLineBreak();
try report.document.addReflowingText("Variables prefixed with ");
try report.document.addUnqualifiedSymbol("_");
try report.document.addReflowingText(" are intended to be unused. Remove the underscore prefix: ");
// Create the suggested name without underscore
const suggested_name = ident_name[1..]; // Remove first character (_)
const owned_suggested = try report.addOwnedString(suggested_name);
try report.document.addUnqualifiedSymbol(owned_suggested);
try report.document.addReflowingText(".");
try report.document.addLineBreak();
try report.document.addLineBreak();
const owned_filename = try report.addOwnedString(filename);
try report.document.addSourceRegion(
region_info,
.warning_highlight,
owned_filename,
self.getSourceAll(),
self.getLineStartsAll(),
);
break :blk report;
},
.expr_not_canonicalized => |data| blk: {
const region_info = self.calcRegionInfo(data.region);
var report = Report.init(allocator, "UNRECOGNIZED SYNTAX", .runtime_error);
try report.document.addReflowingText("I don't recognize this syntax.");
try report.document.addLineBreak();
try report.document.addLineBreak();
const owned_filename = try report.addOwnedString(filename);
try report.document.addSourceRegion(
region_info,
.error_highlight,
owned_filename,
self.getSourceAll(),
self.getLineStartsAll(),
);
try report.document.addLineBreak();
try report.document.addReflowingText("This might be a syntax error, an unsupported language feature, or a typo.");
break :blk report;
},
.crash_expects_string => |data| blk: {
const region_info = self.calcRegionInfo(data.region);
var report = Report.init(allocator, "CRASH EXPECTS STRING", .runtime_error);
try report.document.addReflowingText("The ");
try report.document.addAnnotated("crash", .inline_code);
try report.document.addReflowingText(" keyword expects a string literal as its argument.");
try report.document.addLineBreak();
try report.document.addReflowingText("For example: ");
try report.document.addAnnotated("crash \"Something went wrong\"", .inline_code);
try report.document.addLineBreak();
const owned_filename = try report.addOwnedString(filename);
try report.document.addSourceRegion(
region_info,
.error_highlight,
owned_filename,
self.getSourceAll(),
self.getLineStartsAll(),
);
break :blk report;
},
.duplicate_record_field => |data| blk: {
const field_name = self.getIdent(data.field_name);
const duplicate_region_info = self.calcRegionInfo(data.duplicate_region);
const original_region_info = self.calcRegionInfo(data.original_region);
var report = Report.init(allocator, "DUPLICATE RECORD FIELD", .runtime_error);
const owned_field_name = try report.addOwnedString(field_name);
try report.document.addReflowingText("The record field ");
try report.document.addRecordField(owned_field_name);
try report.document.addReflowingText(" appears more than once in this record.");
try report.document.addLineBreak();
try report.document.addLineBreak();
// Show where the duplicate field is
try report.document.addReflowingText("This field is duplicated here:");
try report.document.addLineBreak();
const owned_filename = try report.addOwnedString(filename);
try report.document.addSourceRegion(
duplicate_region_info,
.error_highlight,
owned_filename,
self.getSourceAll(),
self.getLineStartsAll(),
);
try report.document.addLineBreak();
try report.document.addReflowingText("The field ");
try report.document.addRecordField(owned_field_name);
try report.document.addReflowingText(" was first defined here:");
try report.document.addLineBreak();
try report.document.addSourceRegion(
original_region_info,
.dimmed,
owned_filename,
self.getSourceAll(),
self.getLineStartsAll(),
);
try report.document.addLineBreak();
try report.document.addReflowingText("Record fields must have unique names. Consider renaming one of these fields or removing the duplicate.");
break :blk report;
},
.redundant_exposed => |data| blk: {
const ident_name = self.getIdent(data.ident);
const region_info = self.calcRegionInfo(data.region);
var report = Report.init(allocator, "REDUNDANT EXPOSED", .warning);
const owned_ident = try report.addOwnedString(ident_name);
try report.document.addReflowingText("The identifier ");
try report.document.addUnqualifiedSymbol(owned_ident);
try report.document.addReflowingText(" is exposed multiple times in the module header.");
try report.document.addLineBreak();
try report.document.addLineBreak();
const owned_filename = try report.addOwnedString(filename);
try report.document.addSourceRegion(
region_info,
.error_highlight,
owned_filename,
self.getSourceAll(),
self.getLineStartsAll(),
);
try report.document.addReflowingText("You can remove the duplicate entry to fix this warning.");
break :blk report;
},
.undeclared_type_var => |data| blk: {
const type_var_name = self.getIdent(data.name);
const region_info = self.calcRegionInfo(data.region);
var report = Report.init(allocator, "UNDECLARED TYPE VARIABLE", .runtime_error);
const owned_type_var_name = try report.addOwnedString(type_var_name);
try report.document.addReflowingText("The type variable ");
try report.document.addType(owned_type_var_name);
try report.document.addReflowingText(" is not declared in this scope.");
try report.document.addLineBreak();
try report.document.addLineBreak();
try report.document.addReflowingText("Type variables must be introduced in a type annotation before they can be used.");
try report.document.addLineBreak();
try report.document.addLineBreak();
try report.document.addReflowingText("This type variable is referenced here:");
try report.document.addLineBreak();
const owned_filename = try report.addOwnedString(filename);
try report.document.addSourceRegion(
region_info,
.error_highlight,
owned_filename,
self.getSourceAll(),
self.getLineStartsAll(),
);
break :blk report;
},
.not_implemented => |data| blk: {
const feature = self.getString(data.feature);
var report = Report.init(allocator, "NOT IMPLEMENTED", .fatal);
const owned_feature = try report.addOwnedString(feature);
try report.document.addReflowingText("This feature is not yet implemented: ");
try report.document.addAnnotatedText(owned_feature, .emphasized);
try report.document.addLineBreak();
try report.document.addLineBreak();
const owned_filename = try report.addOwnedString(filename);
const region_info = self.calcRegionInfo(data.region);
try report.document.addSourceRegion(
region_info,
.error_highlight,
owned_filename,
self.getSourceAll(),
self.getLineStartsAll(),
);
try report.document.addLineBreak();
try report.document.addReflowingText("This error doesn't have a proper diagnostic report yet. Let us know if you want to help improve Roc's error messages!");
try report.document.addLineBreak();
break :blk report;
},
.malformed_type_annotation => |data| blk: {
var report = Report.init(allocator, "MALFORMED TYPE", .runtime_error);
try report.document.addReflowingText("This type annotation is malformed or contains invalid syntax.");
try report.document.addLineBreak();
try report.document.addLineBreak();
const owned_filename = try report.addOwnedString(filename);
const region_info = self.calcRegionInfo(data.region);
try report.document.addSourceRegion(
region_info,
.error_highlight,
owned_filename,
self.getSourceAll(),
self.getLineStartsAll(),
);
break :blk report;
},
.if_condition_not_canonicalized => |_| blk: {
var report = Report.init(allocator, "INVALID IF CONDITION", .runtime_error);
try report.document.addReflowingText("The condition in this ");
try report.document.addKeyword("if");
try report.document.addReflowingText(" expression could not be processed.");
try report.document.addLineBreak();
try report.document.addLineBreak();
try report.document.addReflowingText("The condition must be a valid expression that evaluates to a ");
try report.document.addKeyword("Bool");
try report.document.addReflowingText(" value (");
try report.document.addKeyword("Bool.true");
try report.document.addReflowingText(" or ");
try report.document.addKeyword("Bool.false");
try report.document.addReflowingText(").");
break :blk report;
},
.if_then_not_canonicalized => |_| blk: {
var report = Report.init(allocator, "INVALID IF BRANCH", .runtime_error);
try report.document.addReflowingText("The branch in this ");
try report.document.addKeyword("if");
try report.document.addReflowingText(" expression could not be processed.");
try report.document.addLineBreak();
try report.document.addLineBreak();
try report.document.addReflowingText("The branch must contain a valid expression. Check for syntax errors or missing values.");
break :blk report;
},
.if_else_not_canonicalized => |_| blk: {
var report = Report.init(allocator, "INVALID IF BRANCH", .runtime_error);
try report.document.addReflowingText("The ");
try report.document.addKeyword("else");
try report.document.addReflowingText(" branch of this ");
try report.document.addKeyword("if");
try report.document.addReflowingText(" expression could not be processed.");
try report.document.addLineBreak();
try report.document.addLineBreak();
try report.document.addReflowingText("The ");
try report.document.addKeyword("else");
try report.document.addReflowingText(" branch must contain a valid expression. Check for syntax errors or missing values.");
try report.document.addLineBreak();
break :blk report;
},
.if_expr_without_else => |_| blk: {
var report = Report.init(allocator, "IF EXPRESSION WITHOUT ELSE", .runtime_error);
try report.document.addReflowingText("This ");
try report.document.addKeyword("if");
try report.document.addReflowingText(" has no ");
try report.document.addKeyword("else");
try report.document.addReflowingText(" branch, but it's being used as an expression (assigned to a variable, passed to a function, etc.).");
try report.document.addLineBreak();
try report.document.addLineBreak();
try report.document.addReflowingText("You can only use ");
try report.document.addKeyword("if");
try report.document.addReflowingText(" without ");
try report.document.addKeyword("else");
try report.document.addReflowingText(" when it's a statement. When ");
try report.document.addKeyword("if");
try report.document.addReflowingText(" is used as an expression that evaluates to a value, ");
try report.document.addKeyword("else");
try report.document.addReflowingText(" is required because otherwise there wouldn't always be a value available.");
try report.document.addLineBreak();
try report.document.addLineBreak();
try report.document.addReflowingText("Either add an ");
try report.document.addKeyword("else");
try report.document.addReflowingText(" branch, or use this ");
try report.document.addKeyword("if");
try report.document.addReflowingText(" as a standalone statement.");
break :blk report;
},
.pattern_not_canonicalized => |_| blk: {
var report = Report.init(allocator, "INVALID PATTERN", .runtime_error);
try report.document.addReflowingText("This pattern contains invalid syntax or uses unsupported features.");
break :blk report;
},
.pattern_arg_invalid => |_| blk: {
var report = Report.init(allocator, "INVALID PATTERN ARGUMENT", .runtime_error);
try report.document.addReflowingText("Pattern arguments must be valid patterns like identifiers, literals, or destructuring patterns.");
break :blk report;
},
.shadowing_warning => |data| blk: {
const ident_name = self.getIdent(data.ident);
const new_region_info = self.calcRegionInfo(data.region);
const original_region_info = self.calcRegionInfo(data.original_region);
var report = Report.init(allocator, "DUPLICATE DEFINITION", .warning);
const owned_ident = try report.addOwnedString(ident_name);
try report.document.addReflowingText("The name ");
try report.document.addUnqualifiedSymbol(owned_ident);
try report.document.addReflowingText(" is being redeclared in this scope.");
try report.document.addLineBreak();
try report.document.addLineBreak();
// Show where the new declaration is
try report.document.addReflowingText("The redeclaration is here:");
try report.document.addLineBreak();
const owned_filename = try report.addOwnedString(filename);
try report.document.addSourceRegion(
new_region_info,
.error_highlight,
owned_filename,
self.getSourceAll(),
self.getLineStartsAll(),
);
try report.document.addLineBreak();
try report.document.addReflowingText("But ");
try report.document.addUnqualifiedSymbol(owned_ident);
try report.document.addReflowingText(" was already defined here:");
try report.document.addLineBreak();
try report.document.addSourceRegion(
original_region_info,
.dimmed,
owned_filename,
self.getSourceAll(),
self.getLineStartsAll(),
);
break :blk report;
},
.empty_tuple => |data| blk: {
const region_info = self.calcRegionInfo(data.region);
var report = Report.init(allocator, "EMPTY TUPLE NOT ALLOWED", .runtime_error);
const owned_filename = try report.addOwnedString(filename);
try report.document.addReflowingText("I am part way through parsing this tuple, but it is empty:");
try report.document.addLineBreak();
try report.document.addSourceRegion(
region_info,
.error_highlight,
owned_filename,
self.getSourceAll(),
self.getLineStartsAll(),
);
try report.document.addLineBreak();
try report.document.addReflowingText("If you want to represent nothing, try using an empty record: ");
try report.document.addAnnotated("{}", .inline_code);
try report.document.addReflowingText(".");
break :blk report;
},
.lambda_body_not_canonicalized => blk: {
var report = Report.init(allocator, "INVALID LAMBDA", .runtime_error);
try report.document.addReflowingText("The body of this lambda expression is not valid.");
break :blk report;
},
.malformed_where_clause => |data| blk: {
const region_info = self.calcRegionInfo(data.region);
var report = Report.init(allocator, "MALFORMED WHERE CLAUSE", .runtime_error);
try report.document.addReflowingText("This where clause could not be parsed correctly.");
try report.document.addLineBreak();
try report.document.addLineBreak();
const owned_filename = try report.addOwnedString(filename);
try report.document.addSourceRegion(
region_info,
.error_highlight,
owned_filename,
self.getSourceAll(),
self.getLineStartsAll(),
);
try report.document.addLineBreak();
try report.document.addReflowingText("Check the syntax of your where clause.");
break :blk report;
},
.var_across_function_boundary => blk: {
var report = Report.init(allocator, "VAR REASSIGNMENT ERROR", .runtime_error);
try report.document.addReflowingText("Cannot reassign a ");
try report.document.addKeyword("var");
try report.document.addReflowingText(" from outside the function where it was declared.");
try report.document.addLineBreak();
try report.document.addReflowingText("Variables declared with ");
try report.document.addKeyword("var");
try report.document.addReflowingText(" can only be reassigned within the same function scope.");
break :blk report;
},
.tuple_elem_not_canonicalized => blk: {
var report = Report.init(allocator, "INVALID TUPLE ELEMENT", .runtime_error);
try report.document.addReflowingText("This tuple element is malformed or contains invalid syntax.");
break :blk report;
},
.f64_pattern_literal => |data| blk: {
// Extract the literal text from the source
const literal_text = self.getSource(data.region);
var report = Report.init(allocator, "F64 NOT ALLOWED IN PATTERN", .runtime_error);
// Format the message to match origin/main
try report.document.addText("This floating-point literal cannot be used in a pattern match: ");
try report.document.addInlineCode(literal_text);
try report.document.addLineBreak();
try report.document.addLineBreak();
try report.document.addReflowingText("This number exceeds the precision range of Roc's ");
try report.document.addInlineCode("Dec");
try report.document.addReflowingText(" type and would require F64 representation. ");
try report.document.addReflowingText("Floating-point numbers (F64) cannot be used in patterns because they don't have reliable equality comparison.");
try report.document.addLineBreak();
try report.document.addLineBreak();
try report.document.addText("Consider one of these alternatives:");
try report.document.addLineBreak();
try report.document.addText("• Use a guard condition with a range check");
try report.document.addLineBreak();
try report.document.addText("• Use a smaller number that fits in Dec's precision");
try report.document.addLineBreak();
try report.document.addText("• Restructure your code to avoid pattern matching on this value");
try report.document.addLineBreak();
try report.document.addLineBreak();
try report.document.addText("For example, instead of:");
try report.document.addLineBreak();
try report.document.addInlineCode("1e100 => ...");
try report.document.addLineBreak();
try report.document.addText("Use a guard:");
try report.document.addLineBreak();
try report.document.addInlineCode("n if n > 1e99 => ...");
break :blk report;
},
.type_not_exposed => |data| blk: {
const region_info = self.calcRegionInfo(data.region);
var report = Report.init(allocator, "TYPE NOT EXPOSED", .runtime_error);
const type_name_bytes = self.getIdent(data.type_name);
const type_name = try report.addOwnedString(type_name_bytes);
const module_name_bytes = self.getIdent(data.module_name);
const module_name = try report.addOwnedString(module_name_bytes);
// Format the message to match origin/main
try report.document.addText("The type ");
try report.document.addInlineCode(type_name);
try report.document.addReflowingText(" is not exposed by the module ");
try report.document.addInlineCode(module_name);
try report.document.addReflowingText(".");
try report.document.addLineBreak();
try report.document.addLineBreak();
try report.document.addReflowingText("You're attempting to use this type here:");
try report.document.addLineBreak();
const owned_filename = try report.addOwnedString(filename);
try report.document.addSourceRegion(
region_info,
.error_highlight,
owned_filename,
self.getSourceAll(),
self.getLineStartsAll(),
);
break :blk report;
},
.value_not_exposed => |data| blk: {
const region_info = self.calcRegionInfo(data.region);
var report = Report.init(allocator, "VALUE NOT EXPOSED", .runtime_error);
// Format the message to match origin/main
try report.document.addText("The value ");
try report.document.addInlineCode(self.getIdent(data.value_name));
try report.document.addReflowingText(" is not exposed by the module ");
try report.document.addInlineCode(self.getIdent(data.module_name));
try report.document.addReflowingText(".");
try report.document.addLineBreak();
try report.document.addLineBreak();
try report.document.addReflowingText("You're attempting to use this value here:");
try report.document.addLineBreak();
const owned_filename = try report.addOwnedString(filename);
try report.document.addSourceRegion(
region_info,
.error_highlight,
owned_filename,
self.getSourceAll(),
self.getLineStartsAll(),
);
break :blk report;
},
.module_not_found => |data| blk: {
const region_info = self.calcRegionInfo(data.region);
var report = Report.init(allocator, "MODULE NOT FOUND", .runtime_error);
const module_name_bytes = self.getIdent(data.module_name);
const module_name = try report.addOwnedString(module_name_bytes);
// Format the message to match origin/main
try report.document.addText("The module ");
try report.document.addInlineCode(module_name);
try report.document.addReflowingText(" was not found in this Roc project.");
try report.document.addLineBreak();
try report.document.addLineBreak();
try report.document.addReflowingText("You're attempting to use this module here:");
try report.document.addLineBreak();
const owned_filename = try report.addOwnedString(filename);
try report.document.addSourceRegion(
region_info,
.error_highlight,
owned_filename,
self.getSourceAll(),
self.getLineStartsAll(),
);
break :blk report;
},
.module_not_imported => |data| blk: {
const region_info = self.calcRegionInfo(data.region);
var report = Report.init(allocator, "MODULE NOT IMPORTED", .runtime_error);
const module_name_bytes = self.getIdent(data.module_name);
const module_name = try report.addOwnedString(module_name_bytes);
// Format the message to match origin/main
try report.document.addText("There is no module with the name ");
try report.document.addInlineCode(module_name);
try report.document.addReflowingText(" imported into this Roc file.");
try report.document.addLineBreak();
try report.document.addLineBreak();
try report.document.addReflowingText("You're attempting to use this module here:");
try report.document.addLineBreak();
const owned_filename = try report.addOwnedString(filename);
try report.document.addSourceRegion(
region_info,
.error_highlight,
owned_filename,
self.getSourceAll(),
self.getLineStartsAll(),
);
break :blk report;
},
.nested_type_not_found => |data| blk: {
const region_info = self.calcRegionInfo(data.region);
var report = Report.init(allocator, "MISSING NESTED TYPE", .runtime_error);
const parent_bytes = self.getIdent(data.parent_name);
const parent_name = try report.addOwnedString(parent_bytes);
const nested_bytes = self.getIdent(data.nested_name);
const nested_name = try report.addOwnedString(nested_bytes);
try report.document.addInlineCode(parent_name);
try report.document.addReflowingText(" is in scope, but it doesn't have a nested type ");
if (std.mem.eql(u8, parent_bytes, nested_bytes)) {
// Say "also named" if the parent and nested types are equal, e.g. `Foo.Foo` - when
// this happens it can be kind of a confusing message if the message just says
// "Foo is in scope, but it doesn't have a nested type named Foo" compared to
// "Foo is in scope, but it doesn't have a nested type that's also named Foo"
try report.document.addReflowingText("that's also ");
}
try report.document.addReflowingText("named ");
try report.document.addInlineCode(nested_name);
try report.document.addReflowingText(".");
try report.document.addLineBreak();
try report.document.addLineBreak();
try report.document.addReflowingText("It's referenced here:");
try report.document.addLineBreak();
const owned_filename = try report.addOwnedString(filename);
try report.document.addSourceRegion(
region_info,
.error_highlight,
owned_filename,
self.getSourceAll(),
self.getLineStartsAll(),
);
break :blk report;
},
.nested_value_not_found => |data| blk: {
const region_info = self.calcRegionInfo(data.region);
var report = Report.init(allocator, "DOES NOT EXIST", .runtime_error);
const parent_bytes = self.getIdent(data.parent_name);
const parent_name = try report.addOwnedString(parent_bytes);
const nested_bytes = self.getIdent(data.nested_name);
const nested_name = try report.addOwnedString(nested_bytes);
// First line: "Foo.bar does not exist."
const full_name = try std.fmt.allocPrint(allocator, "{s}.{s}", .{ parent_bytes, nested_bytes });
defer allocator.free(full_name);
const owned_full_name = try report.addOwnedString(full_name);
try report.document.addInlineCode(owned_full_name);
try report.document.addReflowingText(" does not exist.");
try report.document.addLineBreak();
try report.document.addLineBreak();
// Second line: "Foo is in scope, but it has no associated bar."
try report.document.addInlineCode(parent_name);
try report.document.addReflowingText(" is in scope, but it has no associated ");
try report.document.addInlineCode(nested_name);
try report.document.addReflowingText(".");
try report.document.addLineBreak();
try report.document.addLineBreak();
try report.document.addReflowingText("It's referenced here:");
try report.document.addLineBreak();
const owned_filename = try report.addOwnedString(filename);
try report.document.addSourceRegion(
region_info,
.error_highlight,
owned_filename,
self.getSourceAll(),
self.getLineStartsAll(),
);
break :blk report;
},
.where_clause_not_allowed_in_type_decl => |data| blk: {
const region_info = self.calcRegionInfo(data.region);
var report = Report.init(allocator, "WHERE CLAUSE NOT ALLOWED IN TYPE DECLARATION", .warning);
// Format the message to match origin/main
try report.document.addText("You cannot define a ");
try report.document.addInlineCode("where");
try report.document.addReflowingText(" clause inside a type declaration.");
try report.document.addLineBreak();
try report.document.addLineBreak();
try report.document.addReflowingText("You're attempting do this here:");
try report.document.addLineBreak();
const owned_filename = try report.addOwnedString(filename);
try report.document.addSourceRegion(
region_info,
.error_highlight,
owned_filename,
self.getSourceAll(),
self.getLineStartsAll(),
);
break :blk report;
},
.type_module_missing_matching_type => |data| blk: {
const region_info = self.calcRegionInfo(data.region);
var report = Report.init(allocator, "TYPE MODULE MISSING MATCHING TYPE", .runtime_error);
const module_name_bytes = self.getIdent(data.module_name);
const module_name = try report.addOwnedString(module_name_bytes);
try report.document.addReflowingText("Type modules must have a type declaration matching the module name.");
try report.document.addLineBreak();
try report.document.addLineBreak();
try report.document.addText("This file is named ");
try report.document.addInlineCode(module_name);
try report.document.addReflowingText(".roc, but no top-level type declaration named ");
try report.document.addInlineCode(module_name);
try report.document.addReflowingText(" was found.");
try report.document.addLineBreak();
try report.document.addLineBreak();
try report.document.addReflowingText("Add either:");
try report.document.addLineBreak();
const nominal_msg = try std.fmt.allocPrint(allocator, "{s} := ...", .{module_name_bytes});
defer allocator.free(nominal_msg);
const owned_nominal = try report.addOwnedString(nominal_msg);
try report.document.addInlineCode(owned_nominal);
try report.document.addReflowingText(" (nominal type)");
try report.document.addLineBreak();
try report.document.addReflowingText("or:");
try report.document.addLineBreak();
const alias_msg = try std.fmt.allocPrint(allocator, "{s} : ...", .{module_name_bytes});
defer allocator.free(alias_msg);
const owned_alias = try report.addOwnedString(alias_msg);
try report.document.addInlineCode(owned_alias);
try report.document.addReflowingText(" (type alias)");
try report.document.addLineBreak();
const owned_filename = try report.addOwnedString(filename);
try report.document.addSourceRegion(
region_info,
.error_highlight,
owned_filename,
self.getSourceAll(),
self.getLineStartsAll(),
);
break :blk report;
},
.default_app_missing_main => |data| blk: {
const region_info = self.calcRegionInfo(data.region);
var report = Report.init(allocator, "MISSING MAIN! FUNCTION", .runtime_error);
try report.document.addReflowingText("Default app modules must have a ");
try report.document.addInlineCode("main!");
try report.document.addReflowingText(" function.");
try report.document.addLineBreak();
try report.document.addLineBreak();
try report.document.addText("No ");
try report.document.addInlineCode("main!");
try report.document.addReflowingText(" function was found.");
try report.document.addLineBreak();
try report.document.addLineBreak();
try report.document.addReflowingText("Add a main! function like:");
try report.document.addLineBreak();
try report.document.addInlineCode("main! = |arg| { ... }");
try report.document.addLineBreak();
const owned_filename = try report.addOwnedString(filename);
try report.document.addSourceRegion(
region_info,
.error_highlight,
owned_filename,
self.getSourceAll(),
self.getLineStartsAll(),
);
break :blk report;
},
.default_app_wrong_arity => |data| blk: {
const region_info = self.calcRegionInfo(data.region);
var report = Report.init(allocator, "MAIN! SHOULD TAKE 1 ARGUMENT", .runtime_error);
try report.document.addInlineCode("main!");
try report.document.addReflowingText(" is defined but has the wrong number of arguments. ");
try report.document.addInlineCode("main!");
try report.document.addReflowingText(" should take 1 argument.");
try report.document.addLineBreak();
try report.document.addLineBreak();
const arity_msg = try std.fmt.allocPrint(allocator, "{d}", .{data.arity});
defer allocator.free(arity_msg);
const owned_arity = try report.addOwnedString(arity_msg);
try report.document.addText("Found ");
try report.document.addInlineCode(owned_arity);
try report.document.addReflowingText(" arguments.");
try report.document.addLineBreak();
try report.document.addLineBreak();
try report.document.addReflowingText("Change it to:");
try report.document.addLineBreak();
try report.document.addInlineCode("main! = |arg| { ... }");
try report.document.addLineBreak();
const owned_filename = try report.addOwnedString(filename);
try report.document.addSourceRegion(
region_info,
.error_highlight,
owned_filename,
self.getSourceAll(),
self.getLineStartsAll(),
);
break :blk report;
},
.cannot_import_default_app => |data| blk: {
const region_info = self.calcRegionInfo(data.region);
var report = Report.init(allocator, "CANNOT IMPORT DEFAULT APP", .runtime_error);
const module_name_bytes = self.getIdent(data.module_name);
const module_name = try report.addOwnedString(module_name_bytes);
try report.document.addReflowingText("You cannot import a default app module.");
try report.document.addLineBreak();
try report.document.addLineBreak();
try report.document.addText("The module ");
try report.document.addInlineCode(module_name);
try report.document.addReflowingText(" is a default app module and cannot be imported.");
try report.document.addLineBreak();
const owned_filename = try report.addOwnedString(filename);
try report.document.addSourceRegion(
region_info,
.error_highlight,
owned_filename,
self.getSourceAll(),
self.getLineStartsAll(),
);
break :blk report;
},
.execution_requires_app_or_default_app => |data| blk: {
const region_info = self.calcRegionInfo(data.region);
var report = Report.init(allocator, "EXECUTION REQUIRES APP OR DEFAULT APP", .runtime_error);
try report.document.addReflowingText("This file cannot be executed because it is not an app or default-app module.");
try report.document.addLineBreak();
try report.document.addLineBreak();
try report.document.addReflowingText("Add either:");
try report.document.addLineBreak();
try report.document.addInlineCode("app");
try report.document.addReflowingText(" header at the top of the file");
try report.document.addLineBreak();
try report.document.addReflowingText("or:");
try report.document.addLineBreak();
try report.document.addReflowingText("a ");
try report.document.addInlineCode("main!");
try report.document.addReflowingText(" function with 1 argument (for default-app)");
try report.document.addLineBreak();
const owned_filename = try report.addOwnedString(filename);
try report.document.addSourceRegion(
region_info,
.error_highlight,
owned_filename,
self.getSourceAll(),
self.getLineStartsAll(),
);
break :blk report;
},
.type_name_case_mismatch => |data| blk: {
const region_info = self.calcRegionInfo(data.region);
var report = Report.init(allocator, "TYPE NAME CASE MISMATCH", .runtime_error);
const module_name_bytes = self.getIdent(data.module_name);
const module_name = try report.addOwnedString(module_name_bytes);
const type_name_bytes = self.getIdent(data.type_name);
const type_name = try report.addOwnedString(type_name_bytes);
try report.document.addReflowingText("Type module name must match the type declaration.");
try report.document.addLineBreak();
try report.document.addLineBreak();
try report.document.addText("This file is named ");
try report.document.addInlineCode(module_name);
try report.document.addReflowingText(".roc, but the type is named ");
try report.document.addInlineCode(type_name);
try report.document.addReflowingText(".");
try report.document.addLineBreak();
try report.document.addLineBreak();
try report.document.addReflowingText("Make sure the type name matches the filename exactly (case-sensitive).");
try report.document.addLineBreak();
const owned_filename = try report.addOwnedString(filename);
try report.document.addSourceRegion(
region_info,
.error_highlight,
owned_filename,
self.getSourceAll(),
self.getLineStartsAll(),
);
break :blk report;
},
.module_header_deprecated => |data| blk: {
const region_info = self.calcRegionInfo(data.region);
var report = Report.init(allocator, "MODULE HEADER DEPRECATED", .warning);
try report.document.addReflowingText("The ");
try report.document.addInlineCode("module");
try report.document.addReflowingText(" header is deprecated.");
try report.document.addLineBreak();
try report.document.addLineBreak();
try report.document.addReflowingText("Type modules (headerless files with a top-level type matching the filename) are now the preferred way to define modules.");
try report.document.addLineBreak();
try report.document.addLineBreak();
try report.document.addReflowingText("Remove the ");
try report.document.addInlineCode("module");
try report.document.addReflowingText(" header and ensure your file defines a type that matches the filename.");
try report.document.addLineBreak();
const owned_filename = try report.addOwnedString(filename);
try report.document.addSourceRegion(
region_info,
.warning_highlight,
owned_filename,
self.getSourceAll(),
self.getLineStartsAll(),
);
break :blk report;
},
.redundant_expose_main_type => |data| blk: {
const region_info = self.calcRegionInfo(data.region);
var report = Report.init(allocator, "REDUNDANT EXPOSE", .warning);
const type_name_bytes = self.getIdent(data.type_name);
const type_name = try report.addOwnedString(type_name_bytes);
const module_name_bytes = self.getIdent(data.module_name);
const module_name = try report.addOwnedString(module_name_bytes);
try report.document.addReflowingText("Redundantly exposing ");
try report.document.addInlineCode(type_name);
try report.document.addReflowingText(" when importing ");
try report.document.addInlineCode(module_name);
try report.document.addReflowingText(".");
try report.document.addLineBreak();
try report.document.addLineBreak();
try report.document.addReflowingText("The type ");
try report.document.addInlineCode(type_name);
try report.document.addReflowingText(" is automatically exposed when importing a type module.");
try report.document.addLineBreak();
try report.document.addLineBreak();
try report.document.addReflowingText("Remove ");
try report.document.addInlineCode(type_name);
try report.document.addReflowingText(" from the exposing clause.");
try report.document.addLineBreak();
const owned_filename = try report.addOwnedString(filename);
try report.document.addSourceRegion(
region_info,
.warning_highlight,
owned_filename,
self.getSourceAll(),
self.getLineStartsAll(),
);
break :blk report;
},
.invalid_main_type_rename_in_exposing => |data| blk: {
const region_info = self.calcRegionInfo(data.region);
var report = Report.init(allocator, "INVALID TYPE RENAME", .runtime_error);
const type_name_bytes = self.getIdent(data.type_name);
const type_name = try report.addOwnedString(type_name_bytes);
const alias_bytes = self.getIdent(data.alias);
const alias = try report.addOwnedString(alias_bytes);
try report.document.addReflowingText("Cannot rename ");
try report.document.addInlineCode(type_name);
try report.document.addReflowingText(" to ");
try report.document.addInlineCode(alias);
try report.document.addReflowingText(" in the exposing clause.");
try report.document.addLineBreak();
try report.document.addLineBreak();
try report.document.addReflowingText("To rename both the module and its main type, use ");
try report.document.addInlineCode("as");
try report.document.addReflowingText(" at the module level:");
try report.document.addLineBreak();
const example_msg = try std.fmt.allocPrint(allocator, "import ModuleName as {s}", .{alias_bytes});
defer allocator.free(example_msg);
const owned_example = try report.addOwnedString(example_msg);
try report.document.addInlineCode(owned_example);
try report.document.addLineBreak();
const owned_filename = try report.addOwnedString(filename);
try report.document.addSourceRegion(
region_info,
.error_highlight,
owned_filename,
self.getSourceAll(),
self.getLineStartsAll(),
);
break :blk report;
},
.ident_already_in_scope => |data| blk: {
const region_info = self.calcRegionInfo(data.region);
const ident_name = self.getIdent(data.ident);
var report = Report.init(allocator, "SHADOWING", .runtime_error);
const owned_ident = try report.addOwnedString(ident_name);
try report.document.addReflowingText("The name ");
try report.document.addUnqualifiedSymbol(owned_ident);
try report.document.addReflowingText(" is already defined in this scope.");
try report.document.addLineBreak();
try report.document.addLineBreak();
try report.document.addReflowingText("Choose a different name for this identifier.");
try report.document.addLineBreak();
try report.document.addLineBreak();
const owned_filename = try report.addOwnedString(filename);
try report.document.addSourceRegion(
region_info,
.error_highlight,
owned_filename,
self.getSourceAll(),
self.getLineStartsAll(),
);
break :blk report;
},
else => unreachable, // All diagnostics must have explicit handlers
};
}
/// Get region info for a given region
pub fn getRegionInfo(self: *const Self, region: Region) !RegionInfo {
return self.common.getRegionInfo(region);
}
/// Returns diagnostic position information for the given region.
/// This is a standalone utility function that takes the source text as a parameter
/// to avoid storing it in the cacheable IR structure.
pub fn calcRegionInfo(self: *const Self, region: Region) RegionInfo {
return self.common.calcRegionInfo(region);
}
/// Extract a literal from source code between given byte offsets
pub fn literal_from_source(self: *const Self, start_offset: u32, end_offset: u32) []const u8 {
return self.common.source[start_offset..end_offset];
}
/// Get the source line for a given region
pub fn getSourceLine(self: *const Self, region: Region) ![]const u8 {
return self.common.getSourceLine(region);
}
/// Serialized representation of ModuleEnv.
/// Uses extern struct to guarantee consistent field layout across optimization levels.
pub const Serialized = extern struct {
// Field order must match the runtime ModuleEnv struct exactly for in-place deserialization
gpa: [2]u64, // Reserve space for allocator (vtable ptr + context ptr), provided during deserialization
common: CommonEnv.Serialized,
types: TypeStore.Serialized,
module_kind: ModuleKind.Serialized,
all_defs: CIR.Def.Span,
all_statements: CIR.Statement.Span,
exports: CIR.Def.Span,
requires_types: RequiredType.SafeList.Serialized,
for_clause_aliases: ForClauseAlias.SafeList.Serialized,
rigid_vars_reserved: [4]u64, // Reserved space for rigid_vars (AutoHashMapUnmanaged is ~32 bytes), initialized at runtime
builtin_statements: CIR.Statement.Span,
for_clause_alias_statements: CIR.Statement.Span,
external_decls: CIR.ExternalDecl.SafeList.Serialized,
imports: CIR.Import.Store.Serialized,
module_name: [2]u64, // Reserve space for slice (ptr + len), provided during deserialization
module_name_idx_reserved: u32, // Reserved space for module_name_idx field (interned during deserialization)
diagnostics: CIR.Diagnostic.Span,
store: NodeStore.Serialized,
evaluation_order_reserved: u64, // Reserved space for evaluation_order field (required for in-place deserialization cast)
// Well-known identifier indices (serialized directly, no lookup needed during deserialization)
idents: CommonIdents,
deferred_numeric_literals: DeferredNumericLiteral.SafeList.Serialized,
import_mapping_reserved: [6]u64, // Reserved space for import_mapping (AutoHashMap is ~40 bytes), initialized at runtime
method_idents: MethodIdents.Serialized,
/// Serialize a ModuleEnv into this Serialized struct, appending data to the writer
pub fn serialize(
self: *Serialized,
env: *const Self,
allocator: std.mem.Allocator,
writer: *CompactWriter,
) !void {
try self.common.serialize(&env.common, allocator, writer);
try self.types.serialize(&env.types, allocator, writer);
// Copy simple values directly
self.module_kind = ModuleKind.Serialized.encode(env.module_kind);
self.all_defs = env.all_defs;
self.all_statements = env.all_statements;
self.exports = env.exports;
self.builtin_statements = env.builtin_statements;
self.for_clause_alias_statements = env.for_clause_alias_statements;
try self.requires_types.serialize(&env.requires_types, allocator, writer);
try self.for_clause_aliases.serialize(&env.for_clause_aliases, allocator, writer);
try self.external_decls.serialize(&env.external_decls, allocator, writer);
try self.imports.serialize(&env.imports, allocator, writer);
self.diagnostics = env.diagnostics;
// Serialize NodeStore
try self.store.serialize(&env.store, allocator, writer);
// Serialize deferred numeric literals (will be empty during serialization since it's only used during type checking/evaluation)
try self.deferred_numeric_literals.serialize(&env.deferred_numeric_literals, allocator, writer);
// Set gpa, module_name, module_name_idx_reserved, evaluation_order_reserved to zeros;
// these are runtime-only and will be set during deserialization.
self.gpa = .{ 0, 0 };
self.module_name = .{ 0, 0 };
self.module_name_idx_reserved = 0;
self.evaluation_order_reserved = 0;
// rigid_vars is runtime-only and initialized fresh during deserialization
self.rigid_vars_reserved = .{ 0, 0, 0, 0 };
// Serialize well-known identifier indices directly (no lookup needed during deserialization)
self.idents = env.idents;
// import_mapping is runtime-only and initialized fresh during deserialization
self.import_mapping_reserved = .{ 0, 0, 0, 0, 0, 0 };
// Serialize method_idents map
try self.method_idents.serialize(&env.method_idents, allocator, writer);
}
/// Deserialize a ModuleEnv from the buffer, updating the ModuleEnv in place
pub fn deserialize(
self: *Serialized,
offset: i64,
gpa: std.mem.Allocator,
source: []const u8,
module_name: []const u8,
) std.mem.Allocator.Error!*Self {
// Verify that Serialized is at least as large as the runtime struct.
// This is required because we're reusing the same memory location.
// On 32-bit platforms, Serialized may be larger due to using fixed-size types for platform-independent serialization.
// In Debug builds, Self may be larger due to debug-only store tracking fields, so skip this check.
comptime {
if (builtin.mode != .Debug) {
std.debug.assert(@sizeOf(@This()) >= @sizeOf(Self));
}
}
// Overwrite ourself with the deserialized version, and return our pointer after casting it to Self.
const env = @as(*Self, @ptrFromInt(@intFromPtr(self)));
// Deserialize common env first so we can look up identifiers
const common = self.common.deserialize(offset, source).*;
env.* = Self{
.gpa = gpa,
.common = common,
.types = self.types.deserialize(offset, gpa).*,
.module_kind = self.module_kind.decode(),
.all_defs = self.all_defs,
.all_statements = self.all_statements,
.exports = self.exports,
.requires_types = self.requires_types.deserialize(offset).*,
.for_clause_aliases = self.for_clause_aliases.deserialize(offset).*,
.builtin_statements = self.builtin_statements,
.for_clause_alias_statements = self.for_clause_alias_statements,
.external_decls = self.external_decls.deserialize(offset).*,
.imports = (try self.imports.deserialize(offset, gpa)).*,
.module_name = module_name,
.module_name_idx = Ident.Idx.NONE, // Not used for deserialized modules (only needed during fresh canonicalization)
.diagnostics = self.diagnostics,
.store = self.store.deserialize(offset, gpa).*,
.evaluation_order = null, // Not serialized, will be recomputed if needed
.idents = self.idents,
.deferred_numeric_literals = self.deferred_numeric_literals.deserialize(offset).*,
.import_mapping = types_mod.import_mapping.ImportMapping.init(gpa),
.method_idents = self.method_idents.deserialize(offset).*,
.rigid_vars = std.AutoHashMapUnmanaged(Ident.Idx, TypeVar){},
};
return env;
}
};
/// Convert a type into a node index
pub fn nodeIdxFrom(idx: anytype) Node.Idx {
return @enumFromInt(@intFromEnum(idx));
}
/// Convert a type into a type var
pub fn varFrom(idx: anytype) TypeVar {
return @enumFromInt(@intFromEnum(idx));
}
/// Adds an identifier to the list of exposed items by its identifier index.
pub fn addExposedById(self: *Self, ident_idx: Ident.Idx) !void {
return try self.common.exposed_items.addExposedById(self.gpa, @bitCast(ident_idx));
}
/// Associates a node index with an exposed identifier.
pub fn setExposedNodeIndexById(self: *Self, ident_idx: Ident.Idx, node_idx: u16) !void {
return try self.common.exposed_items.setNodeIndexById(self.gpa, @bitCast(ident_idx), node_idx);
}
/// Retrieves the node index associated with an exposed identifier, if any.
pub fn getExposedNodeIndexById(self: *const Self, ident_idx: Ident.Idx) ?u16 {
return self.common.getNodeIndexById(self.gpa, ident_idx);
}
/// Get the exposed node index for a type given its statement index.
/// This is used for auto-imported builtin types where we have the statement index pre-computed.
/// For auto-imported types, the statement index IS the node/var index directly.
pub fn getExposedNodeIndexByStatementIdx(self: *const Self, stmt_idx: CIR.Statement.Idx) ?u16 {
_ = self; // Not needed for this simplified implementation
// For auto-imported builtin types (Bool, Try, etc.), the statement index
// IS the node/var index. This is because type declarations get type variables
// indexed by their statement index, not by their position in arrays.
const node_idx: u16 = @intCast(@intFromEnum(stmt_idx));
return node_idx;
}
/// Ensures that the exposed items are sorted by identifier index.
pub fn ensureExposedSorted(self: *Self, allocator: std.mem.Allocator) void {
self.common.exposed_items.ensureSorted(allocator);
}
/// Checks whether the given identifier is exposed by this module.
pub fn containsExposedById(self: *const Self, ident_idx: Ident.Idx) bool {
return self.common.exposed_items.containsById(self.gpa, @bitCast(ident_idx));
}
/// Assert that nodes and regions are in sync
pub inline fn debugAssertArraysInSync(self: *const Self) void {
if (builtin.mode == .Debug) {
const cir_nodes = self.store.nodes.items.len;
const region_nodes = self.store.regions.len();
if (!(cir_nodes == region_nodes)) {
std.debug.panic(
"Arrays out of sync:\n cir_nodes={}\n region_nodes={}\n",
.{ cir_nodes, region_nodes },
);
}
}
}
/// Assert that nodes, regions and types are all in sync
inline fn debugAssertIdxsEql(comptime desc: []const u8, idx1: anytype, idx2: anytype) void {
if (builtin.mode == .Debug) {
const idx1_int = @intFromEnum(idx1);
const idx2_int = @intFromEnum(idx2);
if (idx1_int != idx2_int) {
std.debug.panic(
"{s} idxs out of sync: {} != {}\n",
.{ desc, idx1_int, idx2_int },
);
}
}
}
/// Add a new expression to the node store.
/// This function asserts that the nodes and regions are in sync.
pub fn addDef(self: *Self, expr: CIR.Def, region: Region) std.mem.Allocator.Error!CIR.Def.Idx {
const expr_idx = try self.store.addDef(expr, region);
self.debugAssertArraysInSync();
return expr_idx;
}
/// Add a new type header to the node store.
/// This function asserts that the nodes and regions are in sync.
pub fn addTypeHeader(self: *Self, expr: CIR.TypeHeader, region: Region) std.mem.Allocator.Error!CIR.TypeHeader.Idx {
const expr_idx = try self.store.addTypeHeader(expr, region);
self.debugAssertArraysInSync();
return expr_idx;
}
/// Add a new statement to the node store.
/// This function asserts that the nodes and regions are in sync.
pub fn addStatement(self: *Self, expr: CIR.Statement, region: Region) std.mem.Allocator.Error!CIR.Statement.Idx {
const expr_idx = try self.store.addStatement(expr, region);
self.debugAssertArraysInSync();
return expr_idx;
}
/// Add a new pattern to the node store.
/// This function asserts that the nodes and regions are in sync.
pub fn addPattern(self: *Self, expr: CIR.Pattern, region: Region) std.mem.Allocator.Error!CIR.Pattern.Idx {
const expr_idx = try self.store.addPattern(expr, region);
self.debugAssertArraysInSync();
return expr_idx;
}
/// Add a new expression to the node store.
/// This function asserts that the nodes and regions are in sync.
pub fn addExpr(self: *Self, expr: CIR.Expr, region: Region) std.mem.Allocator.Error!CIR.Expr.Idx {
const expr_idx = try self.store.addExpr(expr, region);
self.debugAssertArraysInSync();
return expr_idx;
}
/// Add a new capture to the node store.
/// This function asserts that the nodes and regions are in sync.
pub fn addCapture(self: *Self, capture: CIR.Expr.Capture, region: Region) std.mem.Allocator.Error!CIR.Expr.Capture.Idx {
const capture_idx = try self.store.addCapture(capture, region);
self.debugAssertArraysInSync();
return capture_idx;
}
/// Add a new record field to the node store.
/// This function asserts that the nodes and regions are in sync.
pub fn addRecordField(self: *Self, expr: CIR.RecordField, region: Region) std.mem.Allocator.Error!CIR.RecordField.Idx {
const expr_idx = try self.store.addRecordField(expr, region);
self.debugAssertArraysInSync();
return expr_idx;
}
/// Add a new record destructuring to the node store.
/// This function asserts that the nodes and regions are in sync.
pub fn addRecordDestruct(self: *Self, expr: CIR.Pattern.RecordDestruct, region: Region) std.mem.Allocator.Error!CIR.Pattern.RecordDestruct.Idx {
const expr_idx = try self.store.addRecordDestruct(expr, region);
self.debugAssertArraysInSync();
return expr_idx;
}
/// Adds a new if branch to the store.
/// This function asserts that the nodes and regions are in sync.
pub fn addIfBranch(self: *Self, expr: CIR.Expr.IfBranch, region: Region) std.mem.Allocator.Error!CIR.Expr.IfBranch.Idx {
const expr_idx = try self.store.addIfBranch(expr, region);
self.debugAssertArraysInSync();
return expr_idx;
}
/// Add a new match branch to the node store.
/// This function asserts that the nodes and regions are in sync.
pub fn addMatchBranch(self: *Self, expr: CIR.Expr.Match.Branch, region: Region) std.mem.Allocator.Error!CIR.Expr.Match.Branch.Idx {
const expr_idx = try self.store.addMatchBranch(expr, region);
self.debugAssertArraysInSync();
return expr_idx;
}
/// Add a new where clause to the node store.
/// This function asserts that the nodes and regions are in sync.
pub fn addWhereClause(self: *Self, expr: CIR.WhereClause, region: Region) std.mem.Allocator.Error!CIR.WhereClause.Idx {
const expr_idx = try self.store.addWhereClause(expr, region);
self.debugAssertArraysInSync();
return expr_idx;
}
/// Add a new type annotation to the node store.
/// This function asserts that the nodes and regions are in sync.
pub fn addTypeAnno(self: *Self, expr: CIR.TypeAnno, region: Region) std.mem.Allocator.Error!CIR.TypeAnno.Idx {
const expr_idx = try self.store.addTypeAnno(expr, region);
self.debugAssertArraysInSync();
return expr_idx;
}
/// Add a new annotation to the node store.
/// This function asserts that the nodes and regions are in sync.
pub fn addAnnotation(self: *Self, expr: CIR.Annotation, region: Region) std.mem.Allocator.Error!CIR.Annotation.Idx {
const expr_idx = try self.store.addAnnotation(expr, region);
self.debugAssertArraysInSync();
return expr_idx;
}
/// Add a new record field to the node store.
/// This function asserts that the nodes and regions are in sync.
pub fn addAnnoRecordField(self: *Self, expr: CIR.TypeAnno.RecordField, region: Region) std.mem.Allocator.Error!CIR.TypeAnno.RecordField.Idx {
const expr_idx = try self.store.addAnnoRecordField(expr, region);
self.debugAssertArraysInSync();
return expr_idx;
}
/// Add a new exposed item to the node store.
/// This function asserts that the nodes and regions are in sync.
pub fn addExposedItem(self: *Self, expr: CIR.ExposedItem, region: Region) std.mem.Allocator.Error!CIR.ExposedItem.Idx {
const expr_idx = try self.store.addExposedItem(expr, region);
self.debugAssertArraysInSync();
return expr_idx;
}
/// Add a diagnostic.
/// This function asserts that the nodes and regions are in sync.
pub fn addDiagnostic(self: *Self, reason: CIR.Diagnostic) std.mem.Allocator.Error!CIR.Diagnostic.Idx {
const expr_idx = try self.store.addDiagnostic(reason);
self.debugAssertArraysInSync();
return expr_idx;
}
/// Add a new malformed node to the node store.
/// This function asserts that the nodes and regions are in sync.
pub fn addMalformed(self: *Self, diagnostic_idx: CIR.Diagnostic.Idx, region: Region) std.mem.Allocator.Error!CIR.Node.Idx {
const malformed_idx = try self.store.addMalformed(diagnostic_idx, region);
self.debugAssertArraysInSync();
return malformed_idx;
}
/// Add a new match branch pattern to the node store.
/// This function asserts that the nodes and regions are in sync.
pub fn addMatchBranchPattern(self: *Self, expr: CIR.Expr.Match.BranchPattern, region: Region) std.mem.Allocator.Error!CIR.Expr.Match.BranchPattern.Idx {
const expr_idx = try self.store.addMatchBranchPattern(expr, region);
self.debugAssertArraysInSync();
return expr_idx;
}
/// Add a new type variable to the node store.
/// This function asserts that the nodes and regions are in sync.
pub fn addTypeSlot(
self: *Self,
parent_node: CIR.Node.Idx,
region: Region,
comptime RetIdx: type,
) std.mem.Allocator.Error!RetIdx {
comptime if (!isCastable(RetIdx)) @compileError("Idx type " ++ @typeName(RetIdx) ++ " is not castable");
const node_idx = try self.store.addTypeVarSlot(parent_node, region);
self.debugAssertArraysInSync();
return @enumFromInt(@intFromEnum(node_idx));
}
/// Adds an external declaration and returns its index
pub fn pushExternalDecl(self: *Self, decl: CIR.ExternalDecl) std.mem.Allocator.Error!CIR.ExternalDecl.Idx {
const idx = @as(u32, @intCast(self.external_decls.len()));
_ = try self.external_decls.append(self.gpa, decl);
return @enumFromInt(idx);
}
/// Retrieves an external declaration by its index
pub fn getExternalDecl(self: *const Self, idx: CIR.ExternalDecl.Idx) *const CIR.ExternalDecl {
return self.external_decls.get(@as(CIR.ExternalDecl.SafeList.Idx, @enumFromInt(@intFromEnum(idx))));
}
/// Adds multiple external declarations and returns a span
pub fn pushExternalDecls(self: *Self, decls: []const CIR.ExternalDecl) std.mem.Allocator.Error!CIR.ExternalDecl.Span {
const start = @as(u32, @intCast(self.external_decls.len()));
for (decls) |decl| {
_ = try self.external_decls.append(self.gpa, decl);
}
return CIR.ExternalDecl.Span{ .span = .{ .start = start, .len = @as(u32, @intCast(decls.len)) } };
}
/// Gets a slice of external declarations from a span
pub fn sliceExternalDecls(self: *const Self, span: CIR.ExternalDecl.Span) []const CIR.ExternalDecl {
const range = CIR.ExternalDecl.SafeList.Range{ .start = @enumFromInt(span.span.start), .count = span.span.len };
return self.external_decls.sliceRange(range);
}
/// Retrieves the text of an identifier by its index
pub fn getIdentText(self: *const Self, idx: Ident.Idx) []const u8 {
return self.getIdent(idx);
}
/// Helper to format pattern index for s-expr output
fn formatPatternIdxNode(gpa: std.mem.Allocator, pattern_idx: CIR.Pattern.Idx) SExpr {
var node = SExpr.init(gpa, "pid");
node.appendUnsignedInt(gpa, @intFromEnum(pattern_idx));
return node;
}
/// Helper function to generate the S-expression node for the entire module.
/// If a single expression is provided, only that expression is returned.
pub fn pushToSExprTree(self: *Self, maybe_expr_idx: ?CIR.Expr.Idx, tree: *SExprTree) std.mem.Allocator.Error!void {
if (maybe_expr_idx) |expr_idx| {
// Only output the given expression
try self.store.getExpr(expr_idx).pushToSExprTree(self, tree, expr_idx);
} else {
const root_begin = tree.beginNode();
try tree.pushStaticAtom("can-ir");
// Iterate over all the definitions in the file and convert each to an S-expression tree
const defs_slice = self.store.sliceDefs(self.all_defs);
const statements_slice = self.store.sliceStatements(self.all_statements);
if (defs_slice.len == 0 and statements_slice.len == 0 and self.external_decls.len() == 0) {
try tree.pushBoolPair("empty", true);
}
const attrs = tree.beginNode();
for (defs_slice) |def_idx| {
try self.store.getDef(def_idx).pushToSExprTree(self, tree);
}
for (statements_slice) |stmt_idx| {
try self.store.getStatement(stmt_idx).pushToSExprTree(self, tree, stmt_idx);
}
for (0..@intCast(self.external_decls.len())) |i| {
const external_decl = self.external_decls.get(@enumFromInt(i));
try external_decl.pushToSExprTree(self, tree);
}
try tree.endNode(root_begin, attrs);
}
}
/// Append region information to an S-expression node for a given index.
pub fn appendRegionInfoToSExprTree(self: *const Self, tree: *SExprTree, idx: anytype) std.mem.Allocator.Error!void {
const region = self.store.getNodeRegion(@enumFromInt(@intFromEnum(idx)));
try self.appendRegionInfoToSExprTreeFromRegion(tree, region);
}
/// Append region information to an S-expression node from a specific region.
pub fn appendRegionInfoToSExprTreeFromRegion(self: *const Self, tree: *SExprTree, region: Region) std.mem.Allocator.Error!void {
const info = self.getRegionInfo(region) catch RegionInfo{
.start_line_idx = 0,
.start_col_idx = 0,
.end_line_idx = 0,
.end_col_idx = 0,
};
try tree.pushBytesRange(
region.start.offset,
region.end.offset,
info,
);
}
/// Get region information for a node.
pub fn getNodeRegionInfo(self: *const Self, idx: anytype) RegionInfo {
const region = self.store.getNodeRegion(@enumFromInt(@intFromEnum(idx)));
return self.getRegionInfo(region);
}
/// Helper function to convert type information to an SExpr node
/// in S-expression format for snapshot testing. Implements the definition-focused
/// format showing final types for defs, expressions, and builtins.
pub fn pushTypesToSExprTree(self: *Self, maybe_expr_idx: ?CIR.Expr.Idx, tree: *SExprTree) std.mem.Allocator.Error!void {
if (maybe_expr_idx) |expr_idx| {
try self.pushExprTypesToSExprTree(expr_idx, tree);
} else {
// Generate full type information for all definitions and expressions
const root_begin = tree.beginNode();
try tree.pushStaticAtom("inferred-types");
const root_attrs = tree.beginNode();
// Create defs section
const defs_begin = tree.beginNode();
try tree.pushStaticAtom("defs");
const defs_attrs = tree.beginNode();
// Iterate through all definitions to extract pattern types
const defs_slice = self.store.sliceDefs(self.all_defs);
for (defs_slice) |def_idx| {
const def = self.store.getDef(def_idx);
// Only process assign patterns - skip destructuring patterns
const pattern = self.store.getPattern(def.pattern);
switch (pattern) {
.assign => {},
else => continue, // Skip non-assign patterns (like destructuring)
}
const pattern_var = varFrom(def.pattern);
// Get the region for this definition
const pattern_node_idx: CIR.Node.Idx = @enumFromInt(@intFromEnum(def.pattern));
const pattern_region = self.store.getRegionAt(pattern_node_idx);
// Create a TypeWriter to format the type
var type_writer = self.initTypeWriter() catch continue;
defer type_writer.deinit();
// Write the type to the buffer
type_writer.write(pattern_var) catch continue;
// Add the pattern type entry
const patt_begin = tree.beginNode();
try tree.pushStaticAtom("patt");
try self.appendRegionInfoToSExprTreeFromRegion(tree, pattern_region);
const type_str = type_writer.get();
try tree.pushStringPair("type", type_str);
try tree.endNode(patt_begin, tree.beginNode());
}
try tree.endNode(defs_begin, defs_attrs);
// Check if we have any type declarations to output
const all_stmts = self.store.sliceStatements(self.all_statements);
var has_type_decl = false;
for (all_stmts) |stmt_idx| {
const stmt = self.store.getStatement(stmt_idx);
switch (stmt) {
.s_alias_decl, .s_nominal_decl => {
has_type_decl = true;
break;
},
else => continue,
}
}
// Create type_decls section if we have any type declarations
if (has_type_decl) {
const type_decls_begin = tree.beginNode();
try tree.pushStaticAtom("type_decls");
const type_decls_attrs = tree.beginNode();
for (all_stmts) |stmt_idx| {
const stmt = self.store.getStatement(stmt_idx);
switch (stmt) {
.s_alias_decl => |alias| {
const stmt_begin = tree.beginNode();
try tree.pushStaticAtom("alias");
// Add region info for the statement
const stmt_region = self.store.getStatementRegion(stmt_idx);
try self.appendRegionInfoToSExprTreeFromRegion(tree, stmt_region);
// Get the type variable for this statement
const stmt_var = varFrom(stmt_idx);
// Create a TypeWriter to format the type
var type_writer = self.initTypeWriter() catch continue;
defer type_writer.deinit();
// Write the type to the buffer
type_writer.write(stmt_var) catch continue;
const type_str = type_writer.get();
try tree.pushStringPair("type", type_str);
const stmt_attrs = tree.beginNode();
// Add the type header
const header = self.store.getTypeHeader(alias.header);
try header.pushToSExprTree(self, tree, alias.header);
try tree.endNode(stmt_begin, stmt_attrs);
},
.s_nominal_decl => |nominal| {
const stmt_begin = tree.beginNode();
try tree.pushStaticAtom("nominal");
// Add region info for the statement
const stmt_region = self.store.getStatementRegion(stmt_idx);
try self.appendRegionInfoToSExprTreeFromRegion(tree, stmt_region);
// Get the type variable for this statement
const stmt_var = varFrom(stmt_idx);
// Create a TypeWriter to format the type
var type_writer = self.initTypeWriter() catch continue;
defer type_writer.deinit();
// Write the type to the buffer
type_writer.write(stmt_var) catch continue;
const type_str = type_writer.get();
try tree.pushStringPair("type", type_str);
const stmt_attrs = tree.beginNode();
// Add the type header
const header = self.store.getTypeHeader(nominal.header);
try header.pushToSExprTree(self, tree, nominal.header);
try tree.endNode(stmt_begin, stmt_attrs);
},
else => continue,
}
}
try tree.endNode(type_decls_begin, type_decls_attrs);
}
// Create expressions section
const exprs_begin = tree.beginNode();
try tree.pushStaticAtom("expressions");
const exprs_attrs = tree.beginNode();
// Iterate through all definitions to extract expression types
for (defs_slice) |def_idx| {
const def = self.store.getDef(def_idx);
const expr_var = varFrom(def.expr);
// Get the region for this expression
const expr_node_idx: CIR.Node.Idx = @enumFromInt(@intFromEnum(def.expr));
const expr_region = self.store.getRegionAt(expr_node_idx);
// Create a TypeWriter to format the type
var type_writer = self.initTypeWriter() catch continue;
defer type_writer.deinit();
// Write the type to the buffer
type_writer.write(expr_var) catch continue;
// Add the expression type entry
const expr_begin = tree.beginNode();
try tree.pushStaticAtom("expr");
try self.appendRegionInfoToSExprTreeFromRegion(tree, expr_region);
const type_str = type_writer.get();
try tree.pushStringPair("type", type_str);
try tree.endNode(expr_begin, tree.beginNode());
}
try tree.endNode(exprs_begin, exprs_attrs);
try tree.endNode(root_begin, root_attrs);
}
}
fn pushExprTypesToSExprTree(self: *Self, expr_idx: CIR.Expr.Idx, tree: *SExprTree) std.mem.Allocator.Error!void {
const expr_begin = tree.beginNode();
try tree.pushStaticAtom("expr");
// Add region info for the expression
try self.appendRegionInfoToSExprTree(tree, expr_idx);
// Get the type variable for this expression
const expr_var = varFrom(expr_idx);
// Create a TypeWriter to format the type
var type_writer = try self.initTypeWriter();
defer type_writer.deinit();
// Write the type to the buffer
try type_writer.write(expr_var);
// Add the formatted type to the S-expression tree
const type_str = type_writer.get();
try tree.pushStringPair("type", type_str);
try tree.endNode(expr_begin, tree.beginNode());
}
/// Retrieves a string literal by its index from the common environment.
pub fn getString(self: *const Self, idx: StringLiteral.Idx) []const u8 {
return self.common.getString(idx);
}
/// Inserts a string literal into the common environment and returns its index.
pub fn insertString(self: *Self, string: []const u8) std.mem.Allocator.Error!StringLiteral.Idx {
return try self.common.insertString(self.gpa, string);
}
/// Returns a mutable reference to the identifier store.
pub fn getIdentStore(self: *Self) *Ident.Store {
return &self.common.idents;
}
/// Returns an immutable reference to the identifier store.
pub fn getIdentStoreConst(self: *const Self) *const Ident.Store {
return &self.common.idents;
}
/// Retrieves the text of an identifier by its index.
pub fn getIdent(self: *const Self, idx: Ident.Idx) []const u8 {
return self.common.getIdent(idx);
}
/// Get the source text for a given region
pub fn getSource(self: *const Self, region: Region) []const u8 {
return self.common.getSource(region);
}
/// Get the entire source text. This is primarily needed for diagnostic output
/// where `addSourceRegion` requires access to the full source and line starts
/// to render error messages with context lines.
///
/// For extracting source text for a specific region, prefer `getSource(region)` instead.
pub fn getSourceAll(self: *const Self) []const u8 {
return self.common.getSourceAll();
}
/// Get all line start offsets. This is primarily needed for diagnostic output
/// where `addSourceRegion` requires access to the full source and line starts
/// to render error messages with context lines.
pub fn getLineStartsAll(self: *const Self) []const u32 {
return self.common.getLineStartsAll();
}
pub fn initTypeWriter(self: *Self) std.mem.Allocator.Error!TypeWriter {
return TypeWriter.initFromParts(self.gpa, &self.types, self.getIdentStore(), null);
}
/// Inserts an identifier into the common environment and returns its index.
pub fn insertIdent(self: *Self, ident: Ident) std.mem.Allocator.Error!Ident.Idx {
return try self.common.insertIdent(self.gpa, ident);
}
/// Creates and inserts a qualified identifier (e.g., "Foo.bar") into the common environment.
/// This handles the full lifecycle: building the qualified name, creating the Ident,
/// inserting it into the store, and cleaning up any temporary allocations.
/// All memory management is handled internally with no caller obligations.
pub fn insertQualifiedIdent(
self: *Self,
parent: []const u8,
child: []const u8,
) std.mem.Allocator.Error!Ident.Idx {
const total_len = parent.len + 1 + child.len; // parent + '.' + child
if (total_len <= 256) {
// Use stack buffer for small identifiers
var buf: [256]u8 = undefined;
const qualified = std.fmt.bufPrint(&buf, "{s}.{s}", .{ parent, child }) catch unreachable;
return try self.insertIdent(Ident.for_text(qualified));
} else {
// Use heap allocation for large identifiers
const qualified = try std.fmt.allocPrint(self.gpa, "{s}.{s}", .{ parent, child });
defer self.gpa.free(qualified);
return try self.insertIdent(Ident.for_text(qualified));
}
}
/// Looks up a method identifier on a type by building the qualified method name.
/// This handles cross-module method lookup by building names like "Builtin.Num.U64.from_numeral".
///
/// Parameters:
/// - type_name: The type's identifier text (e.g., "Num.U64" or "Bool")
/// - method_name: The unqualified method name (e.g., "from_numeral")
///
/// Returns the method's ident index if found, or null if the method doesn't exist.
/// This is a read-only operation that doesn't modify the ident store.
pub fn getMethodIdent(self: *const Self, type_name: []const u8, method_name: []const u8) ?Ident.Idx {
// Build the qualified method name: "{type_name}.{method_name}"
// The type_name may already include the module prefix (e.g., "Num.U64")
// or just be the type name (e.g., "Bool" for Builtin.Bool)
const total_len = self.module_name.len + 1 + type_name.len + 1 + method_name.len;
if (total_len <= 256) {
// Use stack buffer for small identifiers
var buf: [256]u8 = undefined;
// Check if type_name already starts with module_name
if (type_name.len > self.module_name.len and
std.mem.startsWith(u8, type_name, self.module_name) and
type_name[self.module_name.len] == '.')
{
// Type name is already qualified (e.g., "Builtin.Bool")
const qualified = std.fmt.bufPrint(&buf, "{s}.{s}", .{ type_name, method_name }) catch return null;
return self.getIdentStoreConst().findByString(qualified);
} else if (std.mem.eql(u8, type_name, self.module_name)) {
// Type name IS the module name (e.g., looking up method on "Builtin" itself)
const qualified = std.fmt.bufPrint(&buf, "{s}.{s}", .{ type_name, method_name }) catch return null;
return self.getIdentStoreConst().findByString(qualified);
} else {
// Try module-qualified name first (e.g., "Builtin.Num.U64.from_numeral")
const qualified = std.fmt.bufPrint(&buf, "{s}.{s}.{s}", .{ self.module_name, type_name, method_name }) catch return null;
if (self.getIdentStoreConst().findByString(qualified)) |idx| {
return idx;
}
// Fallback: try without module prefix (e.g., "Color.as_str" for app-defined types)
// This handles the case where methods are registered with just the type-qualified name
const simple_qualified = std.fmt.bufPrint(&buf, "{s}.{s}", .{ type_name, method_name }) catch return null;
return self.getIdentStoreConst().findByString(simple_qualified);
}
} else {
// Use heap allocation for large identifiers (rare case)
// Try module-qualified name first
const qualified = if (type_name.len > self.module_name.len and
std.mem.startsWith(u8, type_name, self.module_name) and
type_name[self.module_name.len] == '.')
std.fmt.allocPrint(self.gpa, "{s}.{s}", .{ type_name, method_name }) catch return null
else if (std.mem.eql(u8, type_name, self.module_name))
std.fmt.allocPrint(self.gpa, "{s}.{s}", .{ type_name, method_name }) catch return null
else
std.fmt.allocPrint(self.gpa, "{s}.{s}.{s}", .{ self.module_name, type_name, method_name }) catch return null;
defer self.gpa.free(qualified);
if (self.getIdentStoreConst().findByString(qualified)) |idx| {
return idx;
}
// Fallback for the module-qualified case
if (type_name.len <= self.module_name.len or
!std.mem.startsWith(u8, type_name, self.module_name) or
type_name[self.module_name.len] != '.')
{
const simple_qualified = std.fmt.allocPrint(self.gpa, "{s}.{s}", .{ type_name, method_name }) catch return null;
defer self.gpa.free(simple_qualified);
return self.getIdentStoreConst().findByString(simple_qualified);
}
return null;
}
}
/// Registers a method identifier mapping for fast index-based lookup.
/// This should be called during canonicalization when a method is defined in an associated block.
///
/// Parameters:
/// - type_ident: The type's identifier index (e.g., the ident for "Bool")
/// - method_ident: The method's identifier index (e.g., the ident for "is_eq")
/// - qualified_ident: The qualified method ident (e.g., "Bool.is_eq")
pub fn registerMethodIdent(self: *Self, type_ident: Ident.Idx, method_ident: Ident.Idx, qualified_ident: Ident.Idx) !void {
const key = MethodKey{ .type_ident = type_ident, .method_ident = method_ident };
try self.method_idents.put(self.gpa, key, qualified_ident);
}
/// Looks up a method identifier by type and method ident indices.
/// This is the fast O(log n) index-based lookup that avoids string comparison.
///
/// Parameters:
/// - type_ident: The type's identifier index (must be in this module's ident store)
/// - method_ident: The method's identifier index (must be in this module's ident store)
///
/// Returns the qualified method's ident index if found, or null if not registered.
pub fn lookupMethodIdent(self: *Self, type_ident: Ident.Idx, method_ident: Ident.Idx) ?Ident.Idx {
const key = MethodKey{ .type_ident = type_ident, .method_ident = method_ident };
return self.method_idents.get(self.gpa, key);
}
/// Looks up a method identifier by type and method ident indices (const version).
/// This is the fast O(log n) index-based lookup that avoids string comparison.
pub fn lookupMethodIdentConst(self: *const Self, type_ident: Ident.Idx, method_ident: Ident.Idx) ?Ident.Idx {
const key = MethodKey{ .type_ident = type_ident, .method_ident = method_ident };
// Cast away const for the get operation (it doesn't modify the structure, just ensures sorted)
const mutable_self = @constCast(self);
return mutable_self.method_idents.get(self.gpa, key);
}
/// Looks up a method identifier by translating idents from a source environment.
/// This first finds the corresponding idents in this module, then does index-based lookup.
///
/// Parameters:
/// - source_env: The module environment where type_ident and method_ident are from
/// - type_ident: The type's identifier index in source_env
/// - method_ident: The method's identifier index in source_env
///
/// Returns the qualified method's ident index if found, or null if the method doesn't exist.
/// Falls back to string-based getMethodIdent for backward compatibility with pre-compiled modules.
pub fn lookupMethodIdentFromEnv(self: *Self, source_env: *const Self, type_ident: Ident.Idx, method_ident: Ident.Idx) ?Ident.Idx {
// First, try to find the type and method idents in our own ident store
const type_name = source_env.getIdent(type_ident);
const method_name = source_env.getIdent(method_ident);
// Find corresponding idents in this module
const local_type_ident = self.common.findIdent(type_name) orelse return null;
const local_method_ident = self.common.findIdent(method_name) orelse return null;
// Try index-based lookup first (O(log n))
if (self.lookupMethodIdent(local_type_ident, local_method_ident)) |result| {
return result;
}
// Fall back to string-based lookup for backward compatibility with pre-compiled modules
// that don't have method_idents populated. This can be removed once all modules are recompiled.
return self.getMethodIdent(type_name, method_name);
}
/// Const version of lookupMethodIdentFromEnv for use with immutable module environments.
/// Safe to use on deserialized modules since method_idents is already sorted.
/// Falls back to string-based getMethodIdent for backward compatibility with pre-compiled modules.
pub fn lookupMethodIdentFromEnvConst(self: *const Self, source_env: *const Self, type_ident: Ident.Idx, method_ident: Ident.Idx) ?Ident.Idx {
// First, try to find the type and method idents in our own ident store
const type_name = source_env.getIdent(type_ident);
const method_name = source_env.getIdent(method_ident);
// Find corresponding idents in this module
const local_type_ident = self.common.findIdent(type_name) orelse return null;
const local_method_ident = self.common.findIdent(method_name) orelse return null;
// Try index-based lookup first (O(log n))
if (self.lookupMethodIdentConst(local_type_ident, local_method_ident)) |result| {
return result;
}
// Fall back to string-based lookup for backward compatibility with pre-compiled modules
// that don't have method_idents populated. This can be removed once all modules are recompiled.
return self.getMethodIdent(type_name, method_name);
}
/// Looks up a method identifier when the type and method idents come from different source environments.
/// This is needed when e.g. type_ident is from runtime layout store and method_ident is from CIR.
/// Falls back to string-based getMethodIdent for backward compatibility with pre-compiled modules.
pub fn lookupMethodIdentFromTwoEnvsConst(
self: *const Self,
type_source_env: *const Self,
type_ident: Ident.Idx,
method_source_env: *const Self,
method_ident: Ident.Idx,
) ?Ident.Idx {
// Get strings from respective source environments
const type_name = type_source_env.getIdent(type_ident);
const method_name = method_source_env.getIdent(method_ident);
// Find corresponding idents in this module
const local_type_ident = self.common.findIdent(type_name) orelse return null;
const local_method_ident = self.common.findIdent(method_name) orelse return null;
// Try index-based lookup first (O(log n))
if (self.lookupMethodIdentConst(local_type_ident, local_method_ident)) |result| {
return result;
}
// Fall back to string-based lookup for backward compatibility with pre-compiled modules
// that don't have method_idents populated. This can be removed once all modules are recompiled.
return self.getMethodIdent(type_name, method_name);
}
/// Returns the line start positions for source code position mapping.
/// Each element represents the byte offset where a new line begins.
pub fn getLineStarts(self: *const Self) []const u32 {
return self.common.getLineStartsAll();
}