mirror of
https://github.com/roc-lang/roc.git
synced 2025-12-23 08:48:03 +00:00
950 lines
36 KiB
Zig
950 lines
36 KiB
Zig
//! Type serialization utilities for writing type information as S-expressions.
|
|
//!
|
|
//! This module provides functionality to serialize type store contents and
|
|
//! individual types into S-expression format for debugging, inspection, and
|
|
//! external tool integration. The serialized output helps visualize the
|
|
//! compiler's internal type representations.
|
|
|
|
const std = @import("std");
|
|
const base = @import("base");
|
|
const types_mod = @import("types.zig");
|
|
|
|
const TypesStore = @import("store.zig").Store;
|
|
const Allocator = std.mem.Allocator;
|
|
const Desc = types_mod.Descriptor;
|
|
const Var = types_mod.Var;
|
|
const Content = types_mod.Content;
|
|
const Rank = types_mod.Rank;
|
|
const Mark = types_mod.Mark;
|
|
const RecordField = types_mod.RecordField;
|
|
const TagUnion = types_mod.TagUnion;
|
|
const Tag = types_mod.Tag;
|
|
const VarSafeList = Var.SafeList;
|
|
const RecordFieldSafeMultiList = RecordField.SafeMultiList;
|
|
const TagSafeMultiList = Tag.SafeMultiList;
|
|
const Descriptor = types_mod.Descriptor;
|
|
const TypeIdent = types_mod.TypeIdent;
|
|
const Alias = types_mod.Alias;
|
|
const FlatType = types_mod.FlatType;
|
|
const NominalType = types_mod.NominalType;
|
|
const Record = types_mod.Record;
|
|
const Num = types_mod.Num;
|
|
const Tuple = types_mod.Tuple;
|
|
const Func = types_mod.Func;
|
|
|
|
// const SExpr = base.SExpr;
|
|
const Ident = base.Ident;
|
|
|
|
const TypeContext = enum {
|
|
General,
|
|
NumContent,
|
|
ListContent,
|
|
RecordExtension,
|
|
TagUnionExtension,
|
|
RecordFieldContent,
|
|
TupleFieldContent,
|
|
FunctionArgument,
|
|
FunctionReturn,
|
|
};
|
|
|
|
/// Helper that accepts a `Var` and write it as a nice string.
|
|
/// Entry point is `writeVar`
|
|
const TypeWriter = @This();
|
|
|
|
types: *const TypesStore,
|
|
idents: *const Ident.Store,
|
|
buf: std.array_list.Managed(u8),
|
|
seen: std.array_list.Managed(Var),
|
|
seen_count_var_occurrences: std.array_list.Managed(Var),
|
|
next_name_index: u32,
|
|
name_counters: std.EnumMap(TypeContext, u32),
|
|
flex_var_names_map: std.AutoHashMap(Var, FlexVarNameRange),
|
|
flex_var_names: std.array_list.Managed(u8),
|
|
static_dispatch_constraints: std.array_list.Managed(types_mod.StaticDispatchConstraint),
|
|
|
|
const FlexVarNameRange = struct { start: usize, end: usize };
|
|
|
|
/// Initialize a TypeWriter with immutable types and idents references.
|
|
pub fn initFromParts(gpa: std.mem.Allocator, types_store: *const TypesStore, idents: *const Ident.Store) std.mem.Allocator.Error!TypeWriter {
|
|
return .{
|
|
.types = types_store,
|
|
.idents = idents,
|
|
.buf = try std.array_list.Managed(u8).initCapacity(gpa, 32),
|
|
.seen = try std.array_list.Managed(Var).initCapacity(gpa, 16),
|
|
.seen_count_var_occurrences = try std.array_list.Managed(Var).initCapacity(gpa, 16),
|
|
.next_name_index = 0,
|
|
.name_counters = std.EnumMap(TypeContext, u32).init(.{}),
|
|
.flex_var_names_map = std.AutoHashMap(Var, FlexVarNameRange).init(gpa),
|
|
.flex_var_names = try std.array_list.Managed(u8).initCapacity(gpa, 32),
|
|
.static_dispatch_constraints = try std.array_list.Managed(types_mod.StaticDispatchConstraint).initCapacity(gpa, 32),
|
|
};
|
|
}
|
|
|
|
/// Deinit type writer
|
|
pub fn deinit(self: *TypeWriter) void {
|
|
self.buf.deinit();
|
|
self.seen.deinit();
|
|
self.seen_count_var_occurrences.deinit();
|
|
self.flex_var_names_map.deinit();
|
|
self.flex_var_names.deinit();
|
|
self.static_dispatch_constraints.deinit();
|
|
}
|
|
|
|
/// Reset type writer state
|
|
pub fn reset(self: *TypeWriter) void {
|
|
self.buf.clearRetainingCapacity();
|
|
self.seen.clearRetainingCapacity();
|
|
self.seen_count_var_occurrences.clearRetainingCapacity();
|
|
self.flex_var_names_map.clearRetainingCapacity();
|
|
self.flex_var_names.clearRetainingCapacity();
|
|
self.static_dispatch_constraints.clearRetainingCapacity();
|
|
|
|
self.next_name_index = 0;
|
|
self.name_counters = std.EnumMap(TypeContext, u32).init(.{});
|
|
}
|
|
|
|
/// Writes the current var into the the writers buffer and returns a bytes slice
|
|
pub fn writeGet(self: *TypeWriter, var_: Var) std.mem.Allocator.Error![]const u8 {
|
|
try self.write(var_);
|
|
return self.get();
|
|
}
|
|
|
|
/// Returns the current contents of the type writer's buffer as a slice.
|
|
/// This contains the formatted type representation built up by write operations.
|
|
pub fn get(self: *const TypeWriter) []const u8 {
|
|
return self.buf.items;
|
|
}
|
|
|
|
/// Writes a type variable to the buffer, formatting it as a human-readable string.
|
|
/// This clears any existing content in the buffer before writing.
|
|
pub fn write(self: *TypeWriter, var_: Var) std.mem.Allocator.Error!void {
|
|
self.reset();
|
|
try self.writeVar(var_, var_);
|
|
|
|
if (self.static_dispatch_constraints.items.len > 0) {
|
|
_ = try self.buf.writer().write(" where [");
|
|
for (self.static_dispatch_constraints.items) |constraint| {
|
|
// TODO: Find a better way to do this
|
|
const dispatcher_var = blk: {
|
|
const fn_resolved = self.types.resolveVar(constraint.fn_var).desc.content;
|
|
std.debug.assert(fn_resolved == .structure);
|
|
|
|
const fn_args = switch (fn_resolved.structure) {
|
|
.fn_effectful => |func| func.args,
|
|
.fn_pure => |func| func.args,
|
|
.fn_unbound => |func| func.args,
|
|
else => {
|
|
std.debug.assert(false);
|
|
continue;
|
|
},
|
|
};
|
|
std.debug.assert(fn_args.len() > 0);
|
|
|
|
break :blk self.types.sliceVars(fn_args)[0];
|
|
};
|
|
|
|
try self.writeVar(dispatcher_var, var_);
|
|
_ = try self.buf.writer().write(".");
|
|
_ = try self.buf.writer().write(self.idents.getText(constraint.fn_name));
|
|
_ = try self.buf.writer().write(" : ");
|
|
try self.writeVar(constraint.fn_var, var_);
|
|
}
|
|
_ = try self.buf.writer().write("]");
|
|
}
|
|
}
|
|
|
|
fn generateNextName(self: *TypeWriter) !void {
|
|
// Generate name: a, b, ..., z, aa, ab, ..., az, ba, ...
|
|
// Skip any names that already exist in the identifier store
|
|
// We need at most one more name than the number of existing identifiers
|
|
const max_attempts = self.idents.interner.entry_count + 1;
|
|
var attempts: usize = 0;
|
|
while (attempts < max_attempts) : (attempts += 1) {
|
|
var n = self.next_name_index;
|
|
self.next_name_index += 1;
|
|
|
|
var name_buf: [8]u8 = undefined;
|
|
var name_len: usize = 0;
|
|
|
|
// Generate name in base-26: a, b, ..., z, aa, ab, ..., az, ba, ...
|
|
while (name_len < name_buf.len) {
|
|
name_buf[name_len] = @intCast('a' + (n % 26));
|
|
name_len += 1;
|
|
n = n / 26;
|
|
if (n == 0) break;
|
|
n -= 1;
|
|
}
|
|
|
|
// Names are generated in reverse order, so reverse the buffer
|
|
std.mem.reverse(u8, name_buf[0..name_len]);
|
|
|
|
// Check if this name already exists in the identifier store
|
|
const candidate_name = name_buf[0..name_len];
|
|
const exists = self.idents.interner.contains(candidate_name);
|
|
|
|
if (!exists) {
|
|
// This name is available, use it
|
|
for (candidate_name) |c| {
|
|
try self.buf.writer().writeByte(c);
|
|
}
|
|
break;
|
|
}
|
|
// Name already exists, try the next one
|
|
}
|
|
|
|
// This should never happen in practice, but let's handle it gracefully
|
|
if (attempts >= max_attempts) {
|
|
_ = try self.buf.writer().write("var");
|
|
try self.buf.writer().print("{}", .{self.next_name_index});
|
|
}
|
|
}
|
|
|
|
fn generateContextualName(self: *TypeWriter, context: TypeContext) std.mem.Allocator.Error!void {
|
|
const base_name = switch (context) {
|
|
.NumContent => "size",
|
|
.ListContent => "elem",
|
|
.RecordExtension => "others",
|
|
.TagUnionExtension => "others",
|
|
.RecordFieldContent => "field",
|
|
.TupleFieldContent => "field",
|
|
.FunctionArgument => "arg",
|
|
.FunctionReturn => "ret",
|
|
.General => {
|
|
// Fall back to generic name generation
|
|
try self.generateNextName();
|
|
return;
|
|
},
|
|
};
|
|
|
|
// Try to generate a name with increasing counters until we find one that doesn't collide
|
|
var counter = self.name_counters.get(context) orelse 0;
|
|
var found = false;
|
|
|
|
// We need at most as many attempts as there are existing identifiers
|
|
const max_attempts = self.idents.interner.entry_count;
|
|
var attempts: usize = 0;
|
|
while (!found and attempts < max_attempts) : (attempts += 1) {
|
|
var buf: [32]u8 = undefined;
|
|
const candidate_name = if (counter == 0)
|
|
base_name
|
|
else blk: {
|
|
const name = std.fmt.bufPrint(&buf, "{s}{}", .{ base_name, counter + 1 }) catch {
|
|
// Buffer too small, fall back to generic name
|
|
try self.generateNextName();
|
|
return;
|
|
};
|
|
break :blk name;
|
|
};
|
|
|
|
// Check if this name already exists in the identifier store
|
|
const exists = self.idents.interner.contains(candidate_name);
|
|
|
|
if (!exists) {
|
|
// This name is available, write it to the buffer
|
|
for (candidate_name) |c| {
|
|
try self.buf.append(c);
|
|
}
|
|
found = true;
|
|
} else {
|
|
// Try next counter
|
|
counter += 1;
|
|
}
|
|
}
|
|
|
|
// If we couldn't find a unique contextual name, fall back to generic names
|
|
if (!found) {
|
|
try self.generateNextName();
|
|
return;
|
|
}
|
|
|
|
self.name_counters.put(context, counter + 1);
|
|
}
|
|
|
|
fn writeNameCheckingCollisions(self: *TypeWriter, candidate_name: []const u8) std.mem.Allocator.Error!void {
|
|
// Check if this name already exists in the identifier store
|
|
var exists = false;
|
|
|
|
// Check all identifiers in the store
|
|
var i: u32 = 0;
|
|
while (i < self.idents.interner.outer_indices.items.len) : (i += 1) {
|
|
const ident_idx = Ident.Idx{ .idx = @truncate(i), .attributes = .{ .effectful = false, .ignored = false, .reassignable = false } };
|
|
const existing_name = self.getIdent(ident_idx);
|
|
if (std.mem.eql(u8, existing_name, candidate_name)) {
|
|
exists = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!exists) {
|
|
// This name is available, write it to the buffer
|
|
for (candidate_name) |c| {
|
|
try self.buf.append(c);
|
|
}
|
|
} else {
|
|
// Name collision - we need to handle this differently
|
|
// For now, just fall back to generic name generation
|
|
try self.generateNextName();
|
|
}
|
|
}
|
|
|
|
fn hasSeenVar(self: *const TypeWriter, var_: Var) bool {
|
|
for (self.seen.items) |seen| {
|
|
if (seen == var_) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// Convert a var to a type string
|
|
fn writeVarWithContext(self: *TypeWriter, var_: Var, context: TypeContext, root_var: Var) std.mem.Allocator.Error!void {
|
|
if (@intFromEnum(var_) >= self.types.slots.backing.len()) {
|
|
// Variable is out of bounds - this can happen with corrupted type data
|
|
_ = try self.buf.writer().write("Error");
|
|
return;
|
|
}
|
|
|
|
const resolved = self.types.resolveVar(var_);
|
|
|
|
if (@intFromEnum(resolved.var_) >= self.types.slots.backing.len()) {
|
|
// Variable is out of bounds - this can happen with corrupted type data
|
|
_ = try self.buf.writer().write("Error");
|
|
return;
|
|
}
|
|
|
|
// Check if resolution returned an error descriptor - bail immediately
|
|
if (resolved.desc.content == .err) {
|
|
_ = try self.buf.writer().write("Error");
|
|
return;
|
|
}
|
|
|
|
if (self.hasSeenVar(resolved.var_)) {
|
|
_ = try self.buf.writer().write("...");
|
|
} else {
|
|
try self.seen.append(var_);
|
|
defer _ = self.seen.pop();
|
|
|
|
switch (resolved.desc.content) {
|
|
.flex => |flex| {
|
|
if (flex.name) |ident_idx| {
|
|
_ = try self.buf.writer().write(self.getIdent(ident_idx));
|
|
} else {
|
|
try self.writeFlexVarName(var_, context, root_var);
|
|
}
|
|
|
|
for (self.types.sliceStaticDispatchConstraints(flex.constraints)) |constraint| {
|
|
try self.static_dispatch_constraints.append(constraint);
|
|
}
|
|
},
|
|
.rigid => |rigid| {
|
|
_ = try self.buf.writer().write(self.getIdent(rigid.name));
|
|
// Useful in debugging to see if a var is rigid or not
|
|
// _ = try self.buf.writer().write("[r]");
|
|
|
|
for (self.types.sliceStaticDispatchConstraints(rigid.constraints)) |constraint| {
|
|
try self.static_dispatch_constraints.append(constraint);
|
|
}
|
|
},
|
|
.alias => |alias| {
|
|
try self.writeAlias(alias, root_var);
|
|
},
|
|
.structure => |flat_type| {
|
|
try self.writeFlatType(flat_type, root_var);
|
|
},
|
|
.err => {
|
|
_ = try self.buf.writer().write("Error");
|
|
},
|
|
}
|
|
|
|
// Useful in debugging to see the idx of a var
|
|
// _ = try self.buf.writer().print("[{}]", .{@intFromEnum(resolved.var_)});
|
|
}
|
|
}
|
|
|
|
fn writeVar(self: *TypeWriter, var_: Var, root_var: Var) std.mem.Allocator.Error!void {
|
|
try self.writeVarWithContext(var_, .General, root_var);
|
|
}
|
|
|
|
/// Write an alias type
|
|
fn writeAlias(self: *TypeWriter, alias: Alias, root_var: Var) std.mem.Allocator.Error!void {
|
|
_ = try self.buf.writer().write(self.getIdent(alias.ident.ident_idx));
|
|
var args_iter = self.types.iterAliasArgs(alias);
|
|
if (args_iter.count() > 0) {
|
|
_ = try self.buf.writer().write("(");
|
|
|
|
// Write first arg without comma
|
|
if (args_iter.next()) |arg_var| {
|
|
try self.writeVar(arg_var, root_var);
|
|
}
|
|
|
|
// Write remaining args with comma prefix
|
|
while (args_iter.next()) |arg_var| {
|
|
_ = try self.buf.writer().write(", ");
|
|
try self.writeVar(arg_var, root_var);
|
|
}
|
|
_ = try self.buf.writer().write(")");
|
|
}
|
|
}
|
|
|
|
/// Convert a flat type to a type string
|
|
fn writeFlatType(self: *TypeWriter, flat_type: FlatType, root_var: Var) std.mem.Allocator.Error!void {
|
|
switch (flat_type) {
|
|
.str => {
|
|
_ = try self.buf.writer().write("Str");
|
|
},
|
|
.box => |sub_var| {
|
|
_ = try self.buf.writer().write("Box(");
|
|
try self.writeVar(sub_var, root_var);
|
|
_ = try self.buf.writer().write(")");
|
|
},
|
|
.list => |sub_var| {
|
|
_ = try self.buf.writer().write("List(");
|
|
try self.writeVarWithContext(sub_var, .ListContent, root_var);
|
|
_ = try self.buf.writer().write(")");
|
|
},
|
|
.list_unbound => {
|
|
_ = try self.buf.writer().write("List(_");
|
|
try self.generateContextualName(.ListContent);
|
|
_ = try self.buf.writer().write(")");
|
|
},
|
|
.tuple => |tuple| {
|
|
try self.writeTuple(tuple, root_var);
|
|
},
|
|
.num => |num| {
|
|
try self.writeNum(num, root_var);
|
|
},
|
|
.nominal_type => |nominal_type| {
|
|
try self.writeNominalType(nominal_type, root_var);
|
|
},
|
|
.fn_pure => |func| {
|
|
try self.writeFuncWithArrow(func, " -> ", root_var);
|
|
},
|
|
.fn_effectful => |func| {
|
|
try self.writeFuncWithArrow(func, " => ", root_var);
|
|
},
|
|
.fn_unbound => |func| {
|
|
try self.writeFuncWithArrow(func, " -> ", root_var);
|
|
},
|
|
.record => |record| {
|
|
try self.writeRecord(record, root_var);
|
|
},
|
|
.record_unbound => |fields| {
|
|
try self.writeRecordFields(fields, root_var);
|
|
},
|
|
.empty_record => {
|
|
_ = try self.buf.writer().write("{}");
|
|
},
|
|
.tag_union => |tag_union| {
|
|
try self.writeTagUnion(tag_union, root_var);
|
|
},
|
|
.empty_tag_union => {
|
|
_ = try self.buf.writer().write("[]");
|
|
},
|
|
}
|
|
}
|
|
|
|
/// Write a tuple type
|
|
fn writeTuple(self: *TypeWriter, tuple: Tuple, root_var: Var) std.mem.Allocator.Error!void {
|
|
const elems = self.types.sliceVars(tuple.elems);
|
|
_ = try self.buf.writer().write("(");
|
|
for (elems, 0..) |elem, i| {
|
|
if (i > 0) _ = try self.buf.writer().write(", ");
|
|
try self.writeVarWithContext(elem, .TupleFieldContent, root_var);
|
|
}
|
|
_ = try self.buf.writer().write(")");
|
|
}
|
|
|
|
/// Write a nominal type
|
|
fn writeNominalType(self: *TypeWriter, nominal_type: NominalType, root_var: Var) std.mem.Allocator.Error!void {
|
|
_ = try self.buf.writer().write(self.getIdent(nominal_type.ident.ident_idx));
|
|
|
|
var args_iter = self.types.iterNominalArgs(nominal_type);
|
|
if (args_iter.count() > 0) {
|
|
_ = try self.buf.writer().write("(");
|
|
|
|
// Write first arg without comma
|
|
if (args_iter.next()) |arg_var| {
|
|
try self.writeVar(arg_var, root_var);
|
|
}
|
|
// Write remaining args with comma prefix
|
|
while (args_iter.next()) |arg_var| {
|
|
_ = try self.buf.writer().write(", ");
|
|
try self.writeVar(arg_var, root_var);
|
|
}
|
|
_ = try self.buf.writer().write(")");
|
|
}
|
|
}
|
|
|
|
/// Write record fields without extension
|
|
fn writeRecordFields(self: *TypeWriter, fields: RecordField.SafeMultiList.Range, root_var: Var) std.mem.Allocator.Error!void {
|
|
if (fields.isEmpty()) {
|
|
_ = try self.buf.writer().write("{}");
|
|
return;
|
|
}
|
|
|
|
_ = try self.buf.writer().write("{ ");
|
|
|
|
const fields_slice = self.types.getRecordFieldsSlice(fields);
|
|
|
|
// Write first field - we already verified that there's at least one field
|
|
_ = try self.buf.writer().write(self.getIdent(fields_slice.items(.name)[0]));
|
|
_ = try self.buf.writer().write(": ");
|
|
try self.writeVarWithContext(fields_slice.items(.var_)[0], .RecordFieldContent, root_var);
|
|
|
|
// Write remaining fields
|
|
for (fields_slice.items(.name)[1..], fields_slice.items(.var_)[1..]) |name, var_| {
|
|
_ = try self.buf.writer().write(", ");
|
|
_ = try self.buf.writer().write(self.getIdent(name));
|
|
_ = try self.buf.writer().write(": ");
|
|
try self.writeVarWithContext(var_, .RecordFieldContent, root_var);
|
|
}
|
|
|
|
_ = try self.buf.writer().write(" }");
|
|
}
|
|
|
|
/// Write a function type with a specific arrow (`->` or `=>`)
|
|
fn writeFuncWithArrow(self: *TypeWriter, func: Func, arrow: []const u8, root_var: Var) std.mem.Allocator.Error!void {
|
|
const args = self.types.sliceVars(func.args);
|
|
|
|
// Write arguments
|
|
if (args.len == 0) {
|
|
_ = try self.buf.writer().write("({})");
|
|
} else if (args.len == 1) {
|
|
try self.writeVarWithContext(args[0], .FunctionArgument, root_var);
|
|
} else {
|
|
for (args, 0..) |arg, i| {
|
|
if (i > 0) _ = try self.buf.writer().write(", ");
|
|
try self.writeVarWithContext(arg, .FunctionArgument, root_var);
|
|
}
|
|
}
|
|
|
|
_ = try self.buf.writer().write(arrow);
|
|
|
|
try self.writeVarWithContext(func.ret, .FunctionReturn, root_var);
|
|
}
|
|
|
|
/// Write a record type
|
|
fn writeRecord(self: *TypeWriter, record: Record, root_var: Var) std.mem.Allocator.Error!void {
|
|
const fields = self.types.getRecordFieldsSlice(record.fields);
|
|
|
|
_ = try self.buf.writer().write("{ ");
|
|
for (fields.items(.name), fields.items(.var_), 0..) |field_name, field_var, i| {
|
|
if (i > 0) _ = try self.buf.writer().write(", ");
|
|
_ = try self.buf.writer().write(self.getIdent(field_name));
|
|
_ = try self.buf.writer().write(": ");
|
|
try self.writeVarWithContext(field_var, .RecordFieldContent, root_var);
|
|
}
|
|
|
|
try self.writeRecordExtension(record.ext, fields.len, root_var);
|
|
|
|
_ = try self.buf.writer().write(" }");
|
|
}
|
|
|
|
/// Helper to write record extension, handling nested records
|
|
fn writeRecordExtension(self: *TypeWriter, ext_var: Var, num_fields: usize, root_var: Var) std.mem.Allocator.Error!void {
|
|
const ext_resolved = self.types.resolveVar(ext_var);
|
|
switch (ext_resolved.desc.content) {
|
|
.structure => |flat_type| switch (flat_type) {
|
|
.empty_record => {}, // Don't show empty extension
|
|
.record => |ext_record| {
|
|
// Flatten nested record extensions
|
|
const ext_fields = self.types.getRecordFieldsSlice(ext_record.fields);
|
|
for (ext_fields.items(.name), ext_fields.items(.var_)) |field_name, field_var| {
|
|
_ = try self.buf.writer().write(", ");
|
|
_ = try self.buf.writer().write(self.getIdent(field_name));
|
|
_ = try self.buf.writer().write(": ");
|
|
try self.writeVarWithContext(field_var, .RecordFieldContent, root_var);
|
|
}
|
|
// Recursively handle the extension's extension
|
|
try self.writeRecordExtension(ext_record.ext, num_fields + ext_fields.len, root_var);
|
|
},
|
|
else => {
|
|
if (num_fields > 0) _ = try self.buf.writer().write(", ");
|
|
try self.writeVarWithContext(ext_var, .RecordExtension, root_var);
|
|
},
|
|
},
|
|
.flex => |flex| {
|
|
// Only show flex vars if they have a name or if there are constraints
|
|
if (flex.name != null or flex.constraints.len() > 0) {
|
|
if (num_fields > 0) _ = try self.buf.writer().write(", ");
|
|
try self.writeVarWithContext(ext_var, .RecordExtension, root_var);
|
|
}
|
|
|
|
for (self.types.sliceStaticDispatchConstraints(flex.constraints)) |constraint| {
|
|
try self.static_dispatch_constraints.append(constraint);
|
|
}
|
|
},
|
|
.rigid => |rigid| {
|
|
// Show rigid vars with .. syntax
|
|
if (num_fields > 0) _ = try self.buf.writer().write(", ");
|
|
_ = try self.buf.writer().write("..");
|
|
_ = try self.buf.writer().write(self.getIdent(rigid.name));
|
|
|
|
for (self.types.sliceStaticDispatchConstraints(rigid.constraints)) |constraint| {
|
|
try self.static_dispatch_constraints.append(constraint);
|
|
}
|
|
},
|
|
else => {
|
|
// Show other types (aliases, errors, etc)
|
|
if (num_fields > 0) _ = try self.buf.writer().write(", ");
|
|
try self.writeVarWithContext(ext_var, .RecordExtension, root_var);
|
|
},
|
|
}
|
|
}
|
|
|
|
/// Write a tag union type
|
|
fn writeTagUnion(self: *TypeWriter, tag_union: TagUnion, root_var: Var) std.mem.Allocator.Error!void {
|
|
_ = try self.buf.writer().write("[");
|
|
|
|
// Bounds check the tags range before iterating
|
|
const tags_start_idx = @intFromEnum(tag_union.tags.start);
|
|
const tags_len = self.types.tags.len();
|
|
if (tags_start_idx >= tags_len or tags_start_idx + tag_union.tags.count > tags_len) {
|
|
// Tags range is out of bounds - return error indicator
|
|
_ = try self.buf.writer().write("Error");
|
|
_ = try self.buf.writer().write("]");
|
|
return;
|
|
}
|
|
|
|
var iter = tag_union.tags.iterIndices();
|
|
while (iter.next()) |tag_idx| {
|
|
if (@intFromEnum(tag_idx) > @intFromEnum(tag_union.tags.start)) {
|
|
_ = try self.buf.writer().write(", ");
|
|
}
|
|
|
|
const tag = self.types.tags.get(tag_idx);
|
|
try self.writeTag(tag, root_var);
|
|
}
|
|
|
|
_ = try self.buf.writer().write("]");
|
|
|
|
// Show extension variable if it's not empty
|
|
const ext_resolved = self.types.resolveVar(tag_union.ext);
|
|
switch (ext_resolved.desc.content) {
|
|
.flex => |flex| {
|
|
if (flex.name) |ident_idx| {
|
|
_ = try self.buf.writer().write(self.getIdent(ident_idx));
|
|
} else {
|
|
try self.writeFlexVarName(tag_union.ext, .TagUnionExtension, root_var);
|
|
}
|
|
|
|
for (self.types.sliceStaticDispatchConstraints(flex.constraints)) |constraint| {
|
|
try self.static_dispatch_constraints.append(constraint);
|
|
}
|
|
},
|
|
.structure => |flat_type| switch (flat_type) {
|
|
.empty_tag_union => {}, // Don't show empty extension
|
|
else => {
|
|
try self.writeVarWithContext(tag_union.ext, .TagUnionExtension, root_var);
|
|
},
|
|
},
|
|
.rigid => |rigid| {
|
|
_ = try self.buf.writer().write(self.getIdent(rigid.name));
|
|
// _ = try self.buf.writer().write("[r]");
|
|
|
|
for (self.types.sliceStaticDispatchConstraints(rigid.constraints)) |constraint| {
|
|
try self.static_dispatch_constraints.append(constraint);
|
|
}
|
|
},
|
|
.err => {
|
|
// Extension resolved to error - write error indicator
|
|
_ = try self.buf.writer().write("Error");
|
|
},
|
|
.alias => {
|
|
try self.writeVarWithContext(tag_union.ext, .TagUnionExtension, root_var);
|
|
},
|
|
}
|
|
}
|
|
|
|
/// Write a single tag
|
|
fn writeTag(self: *TypeWriter, tag: Tag, root_var: Var) std.mem.Allocator.Error!void {
|
|
_ = try self.buf.writer().write(self.getIdent(tag.name));
|
|
const args = self.types.sliceVars(tag.args);
|
|
if (args.len > 0) {
|
|
_ = try self.buf.writer().write("(");
|
|
for (args, 0..) |arg, i| {
|
|
if (i > 0) _ = try self.buf.writer().write(", ");
|
|
try self.writeVar(arg, root_var);
|
|
}
|
|
_ = try self.buf.writer().write(")");
|
|
}
|
|
}
|
|
|
|
/// Convert a num type to a type string
|
|
fn writeNum(self: *TypeWriter, num: Num, root_var: Var) std.mem.Allocator.Error!void {
|
|
switch (num) {
|
|
.num_poly => |poly_var| {
|
|
_ = try self.buf.writer().write("Num(");
|
|
try self.writeVarWithContext(poly_var, .NumContent, root_var);
|
|
_ = try self.buf.writer().write(")");
|
|
},
|
|
.int_poly => |poly| {
|
|
_ = try self.buf.writer().write("Int(");
|
|
try self.writeVarWithContext(poly, .NumContent, root_var);
|
|
_ = try self.buf.writer().write(")");
|
|
},
|
|
.frac_poly => |poly| {
|
|
_ = try self.buf.writer().write("Frac(");
|
|
try self.writeVarWithContext(poly, .NumContent, root_var);
|
|
_ = try self.buf.writer().write(")");
|
|
},
|
|
.num_unbound => |_| {
|
|
_ = try self.buf.writer().write("Num(_");
|
|
try self.generateContextualName(.NumContent);
|
|
_ = try self.buf.writer().write(")");
|
|
},
|
|
.int_unbound => |_| {
|
|
_ = try self.buf.writer().write("Int(_");
|
|
try self.generateContextualName(.NumContent);
|
|
_ = try self.buf.writer().write(")");
|
|
},
|
|
.frac_unbound => |_| {
|
|
_ = try self.buf.writer().write("Frac(_");
|
|
try self.generateContextualName(.NumContent);
|
|
_ = try self.buf.writer().write(")");
|
|
},
|
|
.int_precision => |prec| {
|
|
try self.writeIntType(prec, .precision);
|
|
},
|
|
.frac_precision => |prec| {
|
|
try self.writeFracType(prec, .precision);
|
|
},
|
|
.num_compact => |compact| {
|
|
switch (compact) {
|
|
.int => |prec| {
|
|
try self.writeIntType(prec, .compacted);
|
|
},
|
|
.frac => |prec| {
|
|
try self.writeFracType(prec, .compacted);
|
|
},
|
|
}
|
|
},
|
|
}
|
|
}
|
|
|
|
const NumPrecType = enum { precision, compacted };
|
|
|
|
fn writeIntType(self: *TypeWriter, prec: Num.Int.Precision, num_type: NumPrecType) std.mem.Allocator.Error!void {
|
|
switch (num_type) {
|
|
.compacted => {
|
|
_ = switch (prec) {
|
|
.u8 => try self.buf.writer().write("Num(Int(Unsigned8))"),
|
|
.i8 => try self.buf.writer().write("Num(Int(Signed8))"),
|
|
.u16 => try self.buf.writer().write("Num(Int(Unsigned16))"),
|
|
.i16 => try self.buf.writer().write("Num(Int(Signed16))"),
|
|
.u32 => try self.buf.writer().write("Num(Int(Unsigned32))"),
|
|
.i32 => try self.buf.writer().write("Num(Int(Signed32))"),
|
|
.u64 => try self.buf.writer().write("Num(Int(Unsigned64))"),
|
|
.i64 => try self.buf.writer().write("Num(Int(Signed64))"),
|
|
.u128 => try self.buf.writer().write("Num(Int(Unsigned128))"),
|
|
.i128 => try self.buf.writer().write("Num(Int(Signed128))"),
|
|
};
|
|
},
|
|
.precision => {
|
|
_ = switch (prec) {
|
|
.u8 => try self.buf.writer().write("Unsigned8"),
|
|
.i8 => try self.buf.writer().write("Signed8"),
|
|
.u16 => try self.buf.writer().write("Unsigned16"),
|
|
.i16 => try self.buf.writer().write("Signed16"),
|
|
.u32 => try self.buf.writer().write("Unsigned32"),
|
|
.i32 => try self.buf.writer().write("Signed32"),
|
|
.u64 => try self.buf.writer().write("Unsigned64"),
|
|
.i64 => try self.buf.writer().write("Signed64"),
|
|
.u128 => try self.buf.writer().write("Unsigned128"),
|
|
.i128 => try self.buf.writer().write("Signed128"),
|
|
};
|
|
},
|
|
}
|
|
}
|
|
|
|
fn writeFracType(self: *TypeWriter, prec: Num.Frac.Precision, num_type: NumPrecType) std.mem.Allocator.Error!void {
|
|
switch (num_type) {
|
|
.compacted => {
|
|
_ = switch (prec) {
|
|
.f32 => try self.buf.writer().write("Num(Frac(Float32))"),
|
|
.f64 => try self.buf.writer().write("Num(Frac(Float64))"),
|
|
.dec => try self.buf.writer().write("Num(Frac(Decimal))"),
|
|
};
|
|
},
|
|
.precision => {
|
|
_ = switch (prec) {
|
|
.f32 => try self.buf.writer().write("Float32"),
|
|
.f64 => try self.buf.writer().write("Float64"),
|
|
.dec => try self.buf.writer().write("Decimal"),
|
|
};
|
|
},
|
|
}
|
|
}
|
|
|
|
/// Generate a name for a flex var that may appear multiple times in the type
|
|
pub fn writeFlexVarName(self: *TypeWriter, var_: Var, context: TypeContext, root_var: Var) std.mem.Allocator.Error!void {
|
|
const resolved_var = self.types.resolveVar(var_).var_;
|
|
|
|
// If resolved var is out of bounds, it's corrupted - just write a simple name
|
|
if (@intFromEnum(resolved_var) >= self.types.slots.backing.len()) {
|
|
_ = try self.buf.writer().write("_");
|
|
try self.generateContextualName(context);
|
|
return;
|
|
}
|
|
|
|
// Check if we've seen this flex var before.
|
|
if (self.flex_var_names_map.get(resolved_var)) |range| {
|
|
// If so, then use that name
|
|
_ = try self.buf.writer().write(
|
|
self.flex_var_names.items[range.start..range.end],
|
|
);
|
|
} else {
|
|
// Check if this variable appears multiple times
|
|
// Note: counting can fail with corrupted data, so we treat it as appearing once
|
|
const occurrences = try self.countVarOccurrences(resolved_var, root_var);
|
|
if (occurrences <= 1) {
|
|
// If it appears once, then generate and write the name
|
|
_ = try self.buf.writer().write("_");
|
|
try self.generateContextualName(context);
|
|
} else {
|
|
// If it appears more than once, then we have to track the name we
|
|
// assign it so it appears consistently across the type str
|
|
|
|
// Generate a new general var name. We do not use the context here
|
|
// because that may be the current context the var appears in, but
|
|
// the var may later appear in a different context
|
|
const buf_start = self.buf.items.len;
|
|
try self.generateContextualName(.General);
|
|
const buf_end = self.buf.items.len;
|
|
|
|
// Then write down the name we generated for later
|
|
const flex_start = self.flex_var_names.items.len;
|
|
try self.flex_var_names.appendSlice(self.buf.items[buf_start..buf_end]);
|
|
const flex_end = self.flex_var_names.items.len;
|
|
try self.flex_var_names_map.put(resolved_var, .{ .start = flex_start, .end = flex_end });
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Count how many times a variable appears in a type
|
|
fn countVarOccurrences(self: *TypeWriter, search_var: Var, root_var: Var) std.mem.Allocator.Error!usize {
|
|
self.seen_count_var_occurrences.clearRetainingCapacity();
|
|
|
|
var count: usize = 0;
|
|
try self.countVar(search_var, root_var, &count);
|
|
return count;
|
|
}
|
|
|
|
fn countVar(self: *TypeWriter, search_var: Var, current_var: Var, count: *usize) std.mem.Allocator.Error!void {
|
|
if (@intFromEnum(current_var) >= self.types.slots.backing.len()) return;
|
|
|
|
const resolved = self.types.resolveVar(current_var);
|
|
|
|
// If resolution returned an error descriptor, stop traversing
|
|
if (resolved.desc.content == .err) {
|
|
return;
|
|
}
|
|
|
|
// Count if this is the search var
|
|
|
|
// First, check if this is the var we are counting
|
|
if (resolved.var_ == search_var) {
|
|
count.* += 1;
|
|
}
|
|
|
|
// Check if we've already seen this var
|
|
// This avoids infinite recursion
|
|
for (self.seen_count_var_occurrences.items) |seen| {
|
|
if (seen == resolved.var_) return;
|
|
}
|
|
|
|
// Record that we've seen this var
|
|
try self.seen_count_var_occurrences.append(resolved.var_);
|
|
defer _ = self.seen_count_var_occurrences.pop();
|
|
|
|
// Then recurse
|
|
switch (resolved.desc.content) {
|
|
.flex => |flex| {
|
|
const constraints = self.types.sliceStaticDispatchConstraints(flex.constraints);
|
|
for (constraints) |constraint| {
|
|
try self.countVar(search_var, constraint.fn_var, count);
|
|
}
|
|
},
|
|
.rigid => |rigid| {
|
|
const constraints = self.types.sliceStaticDispatchConstraints(rigid.constraints);
|
|
for (constraints) |constraint| {
|
|
try self.countVar(search_var, constraint.fn_var, count);
|
|
}
|
|
},
|
|
.alias => |alias| {
|
|
// For aliases, we only count occurrences in the type arguments
|
|
var args_iter = self.types.iterAliasArgs(alias);
|
|
while (args_iter.next()) |arg_var| {
|
|
try self.countVar(search_var, arg_var, count);
|
|
}
|
|
},
|
|
.structure => |flat_type| {
|
|
try self.countVarInFlatType(search_var, flat_type, count);
|
|
},
|
|
.err => {},
|
|
}
|
|
}
|
|
|
|
fn countVarInFlatType(self: *TypeWriter, search_var: Var, flat_type: FlatType, count: *usize) std.mem.Allocator.Error!void {
|
|
switch (flat_type) {
|
|
.str, .empty_record, .empty_tag_union => {},
|
|
.box => |sub_var| try self.countVar(search_var, sub_var, count),
|
|
.list => |sub_var| try self.countVar(search_var, sub_var, count),
|
|
.list_unbound, .num => {},
|
|
.tuple => |tuple| {
|
|
const elems = self.types.sliceVars(tuple.elems);
|
|
for (elems) |elem| {
|
|
try self.countVar(search_var, elem, count);
|
|
}
|
|
},
|
|
.nominal_type => |nominal_type| {
|
|
var args_iter = self.types.iterNominalArgs(nominal_type);
|
|
while (args_iter.next()) |arg_var| {
|
|
try self.countVar(search_var, arg_var, count);
|
|
}
|
|
},
|
|
.fn_pure, .fn_effectful, .fn_unbound => |func| {
|
|
const args = self.types.sliceVars(func.args);
|
|
for (args) |arg| {
|
|
try self.countVar(search_var, arg, count);
|
|
}
|
|
try self.countVar(search_var, func.ret, count);
|
|
},
|
|
.record => |record| {
|
|
const fields = self.types.getRecordFieldsSlice(record.fields);
|
|
for (fields.items(.var_)) |field_var| {
|
|
try self.countVar(search_var, field_var, count);
|
|
}
|
|
try self.countVar(search_var, record.ext, count);
|
|
},
|
|
.record_unbound => |fields| {
|
|
const fields_slice = self.types.getRecordFieldsSlice(fields);
|
|
for (fields_slice.items(.var_)) |field_var| {
|
|
try self.countVar(search_var, field_var, count);
|
|
}
|
|
},
|
|
.tag_union => |tag_union| {
|
|
// Bounds check the tags range before iterating
|
|
const tags_start_idx = @intFromEnum(tag_union.tags.start);
|
|
const tags_len = self.types.tags.len();
|
|
if (tags_start_idx >= tags_len or tags_start_idx + tag_union.tags.count > tags_len) {
|
|
// Tags range is out of bounds - skip counting in corrupted data
|
|
return;
|
|
}
|
|
|
|
var iter = tag_union.tags.iterIndices();
|
|
while (iter.next()) |tag_idx| {
|
|
const tag = self.types.tags.get(tag_idx);
|
|
const args = self.types.sliceVars(tag.args);
|
|
for (args) |arg_var| {
|
|
try self.countVar(search_var, arg_var, count);
|
|
}
|
|
}
|
|
try self.countVar(search_var, tag_union.ext, count);
|
|
},
|
|
}
|
|
}
|
|
|
|
/// Retrieves the text representation of an identifier by its index.
|
|
/// This is used when formatting types that reference named identifiers.
|
|
pub fn getIdent(self: *const TypeWriter, idx: Ident.Idx) []const u8 {
|
|
return self.idents.getText(idx);
|
|
}
|