Merge remote-tracking branch 'origin/main' into abilities-syntax

This commit is contained in:
Richard Feldman 2023-08-10 20:29:27 -04:00
commit 2da41be29f
No known key found for this signature in database
GPG key ID: F1F21AA5B1D9E43B
524 changed files with 47536 additions and 15089 deletions

View file

@ -14,8 +14,8 @@ use roc_module::low_level::LowLevel;
use roc_module::symbol::Symbol;
use roc_mono::ir::{
Call, CallType, EntryPoint, Expr, HigherOrderLowLevel, HostExposedLayouts, ListLiteralElement,
Literal, ModifyRc, OptLevel, Proc, ProcLayout, SingleEntryPoint, Stmt,
Call, CallType, EntryPoint, ErasedField, Expr, HigherOrderLowLevel, HostExposedLambdaSet,
ListLiteralElement, Literal, ModifyRc, OptLevel, Proc, ProcLayout, SingleEntryPoint, Stmt,
};
use roc_mono::layout::{
Builtin, InLayout, Layout, LayoutInterner, LayoutRepr, Niche, RawFunctionLayout,
@ -117,7 +117,7 @@ where
}
if debug() {
for (i, c) in (format!("{:?}", symbol)).chars().take(25).enumerate() {
for (i, c) in (format!("{symbol:?}")).chars().take(25).enumerate() {
name_bytes[25 + i] = c as u8;
}
}
@ -131,21 +131,23 @@ fn bytes_as_ascii(bytes: &[u8]) -> String {
let mut buf = String::new();
for byte in bytes {
write!(buf, "{:02X}", byte).unwrap();
write!(buf, "{byte:02X}").unwrap();
}
buf
}
pub fn spec_program<'a, 'r, I>(
pub fn spec_program<'a, 'r, I1, I2>(
arena: &'a Bump,
interner: &'r mut STLayoutInterner<'a>,
interner: &'r STLayoutInterner<'a>,
opt_level: OptLevel,
entry_point: roc_mono::ir::EntryPoint<'a>,
procs: I,
procs: I1,
hels: I2,
) -> Result<morphic_lib::Solutions>
where
I: Iterator<Item = &'r Proc<'a>>,
I1: Iterator<Item = &'r Proc<'a>>,
I2: Iterator<Item = &'r HostExposedLambdaSet<'a>>,
{
let main_module = {
let mut m = ModDefBuilder::new();
@ -181,40 +183,38 @@ where
let mut type_definitions = MutSet::default();
let mut host_exposed_functions = Vec::new();
let mut erased_functions = Vec::new();
for hels in hels {
match hels.raw_function_layout {
RawFunctionLayout::Function(_, _, _) => {
let it = hels.proc_layout.arguments.iter().copied();
let bytes =
func_name_bytes_help(hels.symbol, it, Niche::NONE, hels.proc_layout.result);
host_exposed_functions.push((bytes, hels.proc_layout.arguments));
}
RawFunctionLayout::ErasedFunction(..) => {
let it = hels.proc_layout.arguments.iter().copied();
let bytes =
func_name_bytes_help(hels.symbol, it, Niche::NONE, hels.proc_layout.result);
host_exposed_functions.push((bytes, hels.proc_layout.arguments));
}
RawFunctionLayout::ZeroArgumentThunk(_) => {
let bytes =
func_name_bytes_help(hels.symbol, [], Niche::NONE, hels.proc_layout.result);
host_exposed_functions.push((bytes, hels.proc_layout.arguments));
}
}
}
// all other functions
for proc in procs {
let bytes = func_name_bytes(proc);
let func_name = FuncName(&bytes);
if let HostExposedLayouts::HostExposed { aliases, .. } = &proc.host_exposed_layouts {
for (_, hels) in aliases {
match hels.raw_function_layout {
RawFunctionLayout::Function(_, _, _) => {
let it = hels.proc_layout.arguments.iter().copied();
let bytes = func_name_bytes_help(
hels.symbol,
it,
Niche::NONE,
hels.proc_layout.result,
);
host_exposed_functions.push((bytes, hels.proc_layout.arguments));
}
RawFunctionLayout::ZeroArgumentThunk(_) => {
let bytes = func_name_bytes_help(
hels.symbol,
[],
Niche::NONE,
hels.proc_layout.result,
);
host_exposed_functions.push((bytes, hels.proc_layout.arguments));
}
}
}
}
if debug() {
eprintln!(
"{:?}: {:?} with {:?} args",
@ -226,6 +226,11 @@ where
let (spec, type_names) = proc_spec(arena, interner, proc)?;
if proc.is_erased {
let args = &*arena.alloc_slice_fill_iter(proc.args.iter().map(|(lay, _)| *lay));
erased_functions.push((bytes, args));
}
type_definitions.extend(type_names);
m.add_func(func_name, spec)?;
@ -253,6 +258,7 @@ where
entry_point_layout,
Some(roc_main),
&host_exposed_functions,
&erased_functions,
)?;
type_definitions.extend(env.type_names);
@ -279,8 +285,14 @@ where
.collect();
let mut env = Env::new();
let entry_point_function =
build_entry_point(&mut env, interner, layout, None, &host_exposed)?;
let entry_point_function = build_entry_point(
&mut env,
interner,
layout,
None,
&host_exposed,
&erased_functions,
)?;
type_definitions.extend(env.type_names);
@ -360,10 +372,11 @@ fn terrible_hack(builder: &mut FuncDefBuilder, block: BlockId, type_id: TypeId)
fn build_entry_point<'a>(
env: &mut Env<'a>,
interner: &mut STLayoutInterner<'a>,
interner: &STLayoutInterner<'a>,
layout: roc_mono::ir::ProcLayout<'a>,
entry_point_function: Option<FuncName>,
host_exposed_functions: &[([u8; SIZE], &'a [InLayout<'a>])],
erased_functions: &[([u8; SIZE], &'a [InLayout<'a>])],
) -> Result<FuncDef> {
let mut builder = FuncDefBuilder::new();
let outer_block = builder.add_block();
@ -394,7 +407,7 @@ fn build_entry_point<'a>(
}
// add fake calls to host-exposed functions so they are specialized
for (name_bytes, layouts) in host_exposed_functions {
for (name_bytes, layouts) in host_exposed_functions.iter().chain(erased_functions) {
let host_exposed_func_name = FuncName(name_bytes);
if Some(host_exposed_func_name) == entry_point_function {
@ -403,7 +416,7 @@ fn build_entry_point<'a>(
let block = builder.add_block();
let struct_layout = interner.insert_direct_no_semantic(LayoutRepr::struct_(layouts));
let struct_layout = LayoutRepr::struct_(layouts);
let type_id = layout_spec(env, &mut builder, interner, struct_layout)?;
let argument = builder.add_unknown_with(block, &[], type_id)?;
@ -433,7 +446,7 @@ fn build_entry_point<'a>(
fn proc_spec<'a>(
arena: &'a Bump,
interner: &mut STLayoutInterner<'a>,
interner: &STLayoutInterner<'a>,
proc: &Proc<'a>,
) -> Result<(FuncDef, MutSet<UnionLayout<'a>>)> {
let mut builder = FuncDefBuilder::new();
@ -460,10 +473,14 @@ fn proc_spec<'a>(
)?;
let root = BlockExpr(block, value_id);
let args_struct_layout =
interner.insert_direct_no_semantic(LayoutRepr::struct_(argument_layouts.into_bump_slice()));
let args_struct_layout = LayoutRepr::struct_(argument_layouts.into_bump_slice());
let arg_type_id = layout_spec(&mut env, &mut builder, interner, args_struct_layout)?;
let ret_type_id = layout_spec(&mut env, &mut builder, interner, proc.ret_layout)?;
let ret_type_id = layout_spec(
&mut env,
&mut builder,
interner,
interner.get_repr(proc.ret_layout),
)?;
let spec = builder.build(arg_type_id, ret_type_id, root)?;
@ -506,6 +523,12 @@ fn apply_refcount_operation(
builder.add_recursive_touch(block, argument)?;
}
ModifyRc::DecRef(symbol) => {
// this is almost certainly suboptimal, but not incorrect
let argument = env.symbols[symbol];
builder.add_recursive_touch(block, argument)?;
}
ModifyRc::Free(symbol) => {
// this is almost certainly suboptimal, but not incorrect
let argument = env.symbols[symbol];
builder.add_recursive_touch(block, argument)?;
}
@ -516,7 +539,7 @@ fn apply_refcount_operation(
fn stmt_spec<'a>(
builder: &mut FuncDefBuilder,
interner: &mut STLayoutInterner<'a>,
interner: &STLayoutInterner<'a>,
env: &mut Env<'a>,
block: BlockId,
layout: InLayout<'a>,
@ -601,10 +624,15 @@ fn stmt_spec<'a>(
let mut type_ids = Vec::new();
for p in parameters.iter() {
type_ids.push(layout_spec(env, builder, interner, p.layout)?);
type_ids.push(layout_spec(
env,
builder,
interner,
interner.get_repr(p.layout),
)?);
}
let ret_type_id = layout_spec(env, builder, interner, layout)?;
let ret_type_id = layout_spec(env, builder, interner, interner.get_repr(layout))?;
let jp_arg_type_id = builder.add_tuple_type(&type_ids)?;
@ -645,7 +673,7 @@ fn stmt_spec<'a>(
builder.add_sub_block(block, BlockExpr(cont_block, cont_value_id))
}
Jump(id, symbols) => {
let ret_type_id = layout_spec(env, builder, interner, layout)?;
let ret_type_id = layout_spec(env, builder, interner, interner.get_repr(layout))?;
let argument = build_tuple_value(builder, env, block, symbols)?;
let jpid = env.join_points[id];
@ -654,7 +682,7 @@ fn stmt_spec<'a>(
Crash(msg, _) => {
// Model this as a foreign call rather than TERMINATE because
// we want ownership of the message.
let result_type = layout_spec(env, builder, interner, layout)?;
let result_type = layout_spec(env, builder, interner, interner.get_repr(layout))?;
builder.add_unknown_with(block, &[env.symbols[msg]], result_type)
}
@ -692,7 +720,7 @@ fn build_recursive_tuple_type<'a>(
let mut field_types = Vec::new();
for field in layouts.iter() {
let type_id = layout_spec_help(env, builder, interner, *field)?;
let type_id = layout_spec_help(env, builder, interner, interner.get_repr(*field))?;
field_types.push(type_id);
}
@ -708,7 +736,12 @@ fn build_tuple_type<'a>(
let mut field_types = Vec::new();
for field in layouts.iter() {
field_types.push(layout_spec(env, builder, interner, *field)?);
field_types.push(layout_spec(
env,
builder,
interner,
interner.get_repr(*field),
)?);
}
builder.add_tuple_type(&field_types)
@ -742,7 +775,7 @@ fn add_loop(
fn call_spec<'a>(
builder: &mut FuncDefBuilder,
interner: &mut STLayoutInterner<'a>,
interner: &STLayoutInterner<'a>,
env: &mut Env<'a>,
block: BlockId,
layout: InLayout<'a>,
@ -768,6 +801,16 @@ fn call_spec<'a>(
let module = MOD_APP;
builder.add_call(block, spec_var, module, name, arg_value_id)
}
ByPointer {
pointer,
ret_layout,
arg_layouts: _,
} => {
let result_type = layout_spec(env, builder, interner, interner.get_repr(*ret_layout))?;
let fnptr = env.symbols[pointer];
let arg_value_id = build_tuple_value(builder, env, block, call.arguments)?;
builder.add_unknown_with(block, &[fnptr, arg_value_id], result_type)
}
Foreign {
foreign_symbol: _,
ret_layout,
@ -778,7 +821,7 @@ fn call_spec<'a>(
.map(|symbol| env.symbols[symbol])
.collect();
let result_type = layout_spec(env, builder, interner, *ret_layout)?;
let result_type = layout_spec(env, builder, interner, interner.get_repr(*ret_layout))?;
builder.add_unknown_with(block, &arguments, result_type)
}
@ -849,11 +892,10 @@ fn call_spec<'a>(
list_append(builder, block, update_mode_var, state, new_element)
};
let output_element_type = layout_spec(env, builder, interner, *return_layout)?;
let output_element_type =
layout_spec(env, builder, interner, interner.get_repr(*return_layout))?;
let state_layout = interner.insert_direct_no_semantic(LayoutRepr::Builtin(
Builtin::List(*return_layout),
));
let state_layout = LayoutRepr::Builtin(Builtin::List(*return_layout));
let state_type = layout_spec(env, builder, interner, state_layout)?;
let init_state = new_list(builder, block, output_element_type)?;
@ -880,8 +922,7 @@ fn call_spec<'a>(
let arg0_layout = argument_layouts[0];
let state_layout = interner
.insert_direct_no_semantic(LayoutRepr::Builtin(Builtin::List(arg0_layout)));
let state_layout = LayoutRepr::Builtin(Builtin::List(arg0_layout));
let state_type = layout_spec(env, builder, interner, state_layout)?;
let init_state = list;
@ -906,11 +947,10 @@ fn call_spec<'a>(
list_append(builder, block, update_mode_var, state, new_element)
};
let output_element_type = layout_spec(env, builder, interner, *return_layout)?;
let output_element_type =
layout_spec(env, builder, interner, interner.get_repr(*return_layout))?;
let state_layout = interner.insert_direct_no_semantic(LayoutRepr::Builtin(
Builtin::List(*return_layout),
));
let state_layout = LayoutRepr::Builtin(Builtin::List(*return_layout));
let state_type = layout_spec(env, builder, interner, state_layout)?;
let init_state = new_list(builder, block, output_element_type)?;
@ -941,11 +981,10 @@ fn call_spec<'a>(
list_append(builder, block, update_mode_var, state, new_element)
};
let output_element_type = layout_spec(env, builder, interner, *return_layout)?;
let output_element_type =
layout_spec(env, builder, interner, interner.get_repr(*return_layout))?;
let state_layout = interner.insert_direct_no_semantic(LayoutRepr::Builtin(
Builtin::List(*return_layout),
));
let state_layout = LayoutRepr::Builtin(Builtin::List(*return_layout));
let state_type = layout_spec(env, builder, interner, state_layout)?;
let init_state = new_list(builder, block, output_element_type)?;
@ -982,11 +1021,10 @@ fn call_spec<'a>(
list_append(builder, block, update_mode_var, state, new_element)
};
let output_element_type = layout_spec(env, builder, interner, *return_layout)?;
let output_element_type =
layout_spec(env, builder, interner, interner.get_repr(*return_layout))?;
let state_layout = interner.insert_direct_no_semantic(LayoutRepr::Builtin(
Builtin::List(*return_layout),
));
let state_layout = LayoutRepr::Builtin(Builtin::List(*return_layout));
let state_type = layout_spec(env, builder, interner, state_layout)?;
let init_state = new_list(builder, block, output_element_type)?;
@ -1042,7 +1080,7 @@ fn lowlevel_spec<'a>(
) -> Result<ValueId> {
use LowLevel::*;
let type_id = layout_spec(env, builder, interner, layout)?;
let type_id = layout_spec(env, builder, interner, interner.get_repr(layout))?;
let mode = update_mode.to_bytes();
let update_mode_var = UpdateModeVar(&mode);
@ -1150,7 +1188,8 @@ fn lowlevel_spec<'a>(
match interner.get_repr(layout) {
LayoutRepr::Builtin(Builtin::List(element_layout)) => {
let type_id = layout_spec(env, builder, interner, element_layout)?;
let type_id =
layout_spec(env, builder, interner, interner.get_repr(element_layout))?;
new_list(builder, block, type_id)
}
_ => unreachable!("empty array does not have a list layout"),
@ -1198,7 +1237,7 @@ fn lowlevel_spec<'a>(
// TODO overly pessimstic
let arguments: Vec<_> = arguments.iter().map(|symbol| env.symbols[symbol]).collect();
let result_type = layout_spec(env, builder, interner, layout)?;
let result_type = layout_spec(env, builder, interner, interner.get_repr(layout))?;
builder.add_unknown_with(block, &arguments, result_type)
}
@ -1283,7 +1322,7 @@ fn worst_case_type(context: &mut impl TypeContext) -> Result<TypeId> {
fn expr_spec<'a>(
builder: &mut FuncDefBuilder,
interner: &mut STLayoutInterner<'a>,
interner: &STLayoutInterner<'a>,
env: &mut Env<'a>,
block: BlockId,
layout: InLayout<'a>,
@ -1294,21 +1333,16 @@ fn expr_spec<'a>(
match expr {
Literal(literal) => literal_spec(builder, block, literal),
NullPointer => {
let pointer_type = layout_spec(env, builder, interner, layout)?;
let pointer_type = layout_spec(env, builder, interner, interner.get_repr(layout))?;
builder.add_unknown_with(block, &[], pointer_type)
}
Call(call) => call_spec(builder, interner, env, block, layout, call),
Reuse {
tag_layout,
tag_id,
arguments,
..
}
| Tag {
Tag {
tag_layout,
tag_id,
arguments,
reuse: _,
} => {
let data_id = build_tuple_value(builder, env, block, arguments)?;
@ -1347,16 +1381,6 @@ fn expr_spec<'a>(
builder.add_make_named(block, MOD_APP, type_name, tag_value_id)
}
ExprBox { symbol } => {
let value_id = env.symbols[symbol];
with_new_heap_cell(builder, block, value_id)
}
ExprUnbox { symbol } => {
let tuple_id = env.symbols[symbol];
builder.add_get_tuple_field(block, tuple_id, BOX_VALUE_INDEX)
}
Struct(fields) => build_tuple_value(builder, env, block, fields),
UnionAtIndex {
index,
@ -1412,6 +1436,38 @@ fn expr_spec<'a>(
builder.add_get_tuple_field(block, variant_id, index)
}
},
UnionFieldPtrAtIndex {
index,
tag_id,
structure,
union_layout,
} => {
let index = (*index) as u32;
let tag_value_id = env.symbols[structure];
let type_name_bytes = recursive_tag_union_name_bytes(union_layout).as_bytes();
let type_name = TypeName(&type_name_bytes);
// unwrap the named wrapper
let union_id = builder.add_unwrap_named(block, MOD_APP, type_name, tag_value_id)?;
// now we have a tuple (cell, union { ... }); decompose
let heap_cell = builder.add_get_tuple_field(block, union_id, TAG_CELL_INDEX)?;
let union_data = builder.add_get_tuple_field(block, union_id, TAG_DATA_INDEX)?;
// we're reading from this value, so touch the heap cell
builder.add_touch(block, heap_cell)?;
// next, unwrap the union at the tag id that we've got
let variant_id = builder.add_unwrap_union(block, union_data, *tag_id as u32)?;
let value = builder.add_get_tuple_field(block, variant_id, index)?;
// construct the box. Here the heap_cell of the tag is re-used, I'm hoping that that
// conveys to morphic that we're borrowing into the existing tag?!
builder.add_make_tuple(block, &[heap_cell, value])
}
StructAtIndex {
index, structure, ..
} => {
@ -1419,7 +1475,7 @@ fn expr_spec<'a>(
builder.add_get_tuple_field(block, value_id, *index as u32)
}
Array { elem_layout, elems } => {
let type_id = layout_spec(env, builder, interner, *elem_layout)?;
let type_id = layout_spec(env, builder, interner, interner.get_repr(*elem_layout))?;
let list = new_list(builder, block, type_id)?;
@ -1446,7 +1502,8 @@ fn expr_spec<'a>(
EmptyArray => match interner.get_repr(layout) {
LayoutRepr::Builtin(Builtin::List(element_layout)) => {
let type_id = layout_spec(env, builder, interner, element_layout)?;
let type_id =
layout_spec(env, builder, interner, interner.get_repr(element_layout))?;
new_list(builder, block, type_id)
}
_ => unreachable!("empty array does not have a list layout"),
@ -1483,8 +1540,30 @@ fn expr_spec<'a>(
let value = with_new_heap_cell(builder, block, union_data)?;
builder.add_make_named(block, MOD_APP, type_name, value)
}
FunctionPointer { .. } => {
let pointer_type = layout_spec(env, builder, interner, interner.get_repr(layout))?;
builder.add_unknown_with(block, &[], pointer_type)
}
ErasedMake { callee, value } => {
let value = match value {
Some(v) => box_erasure_value_unknown(builder, block, env.symbols[v]),
// model nullptr
None => box_erasure_value_unknown_nullptr(builder, block),
}?;
let callee = env.symbols[callee];
erasure_make(builder, block, value, callee)
}
ErasedLoad { symbol, field } => {
let value = env.symbols[symbol];
let loaded_type = layout_spec(env, builder, interner, interner.get_repr(layout))?;
erasure_load(builder, block, value, *field, loaded_type)
}
RuntimeErrorFunction(_) => {
let type_id = layout_spec(env, builder, interner, layout)?;
let type_id = layout_spec(env, builder, interner, interner.get_repr(layout))?;
builder.add_terminate(block, type_id)
}
@ -1493,6 +1572,16 @@ fn expr_spec<'a>(
builder.add_make_tuple(block, &[])
}
Alloca { initializer, .. } => {
let initializer = &initializer.as_ref().map(|s| env.symbols[s]);
let values = match initializer {
Some(initializer) => std::slice::from_ref(initializer),
None => &[],
};
let type_id = layout_spec(env, builder, interner, interner.get_repr(layout))?;
builder.add_unknown_with(block, values, type_id)
}
}
}
@ -1515,7 +1604,7 @@ fn layout_spec<'a>(
env: &mut Env<'a>,
builder: &mut impl TypeContext,
interner: &STLayoutInterner<'a>,
layout: InLayout<'a>,
layout: LayoutRepr<'a>,
) -> Result<TypeId> {
layout_spec_help(env, builder, interner, layout)
}
@ -1539,16 +1628,19 @@ fn layout_spec_help<'a>(
env: &mut Env<'a>,
builder: &mut impl TypeContext,
interner: &STLayoutInterner<'a>,
layout: InLayout<'a>,
layout: LayoutRepr<'a>,
) -> Result<TypeId> {
use LayoutRepr::*;
match interner.get_repr(layout) {
match layout {
Builtin(builtin) => builtin_spec(env, builder, interner, &builtin),
Struct(field_layouts) => build_recursive_tuple_type(env, builder, interner, field_layouts),
LambdaSet(lambda_set) => {
layout_spec_help(env, builder, interner, lambda_set.runtime_representation())
}
LambdaSet(lambda_set) => layout_spec_help(
env,
builder,
interner,
interner.get_repr(lambda_set.runtime_representation()),
),
Union(union_layout) => {
match union_layout {
UnionLayout::NonRecursive(&[]) => {
@ -1575,12 +1667,14 @@ fn layout_spec_help<'a>(
}
}
Boxed(inner_layout) => {
let inner_type = layout_spec_help(env, builder, interner, inner_layout)?;
Ptr(inner_layout) => {
let inner_type =
layout_spec_help(env, builder, interner, interner.get_repr(inner_layout))?;
let cell_type = builder.add_heap_cell_type();
builder.add_tuple_type(&[cell_type, inner_type])
}
// TODO(recursive-layouts): update once we have recursive pointer loops
RecursivePointer(union_layout) => match interner.get_repr(union_layout) {
LayoutRepr::Union(union_layout) => {
@ -1592,6 +1686,9 @@ fn layout_spec_help<'a>(
}
_ => internal_error!("somehow, a non-recursive layout is under a recursive pointer"),
},
FunctionPointer(_) => function_pointer_type(builder),
Erased(_) => erasure_type(builder),
}
}
@ -1608,7 +1705,8 @@ fn builtin_spec<'a>(
Decimal | Float(_) => builder.add_tuple_type(&[]),
Str => str_type(builder),
List(element_layout) => {
let element_type = layout_spec_help(env, builder, interner, *element_layout)?;
let element_type =
layout_spec_help(env, builder, interner, interner.get_repr(*element_layout))?;
let cell = builder.add_heap_cell_type();
let bag = builder.add_bag_type(element_type)?;
@ -1634,10 +1732,6 @@ fn static_list_type<TC: TypeContext>(builder: &mut TC) -> Result<TypeId> {
const LIST_CELL_INDEX: u32 = 0;
const LIST_BAG_INDEX: u32 = 1;
#[allow(dead_code)]
const BOX_CELL_INDEX: u32 = LIST_CELL_INDEX;
const BOX_VALUE_INDEX: u32 = LIST_BAG_INDEX;
const TAG_CELL_INDEX: u32 = 0;
const TAG_DATA_INDEX: u32 = 1;
@ -1671,3 +1765,78 @@ fn new_num(builder: &mut FuncDefBuilder, block: BlockId) -> Result<ValueId> {
// we model all our numbers as unit values
builder.add_make_tuple(block, &[])
}
fn function_pointer_type<TC: TypeContext>(builder: &mut TC) -> Result<TypeId> {
builder.add_tuple_type(&[])
}
const ERASURE_CALEE_INDEX: u32 = 0;
const ERASURE_VALUE_INDEX: u32 = 1;
/// Erasure type modeled as
///
/// ```text
/// Tuple(callee: FnPtr, value: HeapCell)
/// ```
fn erasure_type<TC: TypeContext>(builder: &mut TC) -> Result<TypeId> {
let value_cell_id = builder.add_heap_cell_type();
let callee_id = function_pointer_type(builder)?;
builder.add_tuple_type(&[value_cell_id, callee_id])
}
fn erasure_box_value_type<TC: TypeContext>(builder: &mut TC) -> TypeId {
builder.add_heap_cell_type()
}
fn box_erasure_value_unknown(
builder: &mut FuncDefBuilder,
block: BlockId,
value: ValueId,
) -> Result<ValueId> {
let heap_cell = erasure_box_value_type(builder);
builder.add_unknown_with(block, &[value], heap_cell)
}
fn box_erasure_value_unknown_nullptr(
builder: &mut FuncDefBuilder,
block: BlockId,
) -> Result<ValueId> {
let heap_cell = erasure_box_value_type(builder);
builder.add_unknown_with(block, &[], heap_cell)
}
/// Erasure value modeled as
///
/// ```text
/// callee = make_tuple(&[])
/// value = unknown(make_tuple(...captures))
///
/// x : Tuple(callee: FnPtr, value: HeapCell)
/// x = make_tuple(callee, value)
/// ```
fn erasure_make(
builder: &mut FuncDefBuilder,
block: BlockId,
value: ValueId,
callee: ValueId,
) -> Result<ValueId> {
builder.add_make_tuple(block, &[value, callee])
}
fn erasure_load(
builder: &mut FuncDefBuilder,
block: BlockId,
value: ValueId,
field: ErasedField,
loaded_type: TypeId,
) -> Result<ValueId> {
match field {
ErasedField::Callee => builder.add_get_tuple_field(block, value, ERASURE_CALEE_INDEX),
ErasedField::Value | ErasedField::ValuePtr => {
let unknown_heap_cell_value =
builder.add_get_tuple_field(block, value, ERASURE_VALUE_INDEX)?;
// Cast the unknown cell to the wanted type
builder.add_unknown_with(block, &[unknown_heap_cell_value], loaded_type)
}
}
}

View file

@ -126,7 +126,7 @@ fn find_wasi_libc_path() -> PathBuf {
internal_error!("cannot find `wasi-libc.a`")
}
#[cfg(all(unix, not(target_os = "macos")))]
#[cfg(unix)]
#[allow(clippy::too_many_arguments)]
pub fn build_zig_host_native(
env_path: &str,
@ -161,7 +161,7 @@ pub fn build_zig_host_native(
zig_cmd.args([
zig_host_src,
&format!("-femit-bin={}", emit_bin),
&format!("-femit-bin={emit_bin}"),
"--pkg-begin",
"glue",
find_zig_glue_path().to_str().unwrap(),
@ -257,99 +257,6 @@ pub fn build_zig_host_native(
zig_cmd
}
#[cfg(target_os = "macos")]
#[allow(clippy::too_many_arguments)]
pub fn build_zig_host_native(
env_path: &str,
env_home: &str,
emit_bin: &str,
zig_host_src: &str,
_target: &str,
opt_level: OptLevel,
shared_lib_path: Option<&Path>,
builtins_host_path: &Path,
// For compatibility with the non-macOS def above. Keep these in sync.
) -> Command {
use serde_json::Value;
// Run `zig env` to find the location of zig's std/ directory
let zig_env_output = zig().args(["env"]).output().unwrap();
let zig_env_json = if zig_env_output.status.success() {
std::str::from_utf8(&zig_env_output.stdout).unwrap_or_else(|utf8_err| {
internal_error!(
"`zig env` failed; its stderr output was invalid utf8 ({:?})",
utf8_err
);
})
} else {
match std::str::from_utf8(&zig_env_output.stderr) {
Ok(stderr) => internal_error!("`zig env` failed - stderr output was: {:?}", stderr),
Err(utf8_err) => internal_error!(
"`zig env` failed; its stderr output was invalid utf8 ({:?})",
utf8_err
),
}
};
let mut zig_compiler_rt_path = match serde_json::from_str(zig_env_json) {
Ok(Value::Object(map)) => match map.get("std_dir") {
Some(Value::String(std_dir)) => PathBuf::from(Path::new(std_dir)),
_ => {
internal_error!("Expected JSON containing a `std_dir` String field from `zig env`, but got: {:?}", zig_env_json);
}
},
_ => {
internal_error!(
"Expected JSON containing a `std_dir` field from `zig env`, but got: {:?}",
zig_env_json
);
}
};
zig_compiler_rt_path.push("special");
zig_compiler_rt_path.push("compiler_rt.zig");
let mut zig_cmd = zig();
zig_cmd
.env_clear()
.env("PATH", env_path)
.env("HOME", env_home);
if let Some(shared_lib_path) = shared_lib_path {
zig_cmd.args([
"build-exe",
"-fPIE",
shared_lib_path.to_str().unwrap(),
builtins_host_path.to_str().unwrap(),
]);
} else {
zig_cmd.args(["build-obj"]);
}
zig_cmd.args([
zig_host_src,
&format!("-femit-bin={}", emit_bin),
"--pkg-begin",
"glue",
find_zig_glue_path().to_str().unwrap(),
"--pkg-end",
// include the zig runtime
"--pkg-begin",
"compiler_rt",
zig_compiler_rt_path.to_str().unwrap(),
"--pkg-end",
// include libc
"--library",
"c",
]);
if matches!(opt_level, OptLevel::Optimize) {
zig_cmd.args(["-O", "ReleaseSafe"]);
} else if matches!(opt_level, OptLevel::Size) {
zig_cmd.args(["-O", "ReleaseSmall"]);
}
zig_cmd
}
pub fn build_zig_host_wasm32(
env_path: &str,
env_home: &str,
@ -492,7 +399,7 @@ pub fn build_swift_host_native(
match arch {
Architecture::Aarch64(_) => command.arg("-arm64"),
_ => command.arg(format!("-{}", arch)),
_ => command.arg(format!("-{arch}")),
};
command
@ -623,7 +530,7 @@ pub fn rebuild_host(
// on windows, we need the nightly toolchain so we can use `-Z export-executable-symbols`
// using `+nightly` only works when running cargo through rustup
let mut cmd = rustup();
cmd.args(["run", "nightly-2022-10-30", "cargo"]);
cmd.args(["run", "nightly-2023-04-15", "cargo"]);
cmd
} else {
@ -1021,7 +928,7 @@ fn link_linux(
.map(|segments| segments.join("/"))
.collect::<Vec<String>>()
.join("\n");
eprintln!("We looked in the following directories:\n{}", dirs);
eprintln!("We looked in the following directories:\n{dirs}");
process::exit(1);
}
};
@ -1171,6 +1078,12 @@ fn link_macos(
// "--gc-sections",
"-arch",
&arch,
// Suppress warnings, because otherwise it prints:
//
// ld: warning: -undefined dynamic_lookup may not work with chained fixups
//
// We can't disable that option without breaking either x64 mac or ARM mac
"-w",
"-macos_version_min",
&get_macos_version(),
])
@ -1178,8 +1091,8 @@ fn link_macos(
let sdk_path = "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/lib";
if Path::new(sdk_path).exists() {
ld_command.arg(format!("-L{}", sdk_path));
ld_command.arg(format!("-L{}/swift", sdk_path));
ld_command.arg(format!("-L{sdk_path}"));
ld_command.arg(format!("-L{sdk_path}/swift"));
};
let roc_link_flags = match env::var("ROC_LINK_FLAGS") {
@ -1381,9 +1294,7 @@ pub fn llvm_module_to_dylib(
assert!(
exit_status.success(),
"\n___________\nLinking command failed with status {:?}:\n\n {:?}\n___________\n",
exit_status,
child
"\n___________\nLinking command failed with status {exit_status:?}:\n\n {child:?}\n___________\n"
);
// Load the dylib

View file

@ -8,8 +8,8 @@ use roc_gen_dev::AssemblyBackendMode;
use roc_gen_llvm::llvm::build::{module_from_builtins, LlvmBackendMode};
use roc_gen_llvm::llvm::externs::add_default_roc_externs;
use roc_load::{
EntryPoint, ExecutionMode, ExpectMetadata, LoadConfig, LoadMonomorphizedError, LoadedModule,
LoadingProblem, MonomorphizedModule, Threading,
EntryPoint, ExecutionMode, ExpectMetadata, FunctionKind, LoadConfig, LoadMonomorphizedError,
LoadedModule, LoadingProblem, MonomorphizedModule, Threading,
};
use roc_mono::ir::{OptLevel, SingleEntryPoint};
use roc_packaging::cache::RocCacheDir;
@ -133,7 +133,7 @@ pub fn gen_from_mono_module<'a>(
// TODO make this polymorphic in the llvm functions so it can be reused for another backend.
fn gen_from_mono_module_llvm<'a>(
arena: &'a bumpalo::Bump,
mut loaded: MonomorphizedModule<'a>,
loaded: MonomorphizedModule<'a>,
roc_file_path: &Path,
target: &target_lexicon::Triple,
opt_level: OptLevel,
@ -166,7 +166,7 @@ fn gen_from_mono_module_llvm<'a>(
let kind_id = Attribute::get_named_enum_kind_id("alwaysinline");
debug_assert!(kind_id > 0);
let enum_attr = context.create_enum_attribute(kind_id, 1);
let enum_attr = context.create_enum_attribute(kind_id, 0);
for function in module.get_functions() {
let name = function.get_name().to_str().unwrap();
@ -232,9 +232,10 @@ fn gen_from_mono_module_llvm<'a>(
roc_gen_llvm::llvm::build::build_procedures(
&env,
&mut loaded.layout_interner,
&loaded.layout_interner,
opt_level,
loaded.procedures,
loaded.host_exposed_lambda_sets,
entry_point,
Some(&app_ll_file),
&loaded.glue_layouts,
@ -320,10 +321,10 @@ fn gen_from_mono_module_llvm<'a>(
if !unrecognized.is_empty() {
let out = unrecognized
.iter()
.map(|x| format!("{:?}", x))
.map(|x| format!("{x:?}"))
.collect::<Vec<String>>()
.join(", ");
eprintln!("Unrecognized sanitizer: {}\nSupported options are \"address\", \"memory\", \"thread\", \"cargo-fuzz\", and \"afl.rs\".", out);
eprintln!("Unrecognized sanitizer: {out}\nSupported options are \"address\", \"memory\", \"thread\", \"cargo-fuzz\", and \"afl.rs\".");
eprintln!("Note: \"cargo-fuzz\" and \"afl.rs\" both enable sanitizer coverage for fuzzing. They just use different parameters to match the respective libraries.")
}
@ -340,7 +341,7 @@ fn gen_from_mono_module_llvm<'a>(
}
let opt = opt.output().unwrap();
assert!(opt.stderr.is_empty(), "{:#?}", opt);
assert!(opt.stderr.is_empty(), "{opt:#?}");
// write the .o file. Note that this builds the .o for the local machine,
// and ignores the `target_machine` entirely.
@ -358,7 +359,7 @@ fn gen_from_mono_module_llvm<'a>(
.output()
.unwrap();
assert!(bc_to_object.status.success(), "{:#?}", bc_to_object);
assert!(bc_to_object.status.success(), "{bc_to_object:#?}");
MemoryBuffer::create_from_file(&app_o_file).expect("memory buffer creation works")
} else if emit_debug_info {
@ -414,7 +415,7 @@ fn gen_from_mono_module_llvm<'a>(
.output()
.unwrap();
assert!(ll_to_object.stderr.is_empty(), "{:#?}", ll_to_object);
assert!(ll_to_object.stderr.is_empty(), "{ll_to_object:#?}");
}
_ => unreachable!(),
}
@ -716,13 +717,13 @@ pub fn handle_error_module(
pub fn handle_loading_problem(problem: LoadingProblem) -> std::io::Result<i32> {
match problem {
LoadingProblem::FormattedReport(report) => {
print!("{}", report);
print!("{report}");
Ok(1)
}
_ => {
// TODO: tighten up the types here, we should always end up with a
// formatted report from load.
print!("Failed with error: {:?}", problem);
print!("Failed with error: {problem:?}");
Ok(1)
}
}
@ -740,8 +741,20 @@ pub fn standard_load_config(
BuildOrdering::AlwaysBuild => ExecutionMode::Executable,
};
// UNSTABLE(lambda-erasure)
let function_kind = if cfg!(debug_assertions) {
if std::env::var("EXPERIMENTAL_ROC_ERASE").is_ok() {
FunctionKind::Erased
} else {
FunctionKind::LambdaSet
}
} else {
FunctionKind::LambdaSet
};
LoadConfig {
target_info,
function_kind,
render: RenderTarget::ColorTerminal,
palette: DEFAULT_PALETTE,
threading,
@ -889,7 +902,7 @@ fn build_loaded_file<'a>(
buf.push('\n');
use std::fmt::Write;
write!(buf, "{}", module_timing).unwrap();
write!(buf, "{module_timing}").unwrap();
if it.peek().is_some() {
buf.push('\n');
@ -914,10 +927,7 @@ fn build_loaded_file<'a>(
.expect("Failed to (re)build platform.");
if emit_timings && !is_platform_prebuilt {
println!(
"Finished rebuilding the platform in {} ms\n",
rebuild_duration
);
println!("Finished rebuilding the platform in {rebuild_duration} ms\n");
}
Some(HostRebuildTiming::BeforeApp(rebuild_duration))
@ -957,8 +967,7 @@ fn build_loaded_file<'a>(
if emit_timings {
println!(
"\n\nCompilation finished!\n\nHere's how long each module took to compile:\n\n{}",
buf
"\n\nCompilation finished!\n\nHere's how long each module took to compile:\n\n{buf}"
);
println!(
@ -972,10 +981,7 @@ fn build_loaded_file<'a>(
let rebuild_duration = thread.join().expect("Failed to (re)build platform.");
if emit_timings && !is_platform_prebuilt {
println!(
"Finished rebuilding the platform in {} ms\n",
rebuild_duration
);
println!("Finished rebuilding the platform in {rebuild_duration} ms\n");
}
}
@ -1007,7 +1013,7 @@ fn build_loaded_file<'a>(
};
let app_o_file = tempfile::Builder::new()
.prefix("roc_app")
.suffix(&format!(".{}", extension))
.suffix(&format!(".{extension}"))
.tempfile()
.map_err(|err| todo!("TODO Gracefully handle tempfile creation error {:?}", err))?;
let app_o_file = app_o_file.path();
@ -1212,6 +1218,8 @@ pub fn check_file<'a>(
let load_config = LoadConfig {
target_info,
// TODO: we may not want this for just checking.
function_kind: FunctionKind::LambdaSet,
// TODO: expose this from CLI?
render: RenderTarget::ColorTerminal,
palette: DEFAULT_PALETTE,
@ -1257,8 +1265,7 @@ pub fn check_file<'a>(
if emit_timings {
println!(
"\n\nCompilation finished!\n\nHere's how long each module took to compile:\n\n{}",
buf
"\n\nCompilation finished!\n\nHere's how long each module took to compile:\n\n{buf}"
);
println!("Finished checking in {} ms\n", compilation_end.as_millis(),);

View file

@ -87,12 +87,12 @@ pub fn target_zig_str(target: &Triple) -> &'static str {
architecture: Architecture::X86_64,
operating_system: OperatingSystem::Darwin,
..
} => "x86_64-apple-darwin",
} => "x86_64-macos-gnu",
Triple {
architecture: Architecture::Aarch64(_),
operating_system: OperatingSystem::Darwin,
..
} => "aarch64-apple-darwin",
} => "aarch64-macos-gnu",
_ => internal_error!("TODO gracefully handle unsupported target: {:?}", target),
}
}
@ -149,20 +149,24 @@ pub fn target_machine(
init_arch(target);
let code_model = match target.architecture {
// LLVM 12 will not compile our programs without a large code model.
// The reason is not totally clear to me, but my guess is a few special-cases in
// llvm/lib/Target/AArch64/AArch64ISelLowering.cpp (instructions)
// llvm/lib/Target/AArch64/AArch64Subtarget.cpp (GoT tables)
// Revisit when upgrading to LLVM 13.
Architecture::Aarch64(..) => CodeModel::Large,
_ => CodeModel::Default,
};
// We used to have a problem that LLVM 12 would not compile our programs without a large code model.
// The reason was not totally clear to us, but one guess is a few special-cases in
// llvm/lib/Target/AArch64/AArch64ISelLowering.cpp (instructions)
// llvm/lib/Target/AArch64/AArch64Subtarget.cpp (GoT tables)
// Revisit when upgrading to LLVM 13.
//
// Most recently, we seem to only see this problem on macOS ARM64; removing this
// failed macOS CI here: https://github.com/roc-lang/roc/pull/5644
#[cfg(all(target_arch = "aarch64", target_os = "macos"))]
let code_model = CodeModel::Large;
#[cfg(not(all(target_arch = "aarch64", target_os = "macos")))]
let code_model = CodeModel::Default;
Target::from_name(arch).unwrap().create_target_machine(
&TargetTriple::create(target_triple_str(target)),
"generic",
"", // TODO: this probably should be TargetMachine::get_host_cpu_features() to enable all features.
"",
opt,
reloc,
code_model,

View file

@ -35,6 +35,7 @@ fn main() {
generate_bc_file(&bitcode_path, "ir-i386", "builtins-i386");
generate_bc_file(&bitcode_path, "ir-x86_64", "builtins-x86_64");
generate_bc_file(&bitcode_path, "ir-aarch64", "builtins-aarch64");
generate_bc_file(
&bitcode_path,
"ir-windows-x86_64",
@ -61,12 +62,12 @@ fn generate_bc_file(bitcode_path: &Path, zig_object: &str, file_name: &str) {
ll_path.set_extension("ll");
let dest_ir_host = ll_path.to_str().expect("Invalid dest ir path");
println!("Compiling host ir to: {}", dest_ir_host);
println!("Compiling host ir to: {dest_ir_host}");
let mut bc_path = bitcode_path.join(file_name);
bc_path.set_extension("bc");
let dest_bc_64bit = bc_path.to_str().expect("Invalid dest bc path");
println!("Compiling 64-bit bitcode to: {}", dest_bc_64bit);
println!("Compiling 64-bit bitcode to: {dest_bc_64bit}");
// workaround for github.com/ziglang/zig/issues/9711
#[cfg(target_os = "macos")]
@ -104,7 +105,7 @@ fn run_command(mut command: Command, flaky_fail_counter: usize) {
false => {
let error_str = match str::from_utf8(&output.stderr) {
Ok(stderr) => stderr.to_string(),
Err(_) => format!("Failed to run \"{}\"", command_str),
Err(_) => format!("Failed to run \"{command_str}\""),
};
// Flaky test errors that only occur sometimes on MacOS ci server.

View file

@ -66,7 +66,7 @@ fn generate_object_file(bitcode_path: &Path, zig_object: &str, object_file_name:
let src_obj_path = bitcode_path.join(object_file_name);
let src_obj = src_obj_path.to_str().expect("Invalid src object path");
println!("Compiling zig object `{}` to: {}", zig_object, src_obj);
println!("Compiling zig object `{zig_object}` to: {src_obj}");
if !DEBUG {
let mut zig_cmd = zig();
@ -77,7 +77,7 @@ fn generate_object_file(bitcode_path: &Path, zig_object: &str, object_file_name:
run_command(zig_cmd, 0);
println!("Moving zig object `{}` to: {}", zig_object, dest_obj);
println!("Moving zig object `{zig_object}` to: {dest_obj}");
// we store this .o file in rust's `target` folder (for wasm we need to leave a copy here too)
fs::copy(src_obj, dest_obj).unwrap_or_else(|err| {
@ -167,7 +167,7 @@ fn run_command(mut command: Command, flaky_fail_counter: usize) {
false => {
let error_str = match str::from_utf8(&output.stderr) {
Ok(stderr) => stderr.to_string(),
Err(_) => format!("Failed to run \"{}\"", command_str),
Err(_) => format!("Failed to run \"{command_str}\""),
};
// Flaky test errors that only occur sometimes on MacOS ci server.

View file

@ -25,18 +25,19 @@ pub fn build(b: *Builder) void {
const host_target = b.standardTargetOptions(.{
.default_target = CrossTarget{
.cpu_model = .baseline,
// TODO allow for native target for maximum speed
},
});
const linux32_target = makeLinux32Target();
const linux64_target = makeLinux64Target();
const linux_x64_target = makeLinuxX64Target();
const linux_aarch64_target = makeLinuxAarch64Target();
const windows64_target = makeWindows64Target();
const wasm32_target = makeWasm32Target();
// LLVM IR
generateLlvmIrFile(b, mode, host_target, main_path, "ir", "builtins-host");
generateLlvmIrFile(b, mode, linux32_target, main_path, "ir-i386", "builtins-i386");
generateLlvmIrFile(b, mode, linux64_target, main_path, "ir-x86_64", "builtins-x86_64");
generateLlvmIrFile(b, mode, linux_x64_target, main_path, "ir-x86_64", "builtins-x86_64");
generateLlvmIrFile(b, mode, linux_aarch64_target, main_path, "ir-aarch64", "builtins-aarch64");
generateLlvmIrFile(b, mode, windows64_target, main_path, "ir-windows-x86_64", "builtins-windows-x86_64");
generateLlvmIrFile(b, mode, wasm32_target, main_path, "ir-wasm32", "builtins-wasm32");
@ -89,6 +90,7 @@ fn generateObjectFile(
obj.strip = true;
obj.target = target;
obj.link_function_sections = true;
obj.force_pic = true;
const obj_step = b.step(step_name, "Build object file for linking");
obj_step.dependOn(&obj.step);
}
@ -103,7 +105,17 @@ fn makeLinux32Target() CrossTarget {
return target;
}
fn makeLinux64Target() CrossTarget {
fn makeLinuxAarch64Target() CrossTarget {
var target = CrossTarget.parse(.{}) catch unreachable;
target.cpu_arch = std.Target.Cpu.Arch.aarch64;
target.os_tag = std.Target.Os.Tag.linux;
target.abi = std.Target.Abi.musl;
return target;
}
fn makeLinuxX64Target() CrossTarget {
var target = CrossTarget.parse(.{}) catch unreachable;
target.cpu_arch = std.Target.Cpu.Arch.x86_64;

View file

@ -18,6 +18,7 @@ const v2u64 = @Vector(2, u64);
// Export it as weak incase it is already linked in by something else.
comptime {
@export(__muloti4, .{ .name = "__muloti4", .linkage = .Weak });
@export(__lshrti3, .{ .name = "__lshrti3", .linkage = .Weak });
if (want_windows_v2u64_abi) {
@export(__divti3_windows_x86_64, .{ .name = "__divti3", .linkage = .Weak });
@export(__modti3_windows_x86_64, .{ .name = "__modti3", .linkage = .Weak });
@ -440,3 +441,47 @@ pub inline fn floatFractionalBits(comptime T: type) comptime_int {
else => @compileError("unknown floating point type " ++ @typeName(T)),
};
}
pub fn __lshrti3(a: i128, b: i32) callconv(.C) i128 {
return lshrXi3(i128, a, b);
}
// Logical shift right: shift in 0 from left to right
// Precondition: 0 <= b < T.bit_count
inline fn lshrXi3(comptime T: type, a: T, b: i32) T {
const word_t = HalveInt(T, false);
const S = std.math.Log2Int(word_t.HalfT);
const input = word_t{ .all = a };
var output: word_t = undefined;
if (b >= word_t.bits) {
output.s.high = 0;
output.s.low = input.s.high >> @intCast(S, b - word_t.bits);
} else if (b == 0) {
return a;
} else {
output.s.high = input.s.high >> @intCast(S, b);
output.s.low = input.s.high << @intCast(S, word_t.bits - b);
output.s.low |= input.s.low >> @intCast(S, b);
}
return output.all;
}
/// Allows to access underlying bits as two equally sized lower and higher
/// signed or unsigned integers.
fn HalveInt(comptime T: type, comptime signed_half: bool) type {
return extern union {
pub const bits = @divExact(@typeInfo(T).Int.bits, 2);
pub const HalfTU = std.meta.Int(.unsigned, bits);
pub const HalfTS = std.meta.Int(.signed, bits);
pub const HalfT = if (signed_half) HalfTS else HalfTU;
all: T,
s: if (native_endian == .Little)
extern struct { low: HalfT, high: HalfT }
else
extern struct { high: HalfT, low: HalfT },
};
}

View file

@ -171,8 +171,8 @@ comptime {
exportStrFn(str.fromUtf8RangeC, "from_utf8_range");
exportStrFn(str.repeat, "repeat");
exportStrFn(str.strTrim, "trim");
exportStrFn(str.strTrimLeft, "trim_left");
exportStrFn(str.strTrimRight, "trim_right");
exportStrFn(str.strTrimStart, "trim_start");
exportStrFn(str.strTrimEnd, "trim_end");
exportStrFn(str.strCloneTo, "clone_to");
exportStrFn(str.withCapacity, "with_capacity");
exportStrFn(str.strGraphemes, "graphemes");
@ -195,8 +195,10 @@ comptime {
exportUtilsFn(utils.test_panic, "test_panic");
exportUtilsFn(utils.increfRcPtrC, "incref_rc_ptr");
exportUtilsFn(utils.decrefRcPtrC, "decref_rc_ptr");
exportUtilsFn(utils.freeRcPtrC, "free_rc_ptr");
exportUtilsFn(utils.increfDataPtrC, "incref_data_ptr");
exportUtilsFn(utils.decrefDataPtrC, "decref_data_ptr");
exportUtilsFn(utils.freeDataPtrC, "free_data_ptr");
exportUtilsFn(utils.isUnique, "is_unique");
exportUtilsFn(utils.decrefCheckNullC, "decref_check_null");
exportUtilsFn(utils.allocateWithRefcountC, "allocate_with_refcount");

View file

@ -2302,7 +2302,7 @@ pub fn strTrim(input_string: RocStr) callconv(.C) RocStr {
}
}
pub fn strTrimLeft(input_string: RocStr) callconv(.C) RocStr {
pub fn strTrimStart(input_string: RocStr) callconv(.C) RocStr {
var string = input_string;
if (string.isEmpty()) {
@ -2350,7 +2350,7 @@ pub fn strTrimLeft(input_string: RocStr) callconv(.C) RocStr {
}
}
pub fn strTrimRight(input_string: RocStr) callconv(.C) RocStr {
pub fn strTrimEnd(input_string: RocStr) callconv(.C) RocStr {
var string = input_string;
if (string.isEmpty()) {
@ -2583,22 +2583,22 @@ test "strTrim: small to small" {
try expect(trimmed.isSmallStr());
}
test "strTrimLeft: empty" {
const trimmedEmpty = strTrimLeft(RocStr.empty());
test "strTrimStart: empty" {
const trimmedEmpty = strTrimStart(RocStr.empty());
try expect(trimmedEmpty.eq(RocStr.empty()));
}
test "strTrimLeft: blank" {
test "strTrimStart: blank" {
const original_bytes = " ";
const original = RocStr.init(original_bytes, original_bytes.len);
defer original.decref();
const trimmed = strTrimLeft(original);
const trimmed = strTrimStart(original);
try expect(trimmed.eq(RocStr.empty()));
}
test "strTrimLeft: large to large" {
test "strTrimStart: large to large" {
const original_bytes = " hello even more giant world ";
const original = RocStr.init(original_bytes, original_bytes.len);
defer original.decref();
@ -2611,12 +2611,12 @@ test "strTrimLeft: large to large" {
try expect(!expected.isSmallStr());
const trimmed = strTrimLeft(original);
const trimmed = strTrimStart(original);
try expect(trimmed.eq(expected));
}
test "strTrimLeft: large to small" {
test "strTrimStart: large to small" {
// `original` will be consumed by the concat; do not free explicitly
const original_bytes = " hello ";
const original = RocStr.init(original_bytes, original_bytes.len);
@ -2629,14 +2629,14 @@ test "strTrimLeft: large to small" {
try expect(expected.isSmallStr());
const trimmed = strTrimLeft(original);
const trimmed = strTrimStart(original);
defer trimmed.decref();
try expect(trimmed.eq(expected));
try expect(!trimmed.isSmallStr());
}
test "strTrimLeft: small to small" {
test "strTrimStart: small to small" {
const original_bytes = " hello ";
const original = RocStr.init(original_bytes, original_bytes.len);
defer original.decref();
@ -2649,28 +2649,28 @@ test "strTrimLeft: small to small" {
try expect(expected.isSmallStr());
const trimmed = strTrimLeft(original);
const trimmed = strTrimStart(original);
try expect(trimmed.eq(expected));
try expect(trimmed.isSmallStr());
}
test "strTrimRight: empty" {
const trimmedEmpty = strTrimRight(RocStr.empty());
test "strTrimEnd: empty" {
const trimmedEmpty = strTrimEnd(RocStr.empty());
try expect(trimmedEmpty.eq(RocStr.empty()));
}
test "strTrimRight: blank" {
test "strTrimEnd: blank" {
const original_bytes = " ";
const original = RocStr.init(original_bytes, original_bytes.len);
defer original.decref();
const trimmed = strTrimRight(original);
const trimmed = strTrimEnd(original);
try expect(trimmed.eq(RocStr.empty()));
}
test "strTrimRight: large to large" {
test "strTrimEnd: large to large" {
const original_bytes = " hello even more giant world ";
const original = RocStr.init(original_bytes, original_bytes.len);
defer original.decref();
@ -2683,12 +2683,12 @@ test "strTrimRight: large to large" {
try expect(!expected.isSmallStr());
const trimmed = strTrimRight(original);
const trimmed = strTrimEnd(original);
try expect(trimmed.eq(expected));
}
test "strTrimRight: large to small" {
test "strTrimEnd: large to small" {
// `original` will be consumed by the concat; do not free explicitly
const original_bytes = " hello ";
const original = RocStr.init(original_bytes, original_bytes.len);
@ -2701,14 +2701,14 @@ test "strTrimRight: large to small" {
try expect(expected.isSmallStr());
const trimmed = strTrimRight(original);
const trimmed = strTrimEnd(original);
defer trimmed.decref();
try expect(trimmed.eq(expected));
try expect(!trimmed.isSmallStr());
}
test "strTrimRight: small to small" {
test "strTrimEnd: small to small" {
const original_bytes = " hello ";
const original = RocStr.init(original_bytes, original_bytes.len);
defer original.decref();
@ -2721,7 +2721,7 @@ test "strTrimRight: small to small" {
try expect(expected.isSmallStr());
const trimmed = strTrimRight(original);
const trimmed = strTrimEnd(original);
try expect(trimmed.eq(expected));
try expect(trimmed.isSmallStr());

View file

@ -220,6 +220,29 @@ pub fn increfDataPtrC(
return increfRcPtrC(isizes, inc_amount);
}
pub fn freeDataPtrC(
bytes_or_null: ?[*]isize,
alignment: u32,
) callconv(.C) void {
var bytes = bytes_or_null orelse return;
const ptr = @ptrToInt(bytes);
const tag_mask: usize = if (@sizeOf(usize) == 8) 0b111 else 0b11;
const masked_ptr = ptr & ~tag_mask;
const isizes: [*]isize = @intToPtr([*]isize, masked_ptr);
return freeRcPtrC(isizes - 1, alignment);
}
pub fn freeRcPtrC(
bytes_or_null: ?[*]isize,
alignment: u32,
) callconv(.C) void {
var bytes = bytes_or_null orelse return;
return free_ptr_to_refcount(bytes, alignment);
}
pub fn decref(
bytes_or_null: ?[*]u8,
data_bytes: usize,
@ -236,13 +259,23 @@ pub fn decref(
decref_ptr_to_refcount(isizes - 1, alignment);
}
inline fn decref_ptr_to_refcount(
inline fn free_ptr_to_refcount(
refcount_ptr: [*]isize,
alignment: u32,
) void {
if (RC_TYPE == Refcount.none) return;
const extra_bytes = std.math.max(alignment, @sizeOf(usize));
// NOTE: we don't even check whether the refcount is "infinity" here!
dealloc(@ptrCast([*]u8, refcount_ptr) - (extra_bytes - @sizeOf(usize)), alignment);
}
inline fn decref_ptr_to_refcount(
refcount_ptr: [*]isize,
alignment: u32,
) void {
if (RC_TYPE == Refcount.none) return;
if (DEBUG_INCDEC and builtin.target.cpu.arch != .wasm32) {
std.debug.print("| decrement {*}: ", .{refcount_ptr});
}
@ -264,13 +297,13 @@ inline fn decref_ptr_to_refcount(
}
if (refcount == REFCOUNT_ONE_ISIZE) {
dealloc(@ptrCast([*]u8, refcount_ptr) - (extra_bytes - @sizeOf(usize)), alignment);
free_ptr_to_refcount(refcount_ptr, alignment);
}
},
Refcount.atomic => {
var last = @atomicRmw(isize, &refcount_ptr[0], std.builtin.AtomicRmwOp.Sub, 1, Monotonic);
if (last == REFCOUNT_ONE_ISIZE) {
dealloc(@ptrCast([*]u8, refcount_ptr) - (extra_bytes - @sizeOf(usize)), alignment);
free_ptr_to_refcount(refcount_ptr, alignment);
}
},
Refcount.none => unreachable,

View file

@ -1,3 +1,7 @@
## Most users won't need Box, it is used for:
## - Holding unknown Roc types when developing [platforms](https://github.com/roc-lang/roc/wiki/Roc-concepts-explained#platform).
## - To improve performance in rare cases.
##
interface Box
exposes [box, unbox]
imports []

View file

@ -7,6 +7,7 @@ interface Dict
clear,
capacity,
len,
isEmpty,
get,
contains,
insert,
@ -21,6 +22,8 @@ interface Dict
insertAll,
keepShared,
removeAll,
map,
joinMap,
]
imports [
Bool.{ Bool, Eq },
@ -98,14 +101,14 @@ Dict k v := {
data : List (k, v),
size : Nat,
} where k implements Hash & Eq
implements [
Eq {
isEq,
},
Hash {
hash: hashDict,
},
]
implements [
Eq {
isEq,
},
Hash {
hash: hashDict,
},
]
isEq : Dict k v, Dict k v -> Bool where k implements Hash & Eq, v implements Eq
isEq = \xs, ys ->
@ -139,12 +142,12 @@ empty = \{} ->
## Returns the max number of elements the dictionary can hold before requiring a rehash.
## ```
## foodDict =
## Dict.empty {}
## |> Dict.insert "apple" "fruit"
## Dict.empty {}
## |> Dict.insert "apple" "fruit"
##
## capacityOfDict = Dict.capacity foodDict
## ```
capacity : Dict k v -> Nat where k implements Hash & Eq
capacity : Dict * * -> Nat
capacity = \@Dict { dataIndices } ->
cap = List.len dataIndices
@ -192,10 +195,20 @@ fromList = \data ->
## |> Dict.len
## |> Bool.isEq 3
## ```
len : Dict k v -> Nat where k implements Hash & Eq
len : Dict * * -> Nat
len = \@Dict { size } ->
size
## Check if the dictinoary is empty.
## ```
## Dict.isEmpty (Dict.empty {} |> Dict.insert "key" 42)
##
## Dict.isEmpty (Dict.empty {})
## ```
isEmpty : Dict * * -> Bool
isEmpty = \@Dict { size } ->
size == 0
## Clears all elements from a dictionary keeping around the allocation if it isn't huge.
## ```
## songs =
@ -225,6 +238,30 @@ clear = \@Dict { metadata, dataIndices, data } ->
size: 0,
}
## Convert each value in the dictionary to something new, by calling a conversion
## function on each of them which receives both the key and the old value. Then return a
## new dictionary containing the same keys and the converted values.
map : Dict k a, (k, a -> b) -> Dict k b
where k implements Hash & Eq, b implements Hash & Eq
map = \dict, transform ->
init = withCapacity (capacity dict)
walk dict init \answer, k, v ->
insert answer k (transform k v)
## Like [Dict.map], except the transformation function wraps the return value
## in a dictionary. At the end, all the dictionaries get joined together
## (using [Dict.insertAll]) into one dictionary.
##
## You may know a similar function named `concatMap` in other languages.
joinMap : Dict a b, (a, b -> Dict x y) -> Dict x y
where a implements Hash & Eq, x implements Hash & Eq
joinMap = \dict, transform ->
init = withCapacity (capacity dict) # Might be a pessimization
walk dict init \answer, k, v ->
insertAll answer (transform k v)
## Iterate through the keys and values in the dictionary and call the provided
## function with signature `state, k, v -> state` for each value, with an
## initial `state` value provided for the first call.
@ -976,16 +1013,16 @@ expect
# TODO: Add a builtin to distinguish big endian systems and change loading orders.
# TODO: Switch out Wymum on systems with slow 128bit multiplication.
LowLevelHasher := { originalSeed : U64, state : U64 } implements [
Hasher {
addBytes,
addU8,
addU16,
addU32,
addU64,
addU128,
complete,
},
]
Hasher {
addBytes,
addU8,
addU16,
addU32,
addU64,
addU128,
complete,
},
]
# unsafe primitive that does not perform a bounds check
# TODO hide behind an InternalList.roc module

View file

@ -208,6 +208,9 @@ interface List
## * Even when copying is faster, other list operations may still be slightly slower with persistent data structures. For example, even if it were a persistent data structure, [List.map], [List.walk], and [List.keepIf] would all need to traverse every element in the list and build up the result from scratch. These operations are all
## * Roc's compiler optimizes many list operations into in-place mutations behind the scenes, depending on how the list is being used. For example, [List.map], [List.keepIf], and [List.set] can all be optimized to perform in-place mutations.
## * If possible, it is usually best for performance to use large lists in a way where the optimizer can turn them into in-place mutations. If this is not possible, a persistent data structure might be faster - but this is a rare enough scenario that it would not be good for the average Roc program's performance if this were the way [List] worked by default. Instead, you can look outside Roc's standard modules for an implementation of a persistent data structure - likely built using [List] under the hood!
# separator so List.isEmpty doesn't absorb the above into its doc comment
## Check if the list is empty.
## ```
## List.isEmpty [1, 2, 3]
@ -222,6 +225,13 @@ isEmpty = \list ->
# but will cause a reference count increment on the value it got out of the list
getUnsafe : List a, Nat -> a
## Returns an element from a list at the given index.
##
## Returns `Err OutOfBounds` if the given index exceeds the List's length
## ```
## expect List.get [100, 200, 300] 1 == Ok 200
## expect List.get [100, 200, 300] 5 == Err OutOfBounds
## ```
get : List a, Nat -> Result a [OutOfBounds]
get = \list, index ->
if index < List.len list then
@ -352,6 +362,10 @@ releaseExcessCapacity : List a -> List a
concat : List a, List a -> List a
## Returns the last element in the list, or `ListWasEmpty` if it was empty.
## ```
## expect List.last [1, 2, 3] == Ok 3
## expect List.last [] == Err ListWasEmpty
## ```
last : List a -> Result a [ListWasEmpty]
last = \list ->
when List.get list (Num.subSaturated (List.len list) 1) is
@ -383,7 +397,7 @@ repeatHelp = \value, count, accum ->
## Returns the list with its elements reversed.
## ```
## List.reverse [1, 2, 3]
## expect List.reverse [1, 2, 3] == [3, 2, 1]
## ```
reverse : List a -> List a
reverse = \list ->
@ -397,9 +411,9 @@ reverseHelp = \list, left, right ->
## Join the given lists together into one list.
## ```
## List.join [[1, 2, 3], [4, 5], [], [6, 7]]
## List.join [[], []]
## List.join []
## expect List.join [[1], [2, 3], [], [4, 5]] == [1, 2, 3, 4, 5]
## expect List.join [[], []] == []
## expect List.join [] == []
## ```
join : List (List a) -> List a
join = \lists ->
@ -597,6 +611,10 @@ dropIf = \list, predicate ->
## Run the given function on each element of a list, and return the
## number of elements for which the function returned `Bool.true`.
## ```
## expect List.countIf [1, -2, -3] Num.isNegative == 2
## expect List.countIf [1, 2, 3] (\num -> num > 1 ) == 2
## ```
countIf : List a, (a -> Bool) -> Nat
countIf = \list, predicate ->
walkState = \state, elem ->
@ -610,11 +628,12 @@ countIf = \list, predicate ->
## This works like [List.map], except only the transformed values that are
## wrapped in `Ok` are kept. Any that are wrapped in `Err` are dropped.
## ```
## List.keepOks [["a", "b"], [], [], ["c", "d", "e"]] List.last
## expect List.keepOks ["1", "Two", "23", "Bird"] Str.toI32 == [1, 23]
##
## fn = \str -> if Str.isEmpty str then Err StrWasEmpty else Ok (Str.len str)
## expect List.keepOks [["a", "b"], [], ["c", "d", "e"], [] ] List.first == ["a", "c"]
##
## List.keepOks ["", "a", "bc", "", "d", "ef", ""]
## fn = \str -> if Str.isEmpty str then Err StrWasEmpty else Ok str
## expect List.keepOks ["", "a", "bc", "", "d", "ef", ""] fn == ["a", "bc", "d", "ef"]
## ```
keepOks : List before, (before -> Result after *) -> List after
keepOks = \list, toResult ->
@ -646,9 +665,9 @@ keepErrs = \list, toResult ->
## Convert each element in the list to something new, by calling a conversion
## function on each of them. Then return a new list of the converted values.
## ```
## List.map [1, 2, 3] (\num -> num + 1)
## expect List.map [1, 2, 3] (\num -> num + 1) == [2, 3, 4]
##
## List.map ["", "a", "bc"] Str.isEmpty
## expect List.map ["", "a", "bc"] Str.isEmpty == [Bool.true, Bool.false, Bool.false]
## ```
map : List a, (a -> b) -> List b
@ -675,6 +694,9 @@ map4 : List a, List b, List c, List d, (a, b, c, d -> e) -> List e
## This works like [List.map], except it also passes the index
## of the element to the conversion function.
## ```
## expect List.mapWithIndex [10, 20, 30] (\num, index -> num + index) == [10, 21, 32]
## ```
mapWithIndex : List a, (a, Nat -> b) -> List b
mapWithIndex = \src, func ->
length = len src
@ -1160,8 +1182,17 @@ mapTry = \list, toResult ->
Result.map (toResult elem) \ok ->
List.append state ok
## This is the same as `iterate` but with [Result] instead of `[Continue, Break]`.
## Using `Result` saves a conditional in `mapTry`.
## Same as [List.walk], except you can stop walking early by returning `Err`.
##
## ## Performance Details
##
## Compared to [List.walk], this can potentially visit fewer elements (which can
## improve performance) at the cost of making each step take longer.
## However, the added cost to each step is extremely small, and can easily
## be outweighed if it results in skipping even a small number of elements.
##
## As such, it is typically better for performance to use this over [List.walk]
## if returning `Break` earlier than the last element is expected to be common.
walkTry : List elem, state, (state, elem -> Result state err) -> Result state err
walkTry = \list, init, func ->
walkTryHelp list init func 0 (List.len list)

View file

@ -33,12 +33,17 @@ interface Num
Decimal,
Binary32,
Binary64,
e,
pi,
tau,
abs,
absDiff,
neg,
add,
sub,
mul,
min,
max,
isLt,
isLte,
isGt,
@ -81,6 +86,7 @@ interface Num
bitwiseAnd,
bitwiseXor,
bitwiseOr,
bitwiseNot,
shiftLeftBy,
shiftRightBy,
shiftRightZfBy,
@ -492,6 +498,18 @@ F32 : Num (FloatingPoint Binary32)
## [sqrt] and trigonometry are massively slower with [Dec] than with [F64].
Dec : Num (FloatingPoint Decimal)
## Euler's number (e)
e : Frac *
e = 2.71828182845904523536028747135266249775724709369995
## Archimedes' constant (π)
pi : Frac *
pi = 3.14159265358979323846264338327950288419716939937510
## Circle constant (τ)
tau : Frac *
tau = 2 * pi
# ------- Functions
## Convert a number to a [Str].
##
@ -777,6 +795,34 @@ sub : Num a, Num a -> Num a
## ∞ or -∞. For all other number types, overflow results in a panic.
mul : Num a, Num a -> Num a
## Obtains the smaller between two numbers of the same type.
##
## ```
## Num.min 100 0
##
## Num.min 3.0 -3.0
## ```
min : Num a, Num a -> Num a
min = \a, b ->
if a < b then
a
else
b
## Obtains the greater between two numbers of the same type.
##
## ```
## Num.max 100 0
##
## Num.max 3.0 -3.0
## ```
max : Num a, Num a -> Num a
max = \a, b ->
if a > b then
a
else
b
sin : Frac a -> Frac a
cos : Frac a -> Frac a
@ -930,10 +976,25 @@ remChecked = \a, b ->
isMultipleOf : Int a, Int a -> Bool
## Does a "bitwise and". Each bit of the output is 1 if the corresponding bit
## of x AND of y is 1, otherwise it's 0.
bitwiseAnd : Int a, Int a -> Int a
## Does a "bitwise exclusive or". Each bit of the output is the same as the
## corresponding bit in x if that bit in y is 0, and it's the complement of
## the bit in x if that bit in y is 1.
bitwiseXor : Int a, Int a -> Int a
## Does a "bitwise or". Each bit of the output is 0 if the corresponding bit
## of x OR of y is 0, otherwise it's 1.
bitwiseOr : Int a, Int a -> Int a
## Returns the complement of x - the number you get by switching each 1 for a
## 0 and each 0 for a 1. This is the same as -x - 1.
bitwiseNot : Int a -> Int a
bitwiseNot = \n ->
bitwiseXor n (subWrap 0 1)
## Bitwise left shift of a number by another
##
## The least significant bits always become 0. This means that shifting left is

View file

@ -42,8 +42,8 @@ withDefault = \result, default ->
## function on it. Then returns a new `Ok` holding the transformed value. If the
## result is `Err`, this has no effect. Use [mapErr] to transform an `Err`.
## ```
## Result.map (Ok 12) Num.negate
## Result.map (Err "yipes!") Num.negate
## Result.map (Ok 12) Num.neg
## Result.map (Err "yipes!") Num.neg
## ```
##
## Functions like `map` are common in Roc; see for example [List.map],

View file

@ -7,6 +7,8 @@ interface Set
walkUntil,
insert,
len,
isEmpty,
capacity,
remove,
contains,
toList,
@ -14,6 +16,8 @@ interface Set
union,
intersection,
difference,
map,
joinMap,
]
imports [
List,
@ -26,14 +30,14 @@ interface Set
## Provides a [set](https://en.wikipedia.org/wiki/Set_(abstract_data_type))
## type which stores a collection of unique values, without any ordering
Set k := Dict.Dict k {} where k implements Hash & Eq
implements [
Eq {
isEq,
},
Hash {
hash: hashSet,
},
]
implements [
Eq {
isEq,
},
Hash {
hash: hashSet,
},
]
isEq : Set k, Set k -> Bool where k implements Hash & Eq
isEq = \xs, ys ->
@ -59,6 +63,13 @@ hashSet = \hasher, @Set inner -> Hash.hash hasher inner
empty : {} -> Set k where k implements Hash & Eq
empty = \{} -> @Set (Dict.empty {})
## Return a dictionary with space allocated for a number of entries. This
## may provide a performance optimization if you know how many entries will be
## inserted.
withCapacity : Nat -> Set k where k implements Hash & Eq
withCapacity = \cap ->
@Set (Dict.withCapacity cap)
## Creates a new `Set` with a single value.
## ```
## singleItemSet = Set.single "Apple"
@ -115,10 +126,32 @@ expect
##
## expect countValues == 3
## ```
len : Set k -> Nat where k implements Hash & Eq
len : Set * -> Nat
len = \@Set dict ->
Dict.len dict
## Returns the max number of elements the set can hold before requiring a rehash.
## ```
## foodSet =
## Set.empty {}
## |> Set.insert "apple"
##
## capacityOfSet = Set.capacity foodSet
## ```
capacity : Set * -> Nat
capacity = \@Set dict ->
Dict.capacity dict
## Check if the set is empty.
## ```
## Set.isEmpty (Set.empty {} |> Set.insert 42)
##
## Set.isEmpty (Set.empty {})
## ```
isEmpty : Set * -> Bool
isEmpty = \@Set dict ->
Dict.isEmpty dict
# Inserting a duplicate key has no effect on length.
expect
actual =
@ -261,6 +294,28 @@ walk : Set k, state, (state, k -> state) -> state where k implements Hash & Eq
walk = \@Set dict, state, step ->
Dict.walk dict state (\s, k, _ -> step s k)
## Convert each value in the set to something new, by calling a conversion
## function on each of them which receives the old value. Then return a
## new set containing the converted values.
map : Set a, (a -> b) -> Set b where a implements Hash & Eq, b implements Hash & Eq
map = \set, transform ->
init = withCapacity (capacity set)
walk set init \answer, k ->
insert answer (transform k)
## Like [Set.map], except the transformation function wraps the return value
## in a set. At the end, all the sets get joined together
## (using [Set.union]) into one set.
##
## You may know a similar function named `concatMap` in other languages.
joinMap : Set a, (a -> Set b) -> Set b where a implements Hash & Eq, b implements Hash & Eq
joinMap = \set, transform ->
init = withCapacity (capacity set) # Might be a pessimization
walk set init \answer, k ->
union answer (transform k)
## Iterate through the values of a given `Set` and build a value, can stop
## iterating part way through the collection.
## ```

View file

@ -103,8 +103,8 @@ interface Str
startsWith,
endsWith,
trim,
trimLeft,
trimRight,
trimStart,
trimEnd,
toDec,
toF64,
toF32,
@ -448,15 +448,15 @@ trim : Str -> Str
## Return the [Str] with all whitespace removed from the beginning.
## ```
## expect Str.trimLeft " Hello \n\n" == "Hello \n\n"
## expect Str.trimStart " Hello \n\n" == "Hello \n\n"
## ```
trimLeft : Str -> Str
trimStart : Str -> Str
## Return the [Str] with all whitespace removed from the end.
## ```
## expect Str.trimRight " Hello \n\n" == " Hello"
## expect Str.trimEnd " Hello \n\n" == " Hello"
## ```
trimRight : Str -> Str
trimEnd : Str -> Str
## Encode a [Str] to a [Dec]. A [Dec] value is a 128-bit decimal
## [fixed-point number](https://en.wikipedia.org/wiki/Fixed-point_arithmetic).
@ -638,12 +638,13 @@ countUtf8Bytes : Str -> Nat
substringUnsafe : Str, Nat, Nat -> Str
## Returns the given [Str] with each occurrence of a substring replaced.
## Returns [Err NotFound] if the substring is not found.
## If the substring is not found, returns the original string.
##
## ```
## expect Str.replaceEach "foo/bar/baz" "/" "_" == Ok "foo_bar_baz"
## expect Str.replaceEach "not here" "/" "_" == Err NotFound
## expect Str.replaceEach "foo/bar/baz" "/" "_" == "foo_bar_baz"
## expect Str.replaceEach "not here" "/" "_" == "not here"
## ```
replaceEach : Str, Str, Str -> Result Str [NotFound]
replaceEach : Str, Str, Str -> Str
replaceEach = \haystack, needle, flower ->
when splitFirst haystack needle is
Ok { before, after } ->
@ -653,9 +654,8 @@ replaceEach = \haystack, needle, flower ->
|> Str.concat before
|> Str.concat flower
|> replaceEachHelp after needle flower
|> Ok
Err err -> Err err
Err NotFound -> haystack
replaceEachHelp : Str, Str, Str, Str -> Str
replaceEachHelp = \buf, haystack, needle, flower ->
@ -668,39 +668,44 @@ replaceEachHelp = \buf, haystack, needle, flower ->
Err NotFound -> Str.concat buf haystack
expect Str.replaceEach "abXdeXghi" "X" "_" == Ok "ab_de_ghi"
expect Str.replaceEach "abXdeXghi" "X" "_" == "ab_de_ghi"
expect Str.replaceEach "abcdefg" "nothing" "_" == "abcdefg"
## Returns the given [Str] with the first occurrence of a substring replaced.
## Returns [Err NotFound] if the substring is not found.
## If the substring is not found, returns the original string.
##
## ```
## expect Str.replaceFirst "foo/bar/baz" "/" "_" == Ok "foo_bar/baz"
## expect Str.replaceFirst "no slashes here" "/" "_" == Err NotFound
## expect Str.replaceFirst "foo/bar/baz" "/" "_" == "foo_bar/baz"
## expect Str.replaceFirst "no slashes here" "/" "_" == "no slashes here"
## ```
replaceFirst : Str, Str, Str -> Result Str [NotFound]
replaceFirst : Str, Str, Str -> Str
replaceFirst = \haystack, needle, flower ->
when splitFirst haystack needle is
Ok { before, after } ->
Ok "\(before)\(flower)\(after)"
"\(before)\(flower)\(after)"
Err err -> Err err
Err NotFound -> haystack
expect Str.replaceFirst "abXdeXghi" "X" "_" == Ok "ab_deXghi"
expect Str.replaceFirst "abXdeXghi" "X" "_" == "ab_deXghi"
expect Str.replaceFirst "abcdefg" "nothing" "_" == "abcdefg"
## Returns the given [Str] with the last occurrence of a substring replaced.
## Returns [Err NotFound] if the substring is not found.
## If the substring is not found, returns the original string.
##
## ```
## expect Str.replaceLast "foo/bar/baz" "/" "_" == Ok "foo/bar_baz"
## expect Str.replaceLast "no slashes here" "/" "_" == Err NotFound
## expect Str.replaceLast "foo/bar/baz" "/" "_" == "foo/bar_baz"
## expect Str.replaceLast "no slashes here" "/" "_" == "no slashes here"
## ```
replaceLast : Str, Str, Str -> Result Str [NotFound]
replaceLast : Str, Str, Str -> Str
replaceLast = \haystack, needle, flower ->
when splitLast haystack needle is
Ok { before, after } ->
Ok "\(before)\(flower)\(after)"
"\(before)\(flower)\(after)"
Err err -> Err err
Err NotFound -> haystack
expect Str.replaceLast "abXdeXghi" "X" "_" == Ok "abXde_ghi"
expect Str.replaceLast "abXdeXghi" "X" "_" == "abXde_ghi"
expect Str.replaceLast "abcdefg" "nothing" "_" == "abcdefg"
## Returns the given [Str] before the first occurrence of a [delimiter](https://www.computerhope.com/jargon/d/delimite.htm), as well
## as the rest of the string after that occurrence.

View file

@ -44,49 +44,49 @@ interface TotallyNotJson
## An opaque type with the `EncoderFormatting` and
## `DecoderFormatting` abilities.
Json := { fieldNameMapping : FieldNameMapping }
implements [
EncoderFormatting {
u8: encodeU8,
u16: encodeU16,
u32: encodeU32,
u64: encodeU64,
u128: encodeU128,
i8: encodeI8,
i16: encodeI16,
i32: encodeI32,
i64: encodeI64,
i128: encodeI128,
f32: encodeF32,
f64: encodeF64,
dec: encodeDec,
bool: encodeBool,
string: encodeString,
list: encodeList,
record: encodeRecord,
tuple: encodeTuple,
tag: encodeTag,
},
DecoderFormatting {
u8: decodeU8,
u16: decodeU16,
u32: decodeU32,
u64: decodeU64,
u128: decodeU128,
i8: decodeI8,
i16: decodeI16,
i32: decodeI32,
i64: decodeI64,
i128: decodeI128,
f32: decodeF32,
f64: decodeF64,
dec: decodeDec,
bool: decodeBool,
string: decodeString,
list: decodeList,
record: decodeRecord,
tuple: decodeTuple,
},
]
implements [
EncoderFormatting {
u8: encodeU8,
u16: encodeU16,
u32: encodeU32,
u64: encodeU64,
u128: encodeU128,
i8: encodeI8,
i16: encodeI16,
i32: encodeI32,
i64: encodeI64,
i128: encodeI128,
f32: encodeF32,
f64: encodeF64,
dec: encodeDec,
bool: encodeBool,
string: encodeString,
list: encodeList,
record: encodeRecord,
tuple: encodeTuple,
tag: encodeTag,
},
DecoderFormatting {
u8: decodeU8,
u16: decodeU16,
u32: decodeU32,
u64: decodeU64,
u128: decodeU128,
i8: decodeI8,
i16: decodeI16,
i32: decodeI32,
i64: decodeI64,
i128: decodeI128,
f32: decodeF32,
f64: decodeF64,
dec: decodeDec,
bool: decodeBool,
string: decodeString,
list: decodeList,
record: decodeRecord,
tuple: decodeTuple,
},
]
## Returns a JSON `Encoder` and `Decoder`
json = @Json { fieldNameMapping: Default }

View file

@ -339,8 +339,8 @@ pub const STR_TO_UTF8: &str = "roc_builtins.str.to_utf8";
pub const STR_FROM_UTF8_RANGE: &str = "roc_builtins.str.from_utf8_range";
pub const STR_REPEAT: &str = "roc_builtins.str.repeat";
pub const STR_TRIM: &str = "roc_builtins.str.trim";
pub const STR_TRIM_LEFT: &str = "roc_builtins.str.trim_left";
pub const STR_TRIM_RIGHT: &str = "roc_builtins.str.trim_right";
pub const STR_TRIM_START: &str = "roc_builtins.str.trim_start";
pub const STR_TRIM_END: &str = "roc_builtins.str.trim_end";
pub const STR_GET_UNSAFE: &str = "roc_builtins.str.get_unsafe";
pub const STR_RESERVE: &str = "roc_builtins.str.reserve";
pub const STR_APPEND_SCALAR: &str = "roc_builtins.str.append_scalar";
@ -393,8 +393,10 @@ pub const UTILS_TEST_PANIC: &str = "roc_builtins.utils.test_panic";
pub const UTILS_ALLOCATE_WITH_REFCOUNT: &str = "roc_builtins.utils.allocate_with_refcount";
pub const UTILS_INCREF_RC_PTR: &str = "roc_builtins.utils.incref_rc_ptr";
pub const UTILS_DECREF_RC_PTR: &str = "roc_builtins.utils.decref_rc_ptr";
pub const UTILS_FREE_RC_PTR: &str = "roc_builtins.utils.free_rc_ptr";
pub const UTILS_INCREF_DATA_PTR: &str = "roc_builtins.utils.incref_data_ptr";
pub const UTILS_DECREF_DATA_PTR: &str = "roc_builtins.utils.decref_data_ptr";
pub const UTILS_FREE_DATA_PTR: &str = "roc_builtins.utils.free_data_ptr";
pub const UTILS_IS_UNIQUE: &str = "roc_builtins.utils.is_unique";
pub const UTILS_DECREF_CHECK_NULL: &str = "roc_builtins.utils.decref_check_null";
pub const UTILS_DICT_PSEUDO_SEED: &str = "roc_builtins.utils.dict_pseudo_seed";

View file

@ -469,8 +469,7 @@ impl IAbilitiesStore<Resolved> {
debug_assert!(
old_specialization.is_none(),
"Existing resolution: {:?}",
old_specialization
"Existing resolution: {old_specialization:?}"
);
}

View file

@ -847,29 +847,17 @@ fn can_annotation_help(
let alias = scope.lookup_alias(symbol).unwrap();
local_aliases.insert(symbol, alias.clone());
if vars.is_empty() && env.home == symbol.module_id() {
let actual_var = var_store.fresh();
introduced_variables.insert_host_exposed_alias(symbol, actual_var);
Type::HostExposedAlias {
name: symbol,
type_arguments: vars,
lambda_set_variables: alias.lambda_set_variables.clone(),
actual: Box::new(alias.typ.clone()),
actual_var,
}
} else {
Type::Alias {
symbol,
type_arguments: vars.into_iter().map(OptAbleType::unbound).collect(),
lambda_set_variables: alias.lambda_set_variables.clone(),
infer_ext_in_output_types: alias
.infer_ext_in_output_variables
.iter()
.map(|v| Type::Variable(*v))
.collect(),
actual: Box::new(alias.typ.clone()),
kind: alias.kind,
}
Type::Alias {
symbol,
type_arguments: vars.into_iter().map(OptAbleType::unbound).collect(),
lambda_set_variables: alias.lambda_set_variables.clone(),
infer_ext_in_output_types: alias
.infer_ext_in_output_variables
.iter()
.map(|v| Type::Variable(*v))
.collect(),
actual: Box::new(alias.typ.clone()),
kind: alias.kind,
}
}

View file

@ -85,13 +85,19 @@ macro_rules! map_symbol_to_lowlevel_and_arity {
// these are used internally and not tied to a symbol
LowLevel::Hash => unimplemented!(),
LowLevel::PtrCast => unimplemented!(),
LowLevel::PtrWrite => unimplemented!(),
LowLevel::PtrStore => unimplemented!(),
LowLevel::PtrLoad => unimplemented!(),
LowLevel::PtrClearTagId => unimplemented!(),
LowLevel::RefCountIncRcPtr => unimplemented!(),
LowLevel::RefCountDecRcPtr=> unimplemented!(),
LowLevel::RefCountIncDataPtr => unimplemented!(),
LowLevel::RefCountDecDataPtr=> unimplemented!(),
LowLevel::RefCountIsUnique => unimplemented!(),
LowLevel::SetJmp => unimplemented!(),
LowLevel::LongJmp => unimplemented!(),
LowLevel::SetLongJmpBuffer => unimplemented!(),
// these are not implemented, not sure why
LowLevel::StrFromInt => unimplemented!(),
LowLevel::StrFromFloat => unimplemented!(),
@ -118,8 +124,8 @@ map_symbol_to_lowlevel_and_arity! {
StrToUtf8; STR_TO_UTF8; 1,
StrRepeat; STR_REPEAT; 2,
StrTrim; STR_TRIM; 1,
StrTrimLeft; STR_TRIM_LEFT; 1,
StrTrimRight; STR_TRIM_RIGHT; 1,
StrTrimStart; STR_TRIM_START; 1,
StrTrimEnd; STR_TRIM_END; 1,
StrToScalars; STR_TO_SCALARS; 1,
StrGetUnsafe; STR_GET_UNSAFE; 2,
StrSubstringUnsafe; STR_SUBSTRING_UNSAFE; 3,

View file

@ -176,7 +176,7 @@ impl Constraints {
let mut buf = String::new();
writeln!(buf, "Constraints statistics for module {:?}:", module_id)?;
writeln!(buf, "Constraints statistics for module {module_id:?}:")?;
writeln!(buf, " constraints length: {}:", self.constraints.len())?;
writeln!(
@ -833,16 +833,16 @@ impl std::fmt::Debug for Constraint {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Eq(Eq(arg0, arg1, arg2, arg3)) => {
write!(f, "Eq({:?}, {:?}, {:?}, {:?})", arg0, arg1, arg2, arg3)
write!(f, "Eq({arg0:?}, {arg1:?}, {arg2:?}, {arg3:?})")
}
Self::Store(arg0, arg1, arg2, arg3) => {
write!(f, "Store({:?}, {:?}, {:?}, {:?})", arg0, arg1, arg2, arg3)
write!(f, "Store({arg0:?}, {arg1:?}, {arg2:?}, {arg3:?})")
}
Self::Lookup(arg0, arg1, arg2) => {
write!(f, "Lookup({:?}, {:?}, {:?})", arg0, arg1, arg2)
write!(f, "Lookup({arg0:?}, {arg1:?}, {arg2:?})")
}
Self::Pattern(arg0, arg1, arg2, arg3) => {
write!(f, "Pattern({:?}, {:?}, {:?}, {:?})", arg0, arg1, arg2, arg3)
write!(f, "Pattern({arg0:?}, {arg1:?}, {arg2:?}, {arg3:?})")
}
Self::True => write!(f, "True"),
Self::SaveTheEnvironment => write!(f, "SaveTheEnvironment"),
@ -851,27 +851,19 @@ impl std::fmt::Debug for Constraint {
Self::IsOpenType(arg0) => f.debug_tuple("IsOpenType").field(arg0).finish(),
Self::IncludesTag(arg0) => f.debug_tuple("IncludesTag").field(arg0).finish(),
Self::PatternPresence(arg0, arg1, arg2, arg3) => {
write!(
f,
"PatternPresence({:?}, {:?}, {:?}, {:?})",
arg0, arg1, arg2, arg3
)
write!(f, "PatternPresence({arg0:?}, {arg1:?}, {arg2:?}, {arg3:?})")
}
Self::Exhaustive(arg0, arg1, arg2, arg3) => {
write!(
f,
"Exhaustive({:?}, {:?}, {:?}, {:?})",
arg0, arg1, arg2, arg3
)
write!(f, "Exhaustive({arg0:?}, {arg1:?}, {arg2:?}, {arg3:?})")
}
Self::Resolve(arg0) => {
write!(f, "Resolve({:?})", arg0)
write!(f, "Resolve({arg0:?})")
}
Self::CheckCycle(arg0, arg1) => {
write!(f, "CheckCycle({:?}, {:?})", arg0, arg1)
write!(f, "CheckCycle({arg0:?}, {arg1:?})")
}
Self::IngestedFile(arg0, arg1, arg2) => {
write!(f, "IngestedFile({:?}, {:?}, {:?})", arg0, arg1, arg2)
write!(f, "IngestedFile({arg0:?}, {arg1:?}, {arg2:?})")
}
}
}

View file

@ -24,7 +24,7 @@ trait CopyEnv {
if descriptor.copy.into_variable().is_some() {
descriptor.copy = OptVariable::NONE;
} else {
debug_assert!(false, "{:?} marked as copied but it wasn't", var);
debug_assert!(false, "{var:?} marked as copied but it wasn't");
}
})
}
@ -1148,6 +1148,7 @@ fn deep_copy_type_vars<C: CopyEnv>(
})
})
}
ErasedLambda => ErasedLambda,
RangedNumber(range) => {
perform_clone!(RangedNumber(range))
@ -1320,7 +1321,7 @@ mod test {
FlexVar(Some(name)) => {
assert_eq!(subs[*name].as_str(), "a");
}
it => panic!("{:?}", it),
it => panic!("{it:?}"),
}
assert_eq!(var, variant_var);
assert!(matches!(
@ -1337,7 +1338,7 @@ mod test {
FlexVar(Some(name)) => {
assert_eq!(subs[*name].as_str(), "b");
}
it => panic!("{:?}", it),
it => panic!("{it:?}"),
}
match arg.value {
@ -1355,10 +1356,10 @@ mod test {
assert_eq!(name.0.as_str(), "G");
assert_eq!(arguments.len(), 0);
}
e => panic!("{:?}", e),
e => panic!("{e:?}"),
}
}
e => panic!("{:?}", e),
e => panic!("{e:?}"),
}
}
@ -1403,7 +1404,7 @@ mod test {
FlexVar(Some(name)) => {
assert_eq!(target[*name].as_str(), "a");
}
it => panic!("{:?}", it),
it => panic!("{it:?}"),
}
assert_eq!(var, variant_var);
assert!(matches!(
@ -1418,7 +1419,7 @@ mod test {
FlexVar(Some(name)) => {
assert_eq!(target[*name].as_str(), "b");
}
it => panic!("{:?}", it),
it => panic!("{it:?}"),
}
match arg.value {
@ -1436,10 +1437,10 @@ mod test {
assert_eq!(name.0.as_str(), "G");
assert_eq!(arguments.len(), 0);
}
e => panic!("{:?}", e),
e => panic!("{e:?}"),
}
}
e => panic!("{:?}", e),
e => panic!("{e:?}"),
}
}

View file

@ -88,6 +88,23 @@ pub struct Annotation {
pub region: Region,
}
impl Annotation {
fn freshen(mut self, var_store: &mut VarStore) -> Self {
let mut substitutions = MutMap::default();
for v in self.introduced_variables.lambda_sets.iter_mut() {
let new = var_store.fresh();
substitutions.insert(*v, new);
*v = new;
}
self.signature.substitute_variables(&substitutions);
self
}
}
#[derive(Debug)]
pub(crate) struct CanDefs {
defs: Vec<Option<Def>>,
@ -1069,8 +1086,7 @@ fn canonicalize_value_defs<'a>(
debug_assert_eq!(env.home, s.module_id());
debug_assert!(
!symbol_to_index.iter().any(|(id, _)| *id == s.ident_id()),
"{:?}",
s
"{s:?}"
);
symbol_to_index.push((s.ident_id(), def_index as u32));
@ -1639,6 +1655,14 @@ pub(crate) fn sort_can_defs_new(
}
};
let host_annotation = if exposed_symbols.contains(&symbol) {
def.annotation
.clone()
.map(|a| (var_store.fresh(), a.freshen(var_store)))
} else {
None
};
if is_initial && !exposed_symbols.contains(&symbol) {
env.problem(Problem::DefsOnlyUsedInRecursion(1, def.region()));
}
@ -1650,6 +1674,7 @@ pub(crate) fn sort_can_defs_new(
Loc::at(def.loc_expr.region, closure_data),
def.expr_var,
def.annotation,
host_annotation,
specializes,
);
}
@ -1659,55 +1684,80 @@ pub(crate) fn sort_can_defs_new(
def.loc_expr,
def.expr_var,
def.annotation,
host_annotation,
specializes,
);
}
}
} else {
match def.loc_pattern.value {
Pattern::Identifier(symbol) => match def.loc_expr.value {
Closure(closure_data) => {
declarations.push_function_def(
Loc::at(def.loc_pattern.region, symbol),
Loc::at(def.loc_expr.region, closure_data),
def.expr_var,
def.annotation,
None,
);
Pattern::Identifier(symbol) => {
let host_annotation = if exposed_symbols.contains(&symbol) {
def.annotation
.clone()
.map(|a| (var_store.fresh(), a.freshen(var_store)))
} else {
None
};
match def.loc_expr.value {
Closure(closure_data) => {
declarations.push_function_def(
Loc::at(def.loc_pattern.region, symbol),
Loc::at(def.loc_expr.region, closure_data),
def.expr_var,
def.annotation,
host_annotation,
None,
);
}
_ => {
declarations.push_value_def(
Loc::at(def.loc_pattern.region, symbol),
def.loc_expr,
def.expr_var,
def.annotation,
host_annotation,
None,
);
}
}
_ => {
declarations.push_value_def(
Loc::at(def.loc_pattern.region, symbol),
def.loc_expr,
def.expr_var,
def.annotation,
None,
);
}
},
}
Pattern::AbilityMemberSpecialization {
ident: symbol,
specializes,
} => match def.loc_expr.value {
Closure(closure_data) => {
declarations.push_function_def(
Loc::at(def.loc_pattern.region, symbol),
Loc::at(def.loc_expr.region, closure_data),
def.expr_var,
def.annotation,
Some(specializes),
);
} => {
let host_annotation = if exposed_symbols.contains(&symbol) {
def.annotation
.clone()
.map(|a| (var_store.fresh(), a.freshen(var_store)))
} else {
None
};
match def.loc_expr.value {
Closure(closure_data) => {
declarations.push_function_def(
Loc::at(def.loc_pattern.region, symbol),
Loc::at(def.loc_expr.region, closure_data),
def.expr_var,
def.annotation,
host_annotation,
Some(specializes),
);
}
_ => {
declarations.push_value_def(
Loc::at(def.loc_pattern.region, symbol),
def.loc_expr,
def.expr_var,
def.annotation,
host_annotation,
Some(specializes),
);
}
}
_ => {
declarations.push_value_def(
Loc::at(def.loc_pattern.region, symbol),
def.loc_expr,
def.expr_var,
def.annotation,
Some(specializes),
);
}
},
}
_ => {
declarations.push_destructure_def(
def.loc_pattern,
@ -1750,6 +1800,14 @@ pub(crate) fn sort_can_defs_new(
Some(r) => Some(Region::span_across(&r, &def.region())),
};
let host_annotation = if exposed_symbols.contains(&symbol) {
def.annotation
.clone()
.map(|a| (var_store.fresh(), a.freshen(var_store)))
} else {
None
};
match def.loc_expr.value {
Closure(closure_data) => {
declarations.push_recursive_def(
@ -1757,6 +1815,7 @@ pub(crate) fn sort_can_defs_new(
Loc::at(def.loc_expr.region, closure_data),
def.expr_var,
def.annotation,
host_annotation,
specializes,
);
}
@ -1766,6 +1825,7 @@ pub(crate) fn sort_can_defs_new(
def.loc_expr,
def.expr_var,
def.annotation,
host_annotation,
specializes,
);
}
@ -1838,7 +1898,7 @@ pub(crate) fn sort_can_defs(
);
let declaration = if def_ordering.references.get_row_col(index, index) {
debug_assert!(!is_specialization, "Self-recursive specializations can only be determined during solving - but it was determined for {:?} now, that's a bug!", def);
debug_assert!(!is_specialization, "Self-recursive specializations can only be determined during solving - but it was determined for {def:?} now, that's a bug!");
if is_initial
&& !def
@ -2272,12 +2332,18 @@ fn canonicalize_pending_body<'a>(
opt_loc_annotation: Option<Loc<crate::annotation::Annotation>>,
) -> DefOutput {
let mut loc_value = &loc_expr.value;
while let ast::Expr::ParensAround(value) = loc_value {
loc_value = value;
}
// We treat closure definitions `foo = \a, b -> ...` differently from other body expressions,
// because they need more bookkeeping (for tail calls, closure captures, etc.)
//
// Only defs of the form `foo = ...` can be closure declarations or self tail calls.
let (loc_can_expr, def_references) = {
match (&loc_can_pattern.value, &loc_expr.value) {
match (&loc_can_pattern.value, &loc_value) {
(
Pattern::Identifier(defined_symbol)
| Pattern::AbilityMemberSpecialization {

View file

@ -222,16 +222,16 @@ pub(crate) fn synthesize_member_impl<'a>(
ability_member: Symbol,
) -> (Symbol, Loc<Pattern>, &'a Loc<ast::Expr<'a>>) {
// @Opaq
let at_opaque = env.arena.alloc_str(&format!("@{}", opaque_name));
let at_opaque = env.arena.alloc_str(&format!("@{opaque_name}"));
let (impl_name, def_body): (String, ast::Expr<'a>) = match ability_member {
Symbol::ENCODE_TO_ENCODER => (
format!("#{}_toEncoder", opaque_name),
format!("#{opaque_name}_toEncoder"),
to_encoder(env, at_opaque),
),
Symbol::DECODE_DECODER => (format!("#{}_decoder", opaque_name), decoder(env, at_opaque)),
Symbol::HASH_HASH => (format!("#{}_hash", opaque_name), hash(env, at_opaque)),
Symbol::BOOL_IS_EQ => (format!("#{}_isEq", opaque_name), is_eq(env, at_opaque)),
Symbol::DECODE_DECODER => (format!("#{opaque_name}_decoder"), decoder(env, at_opaque)),
Symbol::HASH_HASH => (format!("#{opaque_name}_hash"), hash(env, at_opaque)),
Symbol::BOOL_IS_EQ => (format!("#{opaque_name}_isEq"), is_eq(env, at_opaque)),
other => internal_error!("{:?} is not a derivable ability member!", other),
};

View file

@ -1362,7 +1362,7 @@ pub fn build_host_exposed_def(
match typ.shallow_structural_dealias() {
Type::Function(args, _, _) => {
for i in 0..args.len() {
let name = format!("closure_arg_{}_{}", ident, i);
let name = format!("closure_arg_{ident}_{i}");
let arg_symbol = {
let ident = name.clone().into();
@ -1381,7 +1381,7 @@ pub fn build_host_exposed_def(
linked_symbol_arguments.push((arg_var, Expr::Var(arg_symbol, arg_var)));
}
let foreign_symbol_name = format!("roc_fx_{}", ident);
let foreign_symbol_name = format!("roc_fx_{ident}");
let low_level_call = Expr::ForeignCall {
foreign_symbol: foreign_symbol_name.into(),
args: linked_symbol_arguments,
@ -1389,7 +1389,7 @@ pub fn build_host_exposed_def(
};
let effect_closure_symbol = {
let name = format!("effect_closure_{}", ident);
let name = format!("effect_closure_{ident}");
let ident = name.into();
scope.introduce(ident, Region::zero()).unwrap()
@ -1435,7 +1435,7 @@ pub fn build_host_exposed_def(
_ => {
// not a function
let foreign_symbol_name = format!("roc_fx_{}", ident);
let foreign_symbol_name = format!("roc_fx_{ident}");
let low_level_call = Expr::ForeignCall {
foreign_symbol: foreign_symbol_name.into(),
args: linked_symbol_arguments,
@ -1443,7 +1443,7 @@ pub fn build_host_exposed_def(
};
let effect_closure_symbol = {
let name = format!("effect_closure_{}", ident);
let name = format!("effect_closure_{ident}");
let ident = name.into();
scope.introduce(ident, Region::zero()).unwrap()

View file

@ -67,8 +67,7 @@ impl<'a> Env<'a> {
) -> Result<Symbol, RuntimeError> {
debug_assert!(
!module_name_str.is_empty(),
"Called env.qualified_lookup with an unqualified ident: {:?}",
ident
"Called env.qualified_lookup with an unqualified ident: {ident:?}"
);
let module_name = ModuleName::from(module_name_str);

View file

@ -140,6 +140,7 @@ fn index_var(
| Content::FlexAbleVar(_, _)
| Content::RigidAbleVar(_, _)
| Content::LambdaSet(_)
| Content::ErasedLambda
| Content::RangedNumber(..) => return Err(TypeError),
Content::Error => return Err(TypeError),
Content::RecursionVar {

View file

@ -18,7 +18,7 @@ use roc_module::called_via::CalledVia;
use roc_module::ident::{ForeignSymbol, Lowercase, TagName};
use roc_module::low_level::LowLevel;
use roc_module::symbol::Symbol;
use roc_parse::ast::{self, Defs, StrLiteral};
use roc_parse::ast::{self, Defs, PrecedenceConflict, StrLiteral};
use roc_parse::ident::Accessor;
use roc_parse::pattern::PatternType::*;
use roc_problem::can::{PrecedenceProblem, Problem, RuntimeError};
@ -1403,14 +1403,13 @@ pub fn canonicalize_expr<'a>(
(answer, Output::default())
}
&ast::Expr::ParensAround(sub_expr) => {
let (loc_expr, output) = canonicalize_expr(env, var_store, scope, region, sub_expr);
(loc_expr.value, output)
}
// Below this point, we shouln't see any of these nodes anymore because
// operator desugaring should have removed them!
bad_expr @ ast::Expr::ParensAround(_) => {
internal_error!(
"A ParensAround did not get removed during operator desugaring somehow: {:#?}",
bad_expr
);
}
bad_expr @ ast::Expr::SpaceBefore(_, _) => {
internal_error!(
"A SpaceBefore did not get removed during operator desugaring somehow: {:#?}",
@ -2377,11 +2376,115 @@ fn flatten_str_literal<'a>(
}
}
/// Comments, newlines, and nested interpolation are disallowed inside interpolation
pub fn is_valid_interpolation(expr: &ast::Expr<'_>) -> bool {
match expr {
ast::Expr::Var { .. } => true,
ast::Expr::RecordAccess(sub_expr, _) => is_valid_interpolation(sub_expr),
_ => false,
// These definitely contain neither comments nor newlines, so they are valid
ast::Expr::Var { .. }
| ast::Expr::SingleQuote(_)
| ast::Expr::Str(StrLiteral::PlainLine(_))
| ast::Expr::Float(_)
| ast::Expr::Num(_)
| ast::Expr::NonBase10Int { .. }
| ast::Expr::AccessorFunction(_)
| ast::Expr::Crash
| ast::Expr::Underscore(_)
| ast::Expr::MalformedIdent(_, _)
| ast::Expr::Tag(_)
| ast::Expr::OpaqueRef(_)
| ast::Expr::MalformedClosure => true,
// Newlines are disallowed inside interpolation, and these all require newlines
ast::Expr::Dbg(_, _)
| ast::Expr::Defs(_, _)
| ast::Expr::Expect(_, _)
| ast::Expr::When(_, _)
| ast::Expr::Backpassing(_, _, _)
| ast::Expr::IngestedFile(_, _)
| ast::Expr::SpaceBefore(_, _)
| ast::Expr::Str(StrLiteral::Block(_))
| ast::Expr::SpaceAfter(_, _) => false,
// These can contain subexpressions, so we need to recursively check those
ast::Expr::Str(StrLiteral::Line(segments)) => {
segments.iter().all(|segment| match segment {
ast::StrSegment::EscapedChar(_)
| ast::StrSegment::Unicode(_)
| ast::StrSegment::Plaintext(_) => true,
// Disallow nested interpolation. Alternatively, we could allow it but require
// a comment above it apologizing to the next person who has to read the code.
ast::StrSegment::Interpolated(_) => false,
})
}
ast::Expr::Record(fields) => fields.iter().all(|loc_field| match loc_field.value {
ast::AssignedField::RequiredValue(_label, loc_comments, loc_val)
| ast::AssignedField::OptionalValue(_label, loc_comments, loc_val) => {
loc_comments.is_empty() && is_valid_interpolation(&loc_val.value)
}
ast::AssignedField::Malformed(_) | ast::AssignedField::LabelOnly(_) => true,
ast::AssignedField::SpaceBefore(_, _) | ast::AssignedField::SpaceAfter(_, _) => false,
}),
ast::Expr::Tuple(fields) => fields
.iter()
.all(|loc_field| is_valid_interpolation(&loc_field.value)),
ast::Expr::MultipleRecordBuilders(loc_expr)
| ast::Expr::UnappliedRecordBuilder(loc_expr)
| ast::Expr::PrecedenceConflict(PrecedenceConflict { expr: loc_expr, .. })
| ast::Expr::UnaryOp(loc_expr, _)
| ast::Expr::Closure(_, loc_expr) => is_valid_interpolation(&loc_expr.value),
ast::Expr::TupleAccess(sub_expr, _)
| ast::Expr::ParensAround(sub_expr)
| ast::Expr::RecordAccess(sub_expr, _) => is_valid_interpolation(sub_expr),
ast::Expr::Apply(loc_expr, args, _called_via) => {
is_valid_interpolation(&loc_expr.value)
&& args
.iter()
.all(|loc_arg| is_valid_interpolation(&loc_arg.value))
}
ast::Expr::BinOps(loc_exprs, loc_expr) => {
is_valid_interpolation(&loc_expr.value)
&& loc_exprs
.iter()
.all(|(loc_expr, _binop)| is_valid_interpolation(&loc_expr.value))
}
ast::Expr::If(branches, final_branch) => {
is_valid_interpolation(&final_branch.value)
&& branches.iter().all(|(loc_before, loc_after)| {
is_valid_interpolation(&loc_before.value)
&& is_valid_interpolation(&loc_after.value)
})
}
ast::Expr::List(elems) => elems
.iter()
.all(|loc_expr| is_valid_interpolation(&loc_expr.value)),
ast::Expr::RecordUpdate { update, fields } => {
is_valid_interpolation(&update.value)
&& fields.iter().all(|loc_field| match loc_field.value {
ast::AssignedField::RequiredValue(_label, loc_comments, loc_val)
| ast::AssignedField::OptionalValue(_label, loc_comments, loc_val) => {
loc_comments.is_empty() && is_valid_interpolation(&loc_val.value)
}
ast::AssignedField::Malformed(_) | ast::AssignedField::LabelOnly(_) => true,
ast::AssignedField::SpaceBefore(_, _)
| ast::AssignedField::SpaceAfter(_, _) => false,
})
}
ast::Expr::RecordBuilder(fields) => fields.iter().all(|loc_field| match loc_field.value {
ast::RecordBuilderField::Value(_label, comments, loc_expr) => {
comments.is_empty() && is_valid_interpolation(&loc_expr.value)
}
ast::RecordBuilderField::ApplyValue(
_label,
comments_before,
comments_after,
loc_expr,
) => {
comments_before.is_empty()
&& comments_after.is_empty()
&& is_valid_interpolation(&loc_expr.value)
}
ast::RecordBuilderField::Malformed(_) | ast::RecordBuilderField::LabelOnly(_) => true,
ast::RecordBuilderField::SpaceBefore(_, _)
| ast::RecordBuilderField::SpaceAfter(_, _) => false,
}),
}
}
@ -2536,6 +2639,8 @@ pub struct Declarations {
// used for ability member specializatons.
pub specializes: VecMap<usize, Symbol>,
pub host_exposed_annotations: VecMap<usize, (Variable, crate::def::Annotation)>,
pub function_bodies: Vec<Loc<FunctionDef>>,
pub expressions: Vec<Loc<Expr>>,
pub destructs: Vec<DestructureDef>,
@ -2557,6 +2662,7 @@ impl Declarations {
variables: Vec::with_capacity(capacity),
symbols: Vec::with_capacity(capacity),
annotations: Vec::with_capacity(capacity),
host_exposed_annotations: VecMap::new(),
function_bodies: Vec::with_capacity(capacity),
expressions: Vec::with_capacity(capacity),
specializes: VecMap::default(), // number of specializations is probably low
@ -2587,6 +2693,7 @@ impl Declarations {
loc_closure_data: Loc<ClosureData>,
expr_var: Variable,
annotation: Option<Annotation>,
host_annotation: Option<(Variable, Annotation)>,
specializes: Option<Symbol>,
) -> usize {
let index = self.declarations.len();
@ -2609,6 +2716,11 @@ impl Declarations {
Recursive::TailRecursive => DeclarationTag::TailRecursive(function_def_index),
};
if let Some(annotation) = host_annotation {
self.host_exposed_annotations
.insert(self.declarations.len(), annotation);
}
self.declarations.push(tag);
self.variables.push(expr_var);
self.symbols.push(symbol);
@ -2629,6 +2741,7 @@ impl Declarations {
loc_closure_data: Loc<ClosureData>,
expr_var: Variable,
annotation: Option<Annotation>,
host_annotation: Option<(Variable, Annotation)>,
specializes: Option<Symbol>,
) -> usize {
let index = self.declarations.len();
@ -2644,6 +2757,11 @@ impl Declarations {
let function_def_index = Index::push_new(&mut self.function_bodies, loc_function_def);
if let Some(annotation) = host_annotation {
self.host_exposed_annotations
.insert(self.declarations.len(), annotation);
}
self.declarations
.push(DeclarationTag::Function(function_def_index));
self.variables.push(expr_var);
@ -2701,10 +2819,16 @@ impl Declarations {
loc_expr: Loc<Expr>,
expr_var: Variable,
annotation: Option<Annotation>,
host_annotation: Option<(Variable, Annotation)>,
specializes: Option<Symbol>,
) -> usize {
let index = self.declarations.len();
if let Some(annotation) = host_annotation {
self.host_exposed_annotations
.insert(self.declarations.len(), annotation);
}
self.declarations.push(DeclarationTag::Value);
self.variables.push(expr_var);
self.symbols.push(symbol);
@ -2759,6 +2883,7 @@ impl Declarations {
def.expr_var,
def.annotation,
None,
None,
);
}
@ -2769,6 +2894,7 @@ impl Declarations {
def.expr_var,
def.annotation,
None,
None,
);
}
},
@ -2779,6 +2905,7 @@ impl Declarations {
def.expr_var,
def.annotation,
None,
None,
);
}
},

View file

@ -347,9 +347,7 @@ pub fn canonicalize_module_defs<'a>(
// the symbol should already be added to the scope when this module is canonicalized
debug_assert!(
scope.contains_alias(symbol) || scope.abilities_store.is_ability(symbol),
"The {:?} is not a type alias or ability known in {:?}",
symbol,
home
"The {symbol:?} is not a type alias or ability known in {home:?}"
);
// but now we know this symbol by a different identifier, so we still need to add it to

View file

@ -220,8 +220,7 @@ fn from_str_radix(src: &str, radix: u32) -> Result<ParsedNumResult, IntErrorKind
assert!(
(2..=36).contains(&radix),
"from_str_radix_int: must lie in the range `[2, 36]` - found {}",
radix
"from_str_radix_int: must lie in the range `[2, 36]` - found {radix}"
);
let (opt_exact_bound, src) = parse_literal_suffix(src);

View file

@ -7,7 +7,9 @@ use roc_module::called_via::BinOp::Pizza;
use roc_module::called_via::{BinOp, CalledVia};
use roc_module::ident::ModuleName;
use roc_parse::ast::Expr::{self, *};
use roc_parse::ast::{AssignedField, Collection, RecordBuilderField, ValueDef, WhenBranch};
use roc_parse::ast::{
AssignedField, Collection, RecordBuilderField, StrLiteral, StrSegment, ValueDef, WhenBranch,
};
use roc_region::all::{Loc, Region};
// BinOp precedence logic adapted from Gluon by Markus Westerlind
@ -129,7 +131,6 @@ pub fn desugar_expr<'a>(arena: &'a Bump, loc_expr: &'a Loc<Expr<'a>>) -> &'a Loc
Float(..)
| Num(..)
| NonBase10Int { .. }
| Str(_)
| SingleQuote(_)
| AccessorFunction(_)
| Var { .. }
@ -144,6 +145,28 @@ pub fn desugar_expr<'a>(arena: &'a Bump, loc_expr: &'a Loc<Expr<'a>>) -> &'a Loc
| IngestedFile(_, _)
| Crash => loc_expr,
Str(str_literal) => match str_literal {
StrLiteral::PlainLine(_) => loc_expr,
StrLiteral::Line(segments) => {
let region = loc_expr.region;
let value = Str(StrLiteral::Line(desugar_str_segments(arena, segments)));
arena.alloc(Loc { region, value })
}
StrLiteral::Block(lines) => {
let region = loc_expr.region;
let new_lines = Vec::from_iter_in(
lines
.iter()
.map(|segments| desugar_str_segments(arena, segments)),
arena,
);
let value = Str(StrLiteral::Block(new_lines.into_bump_slice()));
arena.alloc(Loc { region, value })
}
},
TupleAccess(sub_expr, paths) => {
let region = loc_expr.region;
let loc_sub_expr = Loc {
@ -288,7 +311,7 @@ pub fn desugar_expr<'a>(arena: &'a Bump, loc_expr: &'a Loc<Expr<'a>>) -> &'a Loc
break builder_arg.closure;
}
SpaceBefore(expr, _) | SpaceAfter(expr, _) | ParensAround(expr) => {
SpaceBefore(expr, _) | SpaceAfter(expr, _) => {
current = *expr;
}
_ => break loc_arg,
@ -382,7 +405,7 @@ pub fn desugar_expr<'a>(arena: &'a Bump, loc_expr: &'a Loc<Expr<'a>>) -> &'a Loc
region: loc_expr.region,
})
}
SpaceBefore(expr, _) | SpaceAfter(expr, _) | ParensAround(expr) => {
SpaceBefore(expr, _) | SpaceAfter(expr, _) => {
// Since we've already begun canonicalization, spaces and parens
// are no longer needed and should be dropped.
desugar_expr(
@ -393,6 +416,20 @@ pub fn desugar_expr<'a>(arena: &'a Bump, loc_expr: &'a Loc<Expr<'a>>) -> &'a Loc
}),
)
}
ParensAround(expr) => {
let desugared = desugar_expr(
arena,
arena.alloc(Loc {
value: **expr,
region: loc_expr.region,
}),
);
arena.alloc(Loc {
value: ParensAround(&desugared.value),
region: loc_expr.region,
})
}
If(if_thens, final_else_branch) => {
// If does not get desugared into `when` so we can give more targeted error messages during type checking.
let desugared_final_else = &*arena.alloc(desugar_expr(arena, final_else_branch));
@ -430,6 +467,34 @@ pub fn desugar_expr<'a>(arena: &'a Bump, loc_expr: &'a Loc<Expr<'a>>) -> &'a Loc
}
}
fn desugar_str_segments<'a>(
arena: &'a Bump,
segments: &'a [StrSegment<'a>],
) -> &'a [StrSegment<'a>] {
Vec::from_iter_in(
segments.iter().map(|segment| match segment {
StrSegment::Plaintext(_) | StrSegment::Unicode(_) | StrSegment::EscapedChar(_) => {
*segment
}
StrSegment::Interpolated(loc_expr) => {
let loc_desugared = desugar_expr(
arena,
arena.alloc(Loc {
region: loc_expr.region,
value: *loc_expr.value,
}),
);
StrSegment::Interpolated(Loc {
region: loc_desugared.region,
value: arena.alloc(loc_desugared.value),
})
}
}),
arena,
)
.into_bump_slice()
}
fn desugar_field<'a>(
arena: &'a Bump,
field: &'a AssignedField<'a, Expr<'a>>,

View file

@ -531,7 +531,7 @@ pub fn canonicalize_pattern<'a>(
use std::ops::Neg;
let sign_str = if is_negative { "-" } else { "" };
let int_str = format!("{}{}", sign_str, int).into_boxed_str();
let int_str = format!("{sign_str}{int}").into_boxed_str();
let i = match int {
// Safety: this is fine because I128::MAX = |I128::MIN| - 1
IntValue::I128(n) if is_negative => {

View file

@ -37,8 +37,7 @@ pub struct CanExprOut {
pub fn can_expr_with(arena: &Bump, home: ModuleId, expr_str: &str) -> CanExprOut {
let loc_expr = roc_parse::test_helpers::parse_loc_with(arena, expr_str).unwrap_or_else(|e| {
panic!(
"can_expr_with() got a parse error when attempting to canonicalize:\n\n{:?} {:?}",
expr_str, e
"can_expr_with() got a parse error when attempting to canonicalize:\n\n{expr_str:?} {e:?}"
)
});

View file

@ -422,7 +422,7 @@ mod test_can {
let CanExprOut { problems, .. } = can_expr_with(&arena, test_home(), src);
assert_eq!(problems.len(), 2);
println!("{:#?}", problems);
println!("{problems:#?}");
assert!(problems.iter().any(|problem| matches!(
problem,
Problem::RuntimeError(RuntimeError::Shadowing { .. })

View file

@ -0,0 +1,19 @@
[package]
name = "roc_checkmate"
description = "A framework for debugging the solver."
authors.workspace = true
edition.workspace = true
license.workspace = true
version.workspace = true
[dependencies]
roc_checkmate_schema = { path = "../checkmate_schema" }
roc_module = { path = "../module" }
roc_solve_schema = { path = "../solve_schema" }
roc_types = { path = "../types" }
chrono.workspace = true
[build-dependencies]
roc_checkmate_schema = { path = "../checkmate_schema" }
serde_json.workspace = true

View file

@ -0,0 +1,5 @@
# `checkmate`
A tool to debug the solver (checker + inference + specialization engine).
See [the document](https://rwx.notion.site/Type-debugging-tools-de42260060784cacbaf08ea4d61e0eb9?pvs=4).

View file

@ -0,0 +1,13 @@
use std::fs;
use roc_checkmate_schema::AllEvents;
fn main() {
println!("cargo:rerun-if-changed=../checkmate_schema");
let schema = AllEvents::schema();
fs::write(
"schema.json",
serde_json::to_string_pretty(&schema).unwrap(),
)
.unwrap();
}

View file

@ -0,0 +1,907 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "AllEvents",
"type": "array",
"items": {
"$ref": "#/definitions/Event"
},
"definitions": {
"AliasKind": {
"oneOf": [
{
"type": "object",
"required": [
"type"
],
"properties": {
"type": {
"type": "string",
"enum": [
"Structural"
]
}
}
},
{
"type": "object",
"required": [
"type"
],
"properties": {
"type": {
"type": "string",
"enum": [
"Opaque"
]
}
}
}
]
},
"AliasTypeVariables": {
"type": "object",
"required": [
"infer_ext_in_output_position_variables",
"lambda_set_variables",
"type_variables"
],
"properties": {
"infer_ext_in_output_position_variables": {
"type": "array",
"items": {
"$ref": "#/definitions/Variable"
}
},
"lambda_set_variables": {
"type": "array",
"items": {
"$ref": "#/definitions/Variable"
}
},
"type_variables": {
"type": "array",
"items": {
"$ref": "#/definitions/Variable"
}
}
}
},
"ClosureType": {
"type": "object",
"required": [
"environment",
"function"
],
"properties": {
"environment": {
"type": "array",
"items": {
"$ref": "#/definitions/Variable"
}
},
"function": {
"$ref": "#/definitions/Symbol"
}
}
},
"Content": {
"oneOf": [
{
"type": "object",
"required": [
"type"
],
"properties": {
"name": {
"type": [
"string",
"null"
]
},
"type": {
"type": "string",
"enum": [
"Flex"
]
}
}
},
{
"type": "object",
"required": [
"name",
"type"
],
"properties": {
"name": {
"type": "string"
},
"type": {
"type": "string",
"enum": [
"Rigid"
]
}
}
},
{
"type": "object",
"required": [
"abilities",
"type"
],
"properties": {
"abilities": {
"type": "array",
"items": {
"$ref": "#/definitions/Symbol"
}
},
"name": {
"type": [
"string",
"null"
]
},
"type": {
"type": "string",
"enum": [
"FlexAble"
]
}
}
},
{
"type": "object",
"required": [
"abilities",
"name",
"type"
],
"properties": {
"abilities": {
"type": "array",
"items": {
"$ref": "#/definitions/Symbol"
}
},
"name": {
"type": "string"
},
"type": {
"type": "string",
"enum": [
"RigidAble"
]
}
}
},
{
"type": "object",
"required": [
"structure",
"type"
],
"properties": {
"name": {
"type": [
"string",
"null"
]
},
"structure": {
"$ref": "#/definitions/Variable"
},
"type": {
"type": "string",
"enum": [
"Recursive"
]
}
}
},
{
"type": "object",
"required": [
"ambient_function",
"solved",
"type",
"unspecialized"
],
"properties": {
"ambient_function": {
"$ref": "#/definitions/Variable"
},
"recursion_var": {
"anyOf": [
{
"$ref": "#/definitions/Variable"
},
{
"type": "null"
}
]
},
"solved": {
"type": "array",
"items": {
"$ref": "#/definitions/ClosureType"
}
},
"type": {
"type": "string",
"enum": [
"LambdaSet"
]
},
"unspecialized": {
"type": "array",
"items": {
"$ref": "#/definitions/UnspecializedClosureType"
}
}
}
},
{
"type": "object",
"required": [
"type"
],
"properties": {
"type": {
"type": "string",
"enum": [
"ErasedLambda"
]
}
}
},
{
"type": "object",
"required": [
"kind",
"name",
"real_variable",
"type",
"variables"
],
"properties": {
"kind": {
"$ref": "#/definitions/AliasKind"
},
"name": {
"$ref": "#/definitions/Symbol"
},
"real_variable": {
"$ref": "#/definitions/Variable"
},
"type": {
"type": "string",
"enum": [
"Alias"
]
},
"variables": {
"$ref": "#/definitions/AliasTypeVariables"
}
}
},
{
"type": "object",
"required": [
"symbol",
"type",
"variables"
],
"properties": {
"symbol": {
"$ref": "#/definitions/Symbol"
},
"type": {
"type": "string",
"enum": [
"Apply"
]
},
"variables": {
"type": "array",
"items": {
"$ref": "#/definitions/Variable"
}
}
}
},
{
"type": "object",
"required": [
"arguments",
"lambda_type",
"ret",
"type"
],
"properties": {
"arguments": {
"type": "array",
"items": {
"$ref": "#/definitions/Variable"
}
},
"lambda_type": {
"$ref": "#/definitions/Variable"
},
"ret": {
"$ref": "#/definitions/Variable"
},
"type": {
"type": "string",
"enum": [
"Function"
]
}
}
},
{
"type": "object",
"required": [
"extension",
"fields",
"type"
],
"properties": {
"extension": {
"$ref": "#/definitions/Variable"
},
"fields": {
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/RecordField"
}
},
"type": {
"type": "string",
"enum": [
"Record"
]
}
}
},
{
"type": "object",
"required": [
"elements",
"extension",
"type"
],
"properties": {
"elements": {
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/Variable"
}
},
"extension": {
"$ref": "#/definitions/Variable"
},
"type": {
"type": "string",
"enum": [
"Tuple"
]
}
}
},
{
"type": "object",
"required": [
"extension",
"tags",
"type"
],
"properties": {
"extension": {
"$ref": "#/definitions/TagUnionExtension"
},
"tags": {
"type": "object",
"additionalProperties": {
"type": "array",
"items": {
"$ref": "#/definitions/Variable"
}
}
},
"type": {
"type": "string",
"enum": [
"TagUnion"
]
}
}
},
{
"type": "object",
"required": [
"extension",
"functions",
"tags",
"type"
],
"properties": {
"extension": {
"$ref": "#/definitions/TagUnionExtension"
},
"functions": {
"type": "array",
"items": {
"$ref": "#/definitions/Symbol"
}
},
"tags": {
"type": "array",
"items": {
"type": "string"
}
},
"type": {
"type": "string",
"enum": [
"FunctionOrTagUnion"
]
}
}
},
{
"type": "object",
"required": [
"extension",
"recursion_var",
"tags",
"type"
],
"properties": {
"extension": {
"$ref": "#/definitions/TagUnionExtension"
},
"recursion_var": {
"$ref": "#/definitions/Variable"
},
"tags": {
"type": "object",
"additionalProperties": {
"type": "array",
"items": {
"$ref": "#/definitions/Variable"
}
}
},
"type": {
"type": "string",
"enum": [
"RecursiveTagUnion"
]
}
}
},
{
"type": "object",
"required": [
"type"
],
"properties": {
"type": {
"type": "string",
"enum": [
"EmptyRecord"
]
}
}
},
{
"type": "object",
"required": [
"type"
],
"properties": {
"type": {
"type": "string",
"enum": [
"EmptyTuple"
]
}
}
},
{
"type": "object",
"required": [
"type"
],
"properties": {
"type": {
"type": "string",
"enum": [
"EmptyTagUnion"
]
}
}
},
{
"type": "object",
"required": [
"range",
"type"
],
"properties": {
"range": {
"$ref": "#/definitions/NumericRange"
},
"type": {
"type": "string",
"enum": [
"RangedNumber"
]
}
}
},
{
"type": "object",
"required": [
"type"
],
"properties": {
"type": {
"type": "string",
"enum": [
"Error"
]
}
}
}
]
},
"Event": {
"oneOf": [
{
"type": "object",
"required": [
"left",
"mode",
"right",
"subevents",
"type"
],
"properties": {
"left": {
"$ref": "#/definitions/Variable"
},
"mode": {
"$ref": "#/definitions/UnificationMode"
},
"right": {
"$ref": "#/definitions/Variable"
},
"subevents": {
"type": "array",
"items": {
"$ref": "#/definitions/Event"
}
},
"success": {
"type": [
"boolean",
"null"
]
},
"type": {
"type": "string",
"enum": [
"Unification"
]
}
}
},
{
"type": "object",
"required": [
"from",
"to",
"type"
],
"properties": {
"from": {
"$ref": "#/definitions/Variable"
},
"to": {
"$ref": "#/definitions/Variable"
},
"type": {
"type": "string",
"enum": [
"VariableUnified"
]
}
}
},
{
"type": "object",
"required": [
"type",
"variable"
],
"properties": {
"content": {
"anyOf": [
{
"$ref": "#/definitions/Content"
},
{
"type": "null"
}
]
},
"rank": {
"anyOf": [
{
"$ref": "#/definitions/Rank"
},
{
"type": "null"
}
]
},
"type": {
"type": "string",
"enum": [
"VariableSetDescriptor"
]
},
"variable": {
"$ref": "#/definitions/Variable"
}
}
}
]
},
"NumericRange": {
"type": "object",
"required": [
"kind",
"min_width",
"signed"
],
"properties": {
"kind": {
"$ref": "#/definitions/NumericRangeKind"
},
"min_width": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"signed": {
"type": "boolean"
}
}
},
"NumericRangeKind": {
"oneOf": [
{
"type": "object",
"required": [
"type"
],
"properties": {
"type": {
"type": "string",
"enum": [
"Int"
]
}
}
},
{
"type": "object",
"required": [
"type"
],
"properties": {
"type": {
"type": "string",
"enum": [
"AnyNum"
]
}
}
}
]
},
"Rank": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"RecordField": {
"type": "object",
"required": [
"field_type",
"kind"
],
"properties": {
"field_type": {
"$ref": "#/definitions/Variable"
},
"kind": {
"$ref": "#/definitions/RecordFieldKind"
}
}
},
"RecordFieldKind": {
"oneOf": [
{
"type": "object",
"required": [
"type"
],
"properties": {
"type": {
"type": "string",
"enum": [
"Demanded"
]
}
}
},
{
"type": "object",
"required": [
"rigid",
"type"
],
"properties": {
"rigid": {
"type": "boolean"
},
"type": {
"type": "string",
"enum": [
"Required"
]
}
}
},
{
"type": "object",
"required": [
"rigid",
"type"
],
"properties": {
"rigid": {
"type": "boolean"
},
"type": {
"type": "string",
"enum": [
"Optional"
]
}
}
}
]
},
"Symbol": {
"type": "string"
},
"TagUnionExtension": {
"oneOf": [
{
"type": "object",
"required": [
"type",
"variable"
],
"properties": {
"type": {
"type": "string",
"enum": [
"Openness"
]
},
"variable": {
"$ref": "#/definitions/Variable"
}
}
},
{
"type": "object",
"required": [
"type",
"variable"
],
"properties": {
"type": {
"type": "string",
"enum": [
"Any"
]
},
"variable": {
"$ref": "#/definitions/Variable"
}
}
}
]
},
"UnificationMode": {
"oneOf": [
{
"type": "object",
"required": [
"type"
],
"properties": {
"type": {
"type": "string",
"enum": [
"Eq"
]
}
}
},
{
"type": "object",
"required": [
"type"
],
"properties": {
"type": {
"type": "string",
"enum": [
"Present"
]
}
}
},
{
"type": "object",
"required": [
"type"
],
"properties": {
"type": {
"type": "string",
"enum": [
"LambdaSetSpecialization"
]
}
}
}
]
},
"UnspecializedClosureType": {
"type": "object",
"required": [
"ability_member",
"lambda_set_region",
"specialization"
],
"properties": {
"ability_member": {
"$ref": "#/definitions/Symbol"
},
"lambda_set_region": {
"type": "integer",
"format": "uint8",
"minimum": 0.0
},
"specialization": {
"$ref": "#/definitions/Variable"
}
}
},
"Variable": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
}
}
}

View file

@ -0,0 +1,175 @@
use std::error::Error;
use roc_checkmate_schema::{AllEvents, Event};
use roc_types::subs as s;
use crate::convert::AsSchema;
#[derive(Debug)]
pub struct Collector {
events: AllEvents,
current_event_path: Vec<usize>,
}
impl Default for Collector {
fn default() -> Self {
Self::new()
}
}
impl Collector {
pub fn new() -> Self {
Self {
events: AllEvents(Vec::new()),
current_event_path: Vec::new(),
}
}
pub fn unify(&mut self, subs: &s::Subs, from: s::Variable, to: s::Variable) {
let to = to.as_schema(subs);
let from = from.as_schema(subs);
self.add_event(Event::VariableUnified { to, from });
}
pub fn set_content(&mut self, subs: &s::Subs, var: s::Variable, content: s::Content) {
let variable = var.as_schema(subs);
let content = content.as_schema(subs);
self.add_event(Event::VariableSetDescriptor {
variable,
content: Some(content),
rank: None,
});
}
pub fn set_rank(&mut self, subs: &s::Subs, var: s::Variable, rank: s::Rank) {
let variable = var.as_schema(subs);
let rank = rank.as_schema(subs);
self.add_event(Event::VariableSetDescriptor {
variable,
rank: Some(rank),
content: None,
});
}
pub fn set_descriptor(&mut self, subs: &s::Subs, var: s::Variable, descriptor: s::Descriptor) {
let variable = var.as_schema(subs);
let rank = descriptor.rank.as_schema(subs);
let content = descriptor.content.as_schema(subs);
self.add_event(Event::VariableSetDescriptor {
variable,
rank: Some(rank),
content: Some(content),
});
}
pub fn start_unification(
&mut self,
subs: &s::Subs,
left: s::Variable,
right: s::Variable,
mode: roc_solve_schema::UnificationMode,
) {
let left = left.as_schema(subs);
let right = right.as_schema(subs);
let mode = mode.as_schema(subs);
let subevents = Vec::new();
self.add_event(Event::Unification {
left,
right,
mode,
subevents,
success: None,
});
}
pub fn end_unification(
&mut self,
subs: &s::Subs,
left: s::Variable,
right: s::Variable,
success: bool,
) {
let current_event = self.get_path_event();
match current_event {
EventW::Sub(Event::Unification {
left: l,
right: r,
success: s,
..
}) => {
assert_eq!(left.as_schema(subs), *l);
assert_eq!(right.as_schema(subs), *r);
assert!(s.is_none());
*s = Some(success);
}
_ => panic!("end_unification called when not in a unification"),
}
self.current_event_path.pop();
}
pub fn write(&self, writer: impl std::io::Write) -> Result<(), Box<dyn Error>> {
self.events.write(writer)?;
Ok(())
}
fn add_event(&mut self, event: impl Into<Event>) {
let mut event = event.into();
let is_appendable = EventW::Sub(&mut event).appendable();
let event = event;
let path_event = self.get_path_event();
let new_event_index = path_event.append(event);
if is_appendable {
self.current_event_path.push(new_event_index);
}
}
fn get_path_event(&mut self) -> EventW {
let mut event = EventW::Top(&mut self.events);
for i in &self.current_event_path {
event = event.index(*i);
}
event
}
}
enum EventW<'a> {
Top(&'a mut AllEvents),
Sub(&'a mut Event),
}
impl<'a> EventW<'a> {
fn append(self, event: Event) -> usize {
let list = self.subevents_mut().unwrap();
let index = list.len();
list.push(event);
index
}
fn appendable(self) -> bool {
self.subevents().is_some()
}
fn index(self, index: usize) -> EventW<'a> {
Self::Sub(&mut self.subevents_mut().unwrap()[index])
}
}
impl<'a> EventW<'a> {
fn subevents(self) -> Option<&'a Vec<Event>> {
use EventW::*;
match self {
Top(events) => Some(&events.0),
Sub(Event::Unification { subevents, .. }) => Some(subevents),
Sub(Event::VariableUnified { .. } | Event::VariableSetDescriptor { .. }) => None,
}
}
fn subevents_mut(self) -> Option<&'a mut Vec<Event>> {
use EventW::*;
match self {
Top(events) => Some(&mut events.0),
Sub(Event::Unification { subevents, .. }) => Some(subevents),
Sub(Event::VariableUnified { .. } | Event::VariableSetDescriptor { .. }) => None,
}
}
}

View file

@ -0,0 +1,323 @@
use std::collections::HashMap;
use roc_module::{ident, symbol};
use roc_types::{
num,
subs::{self, GetSubsSlice, Subs, SubsIndex, SubsSlice, UnionLabels},
types,
};
use roc_checkmate_schema::{
AliasKind, AliasTypeVariables, ClosureType, Content, NumericRange, NumericRangeKind, Rank,
RecordField, RecordFieldKind, Symbol, TagUnionExtension, UnificationMode,
UnspecializedClosureType, Variable,
};
pub trait AsSchema<T> {
fn as_schema(&self, subs: &Subs) -> T;
}
impl<T, U> AsSchema<Option<U>> for Option<T>
where
T: AsSchema<U>,
T: Copy,
{
fn as_schema(&self, subs: &Subs) -> Option<U> {
self.map(|i| i.as_schema(subs))
}
}
impl<T, U> AsSchema<Vec<U>> for &[T]
where
T: AsSchema<U>,
{
fn as_schema(&self, subs: &Subs) -> Vec<U> {
self.iter().map(|i| i.as_schema(subs)).collect()
}
}
impl<T, U> AsSchema<U> for SubsIndex<T>
where
Subs: std::ops::Index<SubsIndex<T>, Output = T>,
T: AsSchema<U>,
{
fn as_schema(&self, subs: &Subs) -> U {
subs[*self].as_schema(subs)
}
}
impl<T, U> AsSchema<Vec<U>> for SubsSlice<T>
where
Subs: GetSubsSlice<T>,
T: AsSchema<U>,
{
fn as_schema(&self, subs: &Subs) -> Vec<U> {
subs.get_subs_slice(*self)
.iter()
.map(|i| i.as_schema(subs))
.collect()
}
}
impl AsSchema<Content> for subs::Content {
fn as_schema(&self, subs: &Subs) -> Content {
use {subs::Content as A, Content as B};
match self {
A::FlexVar(name) => B::Flex(name.as_schema(subs)),
A::RigidVar(name) => B::Rigid(name.as_schema(subs)),
A::FlexAbleVar(name, abilities) => {
B::FlexAble(name.as_schema(subs), abilities.as_schema(subs))
}
A::RigidAbleVar(name, abilities) => {
B::RigidAble(name.as_schema(subs), abilities.as_schema(subs))
}
A::RecursionVar {
structure,
opt_name,
} => B::Recursive(opt_name.as_schema(subs), structure.as_schema(subs)),
A::LambdaSet(lambda_set) => lambda_set.as_schema(subs),
A::ErasedLambda => B::ErasedLambda(),
A::Structure(flat_type) => flat_type.as_schema(subs),
A::Alias(name, type_vars, real_var, kind) => B::Alias(
name.as_schema(subs),
type_vars.as_schema(subs),
real_var.as_schema(subs),
kind.as_schema(subs),
),
A::RangedNumber(range) => B::RangedNumber(range.as_schema(subs)),
A::Error => B::Error(),
}
}
}
impl AsSchema<Content> for subs::FlatType {
fn as_schema(&self, subs: &Subs) -> Content {
match self {
subs::FlatType::Apply(symbol, variables) => {
Content::Apply(symbol.as_schema(subs), variables.as_schema(subs))
}
subs::FlatType::Func(arguments, closure, ret) => Content::Function(
arguments.as_schema(subs),
closure.as_schema(subs),
ret.as_schema(subs),
),
subs::FlatType::Record(fields, ext) => {
Content::Record(fields.as_schema(subs), ext.as_schema(subs))
}
subs::FlatType::Tuple(elems, ext) => {
Content::Tuple(elems.as_schema(subs), ext.as_schema(subs))
}
subs::FlatType::TagUnion(tags, ext) => {
Content::TagUnion(tags.as_schema(subs), ext.as_schema(subs))
}
subs::FlatType::FunctionOrTagUnion(tags, functions, ext) => {
Content::FunctionOrTagUnion(
functions.as_schema(subs),
tags.as_schema(subs),
ext.as_schema(subs),
)
}
subs::FlatType::RecursiveTagUnion(rec_var, tags, ext) => Content::RecursiveTagUnion(
rec_var.as_schema(subs),
tags.as_schema(subs),
ext.as_schema(subs),
),
subs::FlatType::EmptyRecord => Content::EmptyRecord(),
subs::FlatType::EmptyTuple => Content::EmptyTuple(),
subs::FlatType::EmptyTagUnion => Content::EmptyTagUnion(),
}
}
}
impl AsSchema<Content> for subs::LambdaSet {
fn as_schema(&self, subs: &Subs) -> Content {
let subs::LambdaSet {
solved,
unspecialized,
recursion_var,
ambient_function,
} = self;
Content::LambdaSet(
solved.as_schema(subs),
unspecialized.as_schema(subs),
recursion_var.as_schema(subs),
ambient_function.as_schema(subs),
)
}
}
impl AsSchema<String> for ident::Lowercase {
fn as_schema(&self, _subs: &Subs) -> String {
self.to_string()
}
}
impl AsSchema<Symbol> for symbol::Symbol {
fn as_schema(&self, _subs: &Subs) -> Symbol {
Symbol(format!("{:#?}", self))
}
}
impl AsSchema<Variable> for subs::Variable {
fn as_schema(&self, _subs: &Subs) -> Variable {
Variable(self.index())
}
}
impl AsSchema<Option<Variable>> for subs::OptVariable {
fn as_schema(&self, _subs: &Subs) -> Option<Variable> {
self.into_variable().map(|i| i.as_schema(_subs))
}
}
impl AsSchema<Vec<ClosureType>> for UnionLabels<symbol::Symbol> {
fn as_schema(&self, subs: &Subs) -> Vec<ClosureType> {
self.iter_from_subs(subs)
.map(|(function, environment)| ClosureType {
function: function.as_schema(subs),
environment: environment.as_schema(subs),
})
.collect()
}
}
impl AsSchema<UnspecializedClosureType> for types::Uls {
fn as_schema(&self, subs: &Subs) -> UnspecializedClosureType {
let types::Uls(specialization, ability_member, lambda_set_region) = self;
UnspecializedClosureType {
specialization: specialization.as_schema(subs),
ability_member: ability_member.as_schema(subs),
lambda_set_region: *lambda_set_region,
}
}
}
impl AsSchema<AliasTypeVariables> for subs::AliasVariables {
fn as_schema(&self, subs: &Subs) -> AliasTypeVariables {
let type_variables = self.type_variables().as_schema(subs);
let lambda_set_variables = self.lambda_set_variables().as_schema(subs);
let infer_ext_in_output_position_variables =
self.infer_ext_in_output_variables().as_schema(subs);
AliasTypeVariables {
type_variables,
lambda_set_variables,
infer_ext_in_output_position_variables,
}
}
}
impl AsSchema<AliasKind> for types::AliasKind {
fn as_schema(&self, _subs: &Subs) -> AliasKind {
match self {
types::AliasKind::Structural => AliasKind::Structural,
types::AliasKind::Opaque => AliasKind::Opaque,
}
}
}
impl AsSchema<HashMap<String, RecordField>> for subs::RecordFields {
fn as_schema(&self, subs: &Subs) -> HashMap<String, RecordField> {
let mut map = HashMap::new();
for (name, var, field) in self.iter_all() {
let name = name.as_schema(subs);
let field_type = var.as_schema(subs);
let kind = field.as_schema(subs);
map.insert(name, RecordField { field_type, kind });
}
map
}
}
impl AsSchema<RecordFieldKind> for types::RecordField<()> {
fn as_schema(&self, _subs: &Subs) -> RecordFieldKind {
match self {
types::RecordField::Demanded(_) => RecordFieldKind::Demanded,
types::RecordField::Required(_) => RecordFieldKind::Required { rigid: false },
types::RecordField::Optional(_) => RecordFieldKind::Optional { rigid: false },
types::RecordField::RigidRequired(_) => RecordFieldKind::Required { rigid: true },
types::RecordField::RigidOptional(_) => RecordFieldKind::Optional { rigid: true },
}
}
}
impl AsSchema<HashMap<u32, Variable>> for subs::TupleElems {
fn as_schema(&self, subs: &Subs) -> HashMap<u32, Variable> {
let mut map = HashMap::new();
for (index, var) in self.iter_all() {
let name = subs[index] as _;
let var = var.as_schema(subs);
map.insert(name, var);
}
map
}
}
impl AsSchema<HashMap<String, Vec<Variable>>> for subs::UnionTags {
fn as_schema(&self, subs: &Subs) -> HashMap<String, Vec<Variable>> {
let mut map = HashMap::new();
for (tag, payloads) in self.iter_from_subs(subs) {
map.insert(tag.as_schema(subs), payloads.as_schema(subs));
}
map
}
}
impl AsSchema<TagUnionExtension> for subs::TagExt {
fn as_schema(&self, subs: &Subs) -> TagUnionExtension {
match self {
subs::TagExt::Openness(var) => TagUnionExtension::Openness(var.as_schema(subs)),
subs::TagExt::Any(var) => TagUnionExtension::Any(var.as_schema(subs)),
}
}
}
impl AsSchema<NumericRange> for num::NumericRange {
fn as_schema(&self, _subs: &Subs) -> NumericRange {
let kind =
match self {
num::NumericRange::IntAtLeastSigned(_)
| num::NumericRange::IntAtLeastEitherSign(_) => NumericRangeKind::Int,
num::NumericRange::NumAtLeastSigned(_)
| num::NumericRange::NumAtLeastEitherSign(_) => NumericRangeKind::AnyNum,
};
let min_width = self.min_width();
let (signedness, width) = min_width.signedness_and_width();
let signed = signedness.is_signed();
NumericRange {
kind,
signed,
min_width: width,
}
}
}
impl AsSchema<String> for ident::TagName {
fn as_schema(&self, _subs: &Subs) -> String {
self.0.to_string()
}
}
impl AsSchema<Rank> for subs::Rank {
fn as_schema(&self, _subs: &Subs) -> Rank {
Rank(self.into_usize() as _)
}
}
impl AsSchema<UnificationMode> for roc_solve_schema::UnificationMode {
fn as_schema(&self, _subs: &Subs) -> UnificationMode {
if self.is_eq() {
UnificationMode::Eq
} else if self.is_present() {
UnificationMode::Present
} else if self.is_lambda_set_specialization() {
UnificationMode::LambdaSetSpecialization
} else {
unreachable!()
}
}
}

View file

@ -0,0 +1,62 @@
mod collector;
mod convert;
pub use collector::Collector;
pub fn is_checkmate_enabled() -> bool {
#[cfg(debug_assertions)]
{
let flag = std::env::var("ROC_CHECKMATE");
flag.as_deref() == Ok("1")
}
#[cfg(not(debug_assertions))]
{
false
}
}
#[macro_export]
macro_rules! debug_checkmate {
($opt_collector:expr, $cm:ident => $expr:expr) => {
#[cfg(debug_assertions)]
{
if let Some($cm) = $opt_collector.as_mut() {
$expr
}
}
};
}
#[macro_export]
macro_rules! dump_checkmate {
($opt_collector:expr) => {
#[cfg(debug_assertions)]
{
if let Some(cm) = $opt_collector.as_ref() {
$crate::dump_checkmate(cm);
}
}
};
}
pub fn dump_checkmate(collector: &Collector) {
let timestamp = chrono::Local::now().format("%Y%m%d_%H-%M-%S");
let filename = format!("checkmate_{timestamp}.json");
let fi = std::fs::File::create(&filename).unwrap();
collector.write(fi).unwrap();
eprintln!("Wrote checkmate output to {filename}");
}
#[macro_export]
macro_rules! with_checkmate {
({ on => $on:expr, off => $off:expr, }) => {{
#[cfg(debug_assertions)]
{
$on
}
#[cfg(not(debug_assertions))]
{
$off
}
}};
}

View file

@ -0,0 +1,17 @@
/node_modules
/.pnp
.pnp.js
/coverage
/build
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

View file

@ -0,0 +1,4 @@
{
"tsserver.useLocalTsdk": true,
"tsserver.tsdk": "${workspaceFolder}/node_modules/typescript/lib"
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,54 @@
{
"name": "checkmate",
"version": "0.1.0",
"private": true,
"dependencies": {
"elkjs": "^0.8.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.14.2",
"react-router-hash-link": "^2.4.3",
"react-tooltip": "^5.19.0",
"reactflow": "^11.7.4"
},
"scripts": {
"start": "react-scripts start",
"build": "npm run build:codegen && react-scripts build",
"build:codegen": "node ./scripts/gen_schema_dts.js",
"check": "tsc",
"lint": "eslint src/ scripts/",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@types/jest": "^27.5.2",
"@types/node": "^16.18.38",
"@types/react": "^18.2.15",
"@types/react-dom": "^18.2.7",
"@types/react-router-hash-link": "^2.4.6",
"clsx": "^2.0.0",
"json-schema-to-typescript": "^13.0.2",
"react-scripts": "5.0.1",
"tailwindcss": "^3.3.3",
"tiny-typed-emitter": "^2.1.0",
"typescript": "^4.9.5"
}
}

View file

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="checkmate in N" />
<title>Checkmate</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>

View file

@ -0,0 +1,24 @@
const {compileFromFile} = require("json-schema-to-typescript");
const fs = require("node:fs/promises");
const path = require("node:path");
const SCHEMA_PATH = path.resolve(
__dirname,
"..",
"..",
"schema.json"
);
const DTS_PATH = path.resolve(
__dirname,
"..",
"src",
"schema.d.ts"
);
async function main() {
const result = await compileFromFile(SCHEMA_PATH);
await fs.writeFile(DTS_PATH, result);
}
main().catch(console.error);

View file

@ -0,0 +1,37 @@
import React from "react";
import FileInput, { LoadedEvents } from "./components/FileInput";
import Ui from "./components/Ui";
import { BrowserRouter } from "react-router-dom";
export default function App() {
const [events, setEvents] = React.useState<LoadedEvents | null>(null);
return (
<BrowserRouter>
<div className="w-screen h-screen p-2 bg-gray-100 flex flex-col">
<div>
<FileInput setResult={setEvents} />
</div>
<div className="flex-1 overflow-hidden">
<EventsWrapper events={events} />
</div>
</div>
</BrowserRouter>
);
}
interface EventsWrapperProps {
events: LoadedEvents | null;
}
function EventsWrapper({ events }: EventsWrapperProps): JSX.Element {
if (events === null) {
return <div></div>;
}
switch (events.kind) {
case "ok":
return <Ui events={events.events} />;
case "err":
return <div className="text-red-400 text-lg">{events.error}</div>;
}
}

View file

@ -0,0 +1,545 @@
[
{
"type": "Unification",
"left": 1540,
"right": 71,
"mode": { "type": "Eq" },
"success": true,
"subevents": [
{
"type": "VariableSetDescriptor",
"variable": 71,
"rank": 1,
"content": {
"type": "Function",
"arguments": [1543, 1544],
"lambda_type": 1542,
"ret": 1541
}
},
{ "type": "VariableUnified", "from": 1540, "to": 71 },
{ "type": "VariableUnified", "from": 71, "to": 71 }
]
},
{
"type": "Unification",
"left": 71,
"right": 1546,
"mode": { "type": "Eq" },
"success": true,
"subevents": [
{
"type": "Unification",
"left": 1543,
"right": 67,
"mode": { "type": "Eq" },
"success": true,
"subevents": [
{
"type": "VariableSetDescriptor",
"variable": 67,
"rank": 1,
"content": {
"type": "Alias",
"name": "`Num.Num`",
"variables": {
"type_variables": [1545],
"lambda_set_variables": [],
"infer_ext_in_output_position_variables": []
},
"real_variable": 1545,
"kind": { "type": "Opaque" }
}
},
{ "type": "VariableUnified", "from": 1543, "to": 67 },
{ "type": "VariableUnified", "from": 67, "to": 67 }
]
},
{
"type": "Unification",
"left": 1544,
"right": 69,
"mode": { "type": "Eq" },
"success": true,
"subevents": [
{
"type": "VariableSetDescriptor",
"variable": 69,
"rank": 1,
"content": {
"type": "Alias",
"name": "`Num.Num`",
"variables": {
"type_variables": [1545],
"lambda_set_variables": [],
"infer_ext_in_output_position_variables": []
},
"real_variable": 1545,
"kind": { "type": "Opaque" }
}
},
{ "type": "VariableUnified", "from": 1544, "to": 69 },
{ "type": "VariableUnified", "from": 69, "to": 69 }
]
},
{
"type": "Unification",
"left": 1541,
"right": 73,
"mode": { "type": "Eq" },
"success": true,
"subevents": [
{
"type": "VariableSetDescriptor",
"variable": 73,
"rank": 1,
"content": {
"type": "Alias",
"name": "`Num.Num`",
"variables": {
"type_variables": [1545],
"lambda_set_variables": [],
"infer_ext_in_output_position_variables": []
},
"real_variable": 1545,
"kind": { "type": "Opaque" }
}
},
{ "type": "VariableUnified", "from": 1541, "to": 73 },
{ "type": "VariableUnified", "from": 73, "to": 73 }
]
},
{
"type": "Unification",
"left": 1542,
"right": 72,
"mode": { "type": "Eq" },
"success": true,
"subevents": [
{
"type": "VariableSetDescriptor",
"variable": 72,
"rank": 1,
"content": {
"type": "LambdaSet",
"solved": [{ "function": "`Num.add`", "environment": [] }],
"unspecialized": [],
"recursion_var": null,
"ambient_function": 1540
}
},
{ "type": "VariableUnified", "from": 1542, "to": 72 },
{ "type": "VariableUnified", "from": 72, "to": 72 }
]
},
{
"type": "VariableSetDescriptor",
"variable": 1546,
"rank": 1,
"content": {
"type": "Function",
"arguments": [67, 69],
"lambda_type": 1542,
"ret": 73
}
},
{ "type": "VariableUnified", "from": 71, "to": 1546 },
{ "type": "VariableUnified", "from": 1546, "to": 1546 }
]
},
{
"type": "Unification",
"left": 66,
"right": 1547,
"mode": { "type": "Eq" },
"success": true,
"subevents": [
{
"type": "VariableSetDescriptor",
"variable": 1547,
"rank": 1,
"content": {
"type": "RangedNumber",
"range": {
"kind": { "type": "AnyNum" },
"signed": true,
"min_width": 8
}
}
},
{ "type": "VariableUnified", "from": 66, "to": 1547 },
{ "type": "VariableUnified", "from": 1547, "to": 1547 }
]
},
{
"type": "Unification",
"left": 1548,
"right": 67,
"mode": { "type": "Eq" },
"success": true,
"subevents": [
{
"type": "Unification",
"left": 66,
"right": 1545,
"mode": { "type": "Eq" },
"success": true,
"subevents": [
{
"type": "VariableSetDescriptor",
"variable": 1545,
"rank": 1,
"content": {
"type": "RangedNumber",
"range": {
"kind": { "type": "AnyNum" },
"signed": true,
"min_width": 8
}
}
},
{ "type": "VariableUnified", "from": 1547, "to": 1545 },
{ "type": "VariableUnified", "from": 1545, "to": 1545 }
]
},
{
"type": "VariableSetDescriptor",
"variable": 67,
"rank": 1,
"content": {
"type": "Alias",
"name": "`Num.Num`",
"variables": {
"type_variables": [66],
"lambda_set_variables": [],
"infer_ext_in_output_position_variables": []
},
"real_variable": 66,
"kind": { "type": "Opaque" }
}
},
{ "type": "VariableUnified", "from": 1548, "to": 67 },
{ "type": "VariableUnified", "from": 67, "to": 67 }
]
},
{
"type": "Unification",
"left": 68,
"right": 1549,
"mode": { "type": "Eq" },
"success": true,
"subevents": [
{
"type": "VariableSetDescriptor",
"variable": 1549,
"rank": 1,
"content": {
"type": "RangedNumber",
"range": {
"kind": { "type": "AnyNum" },
"signed": true,
"min_width": 8
}
}
},
{ "type": "VariableUnified", "from": 68, "to": 1549 },
{ "type": "VariableUnified", "from": 1549, "to": 1549 }
]
},
{
"type": "Unification",
"left": 1550,
"right": 69,
"mode": { "type": "Eq" },
"success": true,
"subevents": [
{
"type": "Unification",
"left": 68,
"right": 1545,
"mode": { "type": "Eq" },
"success": true,
"subevents": [
{
"type": "VariableSetDescriptor",
"variable": 1545,
"rank": 1,
"content": {
"type": "RangedNumber",
"range": {
"kind": { "type": "AnyNum" },
"signed": true,
"min_width": 8
}
}
},
{ "type": "VariableUnified", "from": 1549, "to": 1545 },
{ "type": "VariableUnified", "from": 1545, "to": 1545 }
]
},
{
"type": "VariableSetDescriptor",
"variable": 69,
"rank": 1,
"content": {
"type": "Alias",
"name": "`Num.Num`",
"variables": {
"type_variables": [68],
"lambda_set_variables": [],
"infer_ext_in_output_position_variables": []
},
"real_variable": 68,
"kind": { "type": "Opaque" }
}
},
{ "type": "VariableUnified", "from": 1550, "to": 69 },
{ "type": "VariableUnified", "from": 69, "to": 69 }
]
},
{
"type": "Unification",
"left": 73,
"right": 1551,
"mode": { "type": "Eq" },
"success": true,
"subevents": [
{
"type": "Unification",
"left": 73,
"right": 1552,
"mode": { "type": "Eq" },
"success": true,
"subevents": [
{
"type": "Unification",
"left": 1545,
"right": 1553,
"mode": { "type": "Eq" },
"success": true,
"subevents": [
{
"type": "Unification",
"left": 1556,
"right": 1554,
"mode": { "type": "Eq" },
"success": true,
"subevents": [
{
"type": "VariableSetDescriptor",
"variable": 1554,
"rank": 1,
"content": {
"type": "Alias",
"name": "`Num.Unsigned8`",
"variables": {
"type_variables": [],
"lambda_set_variables": [],
"infer_ext_in_output_position_variables": []
},
"real_variable": 1555,
"kind": { "type": "Opaque" }
}
},
{ "type": "VariableUnified", "from": 1556, "to": 1554 },
{ "type": "VariableUnified", "from": 1554, "to": 1554 }
]
}
]
},
{
"type": "Unification",
"left": 1545,
"right": 1553,
"mode": { "type": "Eq" },
"success": true,
"subevents": [
{
"type": "VariableSetDescriptor",
"variable": 1553,
"rank": 1,
"content": {
"type": "Alias",
"name": "`Num.Integer`",
"variables": {
"type_variables": [1556],
"lambda_set_variables": [],
"infer_ext_in_output_position_variables": []
},
"real_variable": 1556,
"kind": { "type": "Opaque" }
}
},
{ "type": "VariableUnified", "from": 1545, "to": 1553 },
{ "type": "VariableUnified", "from": 1553, "to": 1553 }
]
},
{
"type": "VariableSetDescriptor",
"variable": 1552,
"rank": 1,
"content": {
"type": "Alias",
"name": "`Num.Num`",
"variables": {
"type_variables": [1545],
"lambda_set_variables": [],
"infer_ext_in_output_position_variables": []
},
"real_variable": 1545,
"kind": { "type": "Opaque" }
}
},
{ "type": "VariableUnified", "from": 73, "to": 1552 },
{ "type": "VariableUnified", "from": 1552, "to": 1552 }
]
}
]
},
{
"type": "Unification",
"left": 1551,
"right": 75,
"mode": { "type": "Eq" },
"success": true,
"subevents": [
{
"type": "VariableSetDescriptor",
"variable": 75,
"rank": 1,
"content": {
"type": "Alias",
"name": "`Num.U8`",
"variables": {
"type_variables": [],
"lambda_set_variables": [],
"infer_ext_in_output_position_variables": []
},
"real_variable": 1552,
"kind": { "type": "Structural" }
}
},
{ "type": "VariableUnified", "from": 1551, "to": 75 },
{ "type": "VariableUnified", "from": 75, "to": 75 }
]
},
{
"type": "Unification",
"left": 80,
"right": 75,
"mode": { "type": "Eq" },
"success": true,
"subevents": [
{
"type": "VariableSetDescriptor",
"variable": 75,
"rank": 1,
"content": {
"type": "Alias",
"name": "`Num.U8`",
"variables": {
"type_variables": [],
"lambda_set_variables": [],
"infer_ext_in_output_position_variables": []
},
"real_variable": 1552,
"kind": { "type": "Structural" }
}
},
{ "type": "VariableUnified", "from": 80, "to": 75 },
{ "type": "VariableUnified", "from": 75, "to": 75 }
]
},
{
"type": "Unification",
"left": 1557,
"right": 79,
"mode": { "type": "Eq" },
"success": true,
"subevents": [
{
"type": "VariableSetDescriptor",
"variable": 79,
"rank": 1,
"content": {
"type": "Alias",
"name": "`Bool.Bool`",
"variables": {
"type_variables": [],
"lambda_set_variables": [],
"infer_ext_in_output_position_variables": []
},
"real_variable": 1558,
"kind": { "type": "Opaque" }
}
},
{ "type": "VariableUnified", "from": 1557, "to": 79 },
{ "type": "VariableUnified", "from": 79, "to": 79 }
]
},
{
"type": "Unification",
"left": 79,
"right": 5,
"mode": { "type": "Eq" },
"success": true,
"subevents": [
{
"type": "Unification",
"left": 1558,
"right": 4,
"mode": { "type": "Eq" },
"success": true,
"subevents": [
{
"type": "Unification",
"left": 84,
"right": 3,
"mode": { "type": "Eq" },
"success": true,
"subevents": [
{
"type": "VariableSetDescriptor",
"variable": 3,
"rank": 0,
"content": { "type": "EmptyTagUnion" }
},
{ "type": "VariableUnified", "from": 84, "to": 3 },
{ "type": "VariableUnified", "from": 3, "to": 3 }
]
},
{
"type": "VariableSetDescriptor",
"variable": 4,
"rank": 0,
"content": {
"type": "TagUnion",
"tags": { "False": [], "True": [] },
"extension": { "type": "Any", "variable": 84 }
}
},
{ "type": "VariableUnified", "from": 1558, "to": 4 },
{ "type": "VariableUnified", "from": 4, "to": 4 }
]
},
{
"type": "VariableSetDescriptor",
"variable": 5,
"rank": 0,
"content": {
"type": "Alias",
"name": "`Bool.Bool`",
"variables": {
"type_variables": [],
"lambda_set_variables": [],
"infer_ext_in_output_position_variables": []
},
"real_variable": 1558,
"kind": { "type": "Opaque" }
}
},
{ "type": "VariableUnified", "from": 79, "to": 5 },
{ "type": "VariableUnified", "from": 5, "to": 5 }
]
}
]

View file

@ -0,0 +1,28 @@
import clsx from "clsx";
interface EpochCellProps {
className?: string;
children?: React.ReactNode;
focus?: boolean;
}
export const EPOCH_STYLES =
"text-slate-900 font-mono bg-slate-200 p-1 py-0 rounded-sm text-sm transition ease-in-out duration-700 mr-2";
export default function EpochCell({
className,
children,
focus,
}: EpochCellProps) {
return (
<span
className={clsx(
EPOCH_STYLES,
className,
focus === true ? "ring-2 ring-blue-500" : "ring-1 ring-slate-500"
)}
>
{children}
</span>
);
}

View file

@ -0,0 +1,18 @@
import clsx from "clsx";
import { Variable } from "../../schema";
import { VariableName } from "./VariableName";
export interface UnknownVariableProps {
variable: Variable;
}
export function UnknownVariable({
variable,
}: UnknownVariableProps): JSX.Element {
return (
<div className={clsx("rounded-md whitespace-nowrap space-x-1 pr-1")}>
<VariableName className="inline-block" variable={variable} />
<span>???</span>
</div>
);
}

View file

@ -0,0 +1,73 @@
import { ComponentProps } from "react";
import clsx from "clsx";
import { QuerySubs, TypeDescriptor } from "../../engine/subs";
import { Variable } from "../../schema";
import DrawHeadConstructor from "../Content/HeadConstructor";
import { contentStyles } from "./../Content";
import { VariableName } from "./VariableName";
interface VariableElProps {
variable: Variable;
subs: QuerySubs;
onClick?: (variable: Variable) => void;
nested?: boolean;
raw?: boolean;
}
export function VariableElPretty(props: VariableElProps): JSX.Element {
const { variable, subs } = props;
const desc = subs.get_root(variable);
const content = (
<DrawHeadConstructor
desc={desc}
drawVariablePretty={(variable) => (
<VariableElPretty {...props} variable={variable} nested />
)}
drawVariableRaw={(variable) => (
<VariableElRaw {...props} variable={variable} nested raw />
)}
/>
);
return (
<Helper {...props} desc={desc}>
{content}
</Helper>
);
}
function VariableElRaw(props: VariableElProps): JSX.Element {
const desc = props.subs.get_root(props.variable);
return <Helper {...props} desc={desc}></Helper>;
}
function Helper({
children,
variable,
desc,
onClick,
nested,
raw,
}: VariableElProps &
Pick<ComponentProps<"div">, "children"> & {
desc: TypeDescriptor | undefined;
}): JSX.Element {
const { bg } = contentStyles(desc);
return (
<span
className={clsx(
"rounded-md whitespace-nowrap",
bg,
nested ? "text-sm" : "p-0.5 pl-0 text-base"
)}
>
{(!nested || raw) && (
<VariableName
variable={variable}
onClick={onClick}
className={nested ? "text-md" : "p-0.5"}
/>
)}
{children ? <span className="px-1">{children}</span> : <></>}
</span>
);
}

View file

@ -0,0 +1,34 @@
import clsx from "clsx";
import { QuerySubs } from "../../engine/subs";
import { Variable } from "../../schema";
import { VariableName } from "./VariableName";
export interface VariableLinkProps {
variable: Variable;
subs: QuerySubs;
onClick?: (variable: Variable) => void;
}
export function VariableLink({
variable,
subs,
onClick,
}: VariableLinkProps): JSX.Element {
const root = subs.get_root_key(variable);
if (variable === root) {
throw new Error("VariableLink: variable is root");
}
return (
<div className={clsx("rounded-md whitespace-nowrap space-x-1")}>
<VariableName className="inline-block" variable={variable} />
<span></span>
<VariableName
className="inline-block"
variable={root}
onClick={onClick}
/>
</div>
);
}

View file

@ -0,0 +1,30 @@
import clsx from "clsx";
import { Variable } from "../../schema";
export interface VariableNameProps {
variable: Variable;
onClick?: (variable: Variable) => void;
className?: string;
}
export function VariableName({
variable,
onClick,
className,
}: VariableNameProps): JSX.Element {
return (
<span
className={clsx(
"ring-1 ring-inset ring-black-100 px-1 bg-white rounded-md",
onClick && "cursor-pointer",
className
)}
onClick={(e) => {
e.stopPropagation();
onClick?.(variable);
}}
>
{variable}
</span>
);
}

View file

@ -0,0 +1,344 @@
import { TypeDescriptor } from "../../engine/subs";
import {
ClosureType,
RecordFieldKind,
UnspecializedClosureType,
Variable,
} from "../../schema";
type DrawVariable = (variable: Variable) => JSX.Element;
export interface DrawHeadConstructorProps {
desc: TypeDescriptor | undefined;
drawVariablePretty: DrawVariable;
drawVariableRaw: DrawVariable;
}
export default function DrawHeadConstructor({
desc,
drawVariablePretty,
drawVariableRaw,
}: DrawHeadConstructorProps): JSX.Element {
if (!desc) {
return <>???</>;
}
const content = desc.content;
switch (content.type) {
case "Flex":
case "Rigid": {
const { name } = content;
return name ? <>{name}</> : <>_</>;
}
case "FlexAble":
case "RigidAble": {
const { name, abilities } = content;
const nameEl = name ? <>{name}</> : <>_</>;
return (
<>
{nameEl} has {abilities.join(", ")}
</>
);
}
case "Recursive": {
const { name, structure } = content;
const structureEl = drawVariableRaw(structure);
const nameEl = name ? <>{name} to </> : <></>;
return (
<>
&lt;{nameEl}
{structureEl}&gt;
</>
);
}
case "LambdaSet": {
const { ambient_function, solved, unspecialized, recursion_var } =
content;
const ambientFunctionEl = drawVariableRaw(ambient_function);
const solvedEl = (
<DrawSolved drawVariableRaw={drawVariableRaw} solved={solved} />
);
const unspecializedEl = (
<DrawUnspecialized
drawVariableRaw={drawVariableRaw}
unspecialized={unspecialized}
/>
);
const recursionVarEl = recursion_var ? (
<> as &lt;{drawVariableRaw(recursion_var)}&gt;</>
) : (
<></>
);
return (
<>
[{solvedEl}
{unspecializedEl}]{recursionVarEl} ^{ambientFunctionEl}
</>
);
}
case "ErasedLambda": {
return <>?</>;
}
case "Alias": {
const { kind, name, variables } = content;
const prefix = kind.type === "Opaque" ? "@" : "";
const variablesEl = (
<DrawVarArgumentsList
drawVariableRaw={drawVariableRaw}
variables={variables.type_variables}
/>
);
return (
<span>
{prefix}
{sym(name)}
{variablesEl}
</span>
);
}
case "Apply": {
const { symbol, variables } = content;
const variablesEl = (
<DrawVarArgumentsList
drawVariableRaw={drawVariableRaw}
variables={variables}
/>
);
return (
<>
{sym(symbol)}
{variablesEl}
</>
);
}
case "Function": {
const { arguments: args, lambda_type, ret } = content;
const argsEl = args.map((arg, i) => (
<span key={i}>
{i !== 0 ? ", " : ""}
{drawVariablePretty(arg)}
</span>
));
const lambdaTypeEl = drawVariablePretty(lambda_type);
const retEl = drawVariablePretty(ret);
return (
<>
{argsEl}
{" -"}
{lambdaTypeEl}
{"-> "}
{retEl}
</>
);
}
case "Record": {
const { fields, extension } = content;
const fieldsEl = Object.entries(fields).map(([key, value], i) => {
const { field_type, kind } = value;
return (
<span key={i}>
{i !== 0 ? ", " : ""}
{key} {<DrawFieldKind kind={kind} />}{" "}
{drawVariablePretty(field_type)}
</span>
);
});
return (
<>
{"{"}
{fieldsEl}
{"}"}
{drawVariablePretty(extension)}
</>
);
}
case "Tuple": {
const { elements, extension } = content;
const elemsEl = Object.entries(elements).map(([key, value], i) => {
return (
<span key={i}>
{i !== 0 ? ", " : ""}
{key}: {drawVariablePretty(value)}
</span>
);
});
return (
<>
({elemsEl}){drawVariablePretty(extension)}
</>
);
}
case "TagUnion": {
const { tags, extension } = content;
return (
<>
<DrawTags tags={tags} drawVariableRaw={drawVariableRaw} />
{drawVariablePretty(extension.variable)}
</>
);
}
case "RecursiveTagUnion": {
const { tags, extension, recursion_var } = content;
return (
<>
(<DrawTags tags={tags} drawVariableRaw={drawVariableRaw} />
{drawVariablePretty(extension.variable)} as &lt;
{drawVariableRaw(recursion_var)}&gt;)
</>
);
}
case "FunctionOrTagUnion": {
const { functions, tags, extension } = content;
const functionsEl = functions.map((f, i) => (
<span key={i}>
{i !== 0 ? ", " : ""}
{sym(f)}
</span>
));
const tagsEl = tags.map((t, i) => (
<span key={i}>
{i !== 0 ? ", " : ""}
{t}
</span>
));
return (
<>
[{functionsEl} | {tagsEl}]{drawVariablePretty(extension.variable)}
</>
);
}
case "RangedNumber": {
const {
range: { kind, min_width, signed },
} = content;
switch (kind.type) {
case "AnyNum":
return <>{min_width}+</>;
case "Int":
return signed ? <>{min_width}+</> : <>{min_width}+</>;
}
break;
}
case "EmptyRecord": {
return <>{"{}"}</>;
}
case "EmptyTuple": {
return <>()</>;
}
case "EmptyTagUnion": {
return <>[]</>;
}
case "Error": {
return <></>;
}
}
}
function DrawVarArgumentsList({
variables,
drawVariableRaw,
}: {
variables: Variable[];
drawVariableRaw: DrawVariable;
}): JSX.Element {
return variables.length !== 0 ? (
<>
{" "}
{variables.map((v, i) => (
<span key={i}>{drawVariableRaw(v)}</span>
))}
</>
) : (
<></>
);
}
function DrawSolved({
solved,
drawVariableRaw,
}: {
solved: ClosureType[];
drawVariableRaw: DrawVariable;
}): JSX.Element {
const tags = solved.map(({ environment, function: fn }, i) => (
<span key={i}>
{i !== 0 ? ", " : ""}
<DrawTag
tag={sym(fn)}
variables={environment}
drawVariableRaw={drawVariableRaw}
/>
</span>
));
return <>[{tags}]</>;
}
function DrawTags({
tags,
drawVariableRaw,
}: {
tags: Record<string, Variable[]>;
drawVariableRaw: DrawVariable;
}): JSX.Element {
const tagsEl = Object.entries(tags).map(([tag, vars], i) => (
<span key={i}>
{i !== 0 ? ", " : ""}
<DrawTag tag={tag} variables={vars} drawVariableRaw={drawVariableRaw} />
</span>
));
return <>[{tagsEl}]</>;
}
function DrawUnspecialized({
unspecialized,
drawVariableRaw,
}: {
unspecialized: UnspecializedClosureType[];
drawVariableRaw: DrawVariable;
}): JSX.Element {
const unspecs = unspecialized.map(
({ ability_member, lambda_set_region, specialization }, i) => (
<span key={i}>
{" + "}
{drawVariableRaw(specialization)}:{sym(ability_member)}:
{lambda_set_region}
</span>
)
);
return <>{unspecs}</>;
}
function DrawTag({
tag,
variables,
drawVariableRaw,
}: {
tag: string;
variables: Variable[];
drawVariableRaw: DrawVariable;
}): JSX.Element {
return (
<>
{tag}
<DrawVarArgumentsList
drawVariableRaw={drawVariableRaw}
variables={variables}
/>
</>
);
}
function DrawFieldKind({ kind }: { kind: RecordFieldKind }): JSX.Element {
switch (kind.type) {
case "Required":
case "Demanded":
return <>:</>;
case "Optional":
return <>?</>;
}
}
function sym(symbol: string): string {
if (symbol.startsWith("`")) symbol = symbol.slice(1);
if (symbol.endsWith("`")) symbol = symbol.slice(0, -1);
return symbol.split(".").at(-1)!;
}

View file

@ -0,0 +1,68 @@
import { TypeDescriptor } from "../../engine/subs";
import { assertExhaustive } from "../../utils/exhaustive";
export interface ContentStyles {
name: string;
bg: string;
}
export function contentStyles(desc: TypeDescriptor | undefined): ContentStyles {
if (!desc) {
return { name: "???", bg: "bg-red-500" };
}
const content = desc.content;
switch (content.type) {
case "Flex":
return { name: "Flex", bg: "bg-blue-300" };
case "FlexAble":
return { name: "FlexAble", bg: "bg-blue-400" };
case "Rigid":
return { name: "Rigid", bg: "bg-indigo-300" };
case "RigidAble":
return { name: "RigidAble", bg: "bg-indigo-400" };
case "Recursive":
return { name: "Rec", bg: "bg-blue-grey-500" };
case "LambdaSet":
return { name: "LambdaSet", bg: "bg-green-500" };
case "ErasedLambda":
return { name: "ErasedLambda", bg: "bg-green-700" };
case "Alias": {
switch (content.kind.type) {
case "Structural":
return { name: "Alias", bg: "bg-yellow-300" };
case "Opaque":
return { name: "Opaque", bg: "bg-amber-400" };
default:
assertExhaustive(content.kind);
}
break;
}
case "Apply":
return { name: "Apply", bg: "bg-orange-500" };
case "Function":
return { name: "Func", bg: "bg-teal-400" };
case "Record":
return { name: "Record", bg: "bg-purple-400" };
case "Tuple":
return { name: "Tuple", bg: "bg-deep-purple-400" };
case "TagUnion":
return { name: "Tags", bg: "bg-cyan-200" };
case "FunctionOrTagUnion":
return { name: "Func|Tags", bg: "bg-cyan-300" };
case "RecursiveTagUnion":
return { name: "RecTags", bg: "bg-cyan-400" };
case "RangedNumber":
return { name: "", bg: "bg-lime-400" };
case "EmptyRecord":
return { name: "{}", bg: "bg-purple-400" };
case "EmptyTuple":
return { name: "()", bg: "bg-deep-purple-400" };
case "EmptyTagUnion":
return { name: "[]", bg: "bg-cyan-200" };
case "Error":
return { name: "Error", bg: "bg-red-400" };
}
}
export const LinkStyles: ContentStyles = { name: "Link", bg: "bg-slate-500" };

View file

@ -0,0 +1,27 @@
import { EventEpoch } from "../../engine/engine";
import { Variable } from "../../schema";
import { VariableElPretty } from "../Common/Variable";
import { CommonProps } from "./types";
interface VariableProps extends CommonProps {
epoch: EventEpoch;
variable: Variable;
}
export function VariableEl({
engine,
epoch,
variable,
graphEe,
}: VariableProps): JSX.Element {
engine.stepTo(epoch);
return (
<VariableElPretty
variable={variable}
subs={engine.subs}
onClick={(variable: Variable) => {
graphEe.emit("focusVariable", variable);
}}
></VariableElPretty>
);
}

View file

@ -0,0 +1,9 @@
import { TypedEmitter } from "tiny-typed-emitter";
import type { Engine, EventEpoch } from "../../engine/engine";
import { GraphMessage } from "../../utils/events";
export interface CommonProps {
selectedEpochs: EventEpoch[];
engine: Engine;
graphEe: TypedEmitter<GraphMessage>;
}

View file

@ -0,0 +1,76 @@
export interface GroupInfo {
group: string;
groupHover: string;
}
export function depthToGroupInfo(depth: number): GroupInfo {
switch (depth) {
case 0:
return {
group: `group/event-0`,
groupHover: `group-hover/event-0:opacity-100`,
};
case 1:
return {
group: `group/event-1`,
groupHover: `group-hover/event-1:opacity-100`,
};
case 2:
return {
group: `group/event-2`,
groupHover: `group-hover/event-2:opacity-100`,
};
case 3:
return {
group: `group/event-3`,
groupHover: `group-hover/event-3:opacity-100`,
};
case 4:
return {
group: `group/event-4`,
groupHover: `group-hover/event-4:opacity-100`,
};
case 5:
return {
group: `group/event-5`,
groupHover: `group-hover/event-5:opacity-100`,
};
case 6:
return {
group: `group/event-6`,
groupHover: `group-hover/event-6:opacity-100`,
};
case 7:
return {
group: `group/event-7`,
groupHover: `group-hover/event-7:opacity-100`,
};
case 8:
return {
group: `group/event-8`,
groupHover: `group-hover/event-8:opacity-100`,
};
case 9:
return {
group: `group/event-9`,
groupHover: `group-hover/event-9:opacity-100`,
};
case 10:
return {
group: `group/event-10`,
groupHover: `group-hover/event-10:opacity-100`,
};
case 11:
return {
group: `group/event-11`,
groupHover: `group-hover/event-11:opacity-100`,
};
case 12:
return {
group: `group/event-12`,
groupHover: `group-hover/event-12:opacity-100`,
};
default:
throw new Error(`Too deep: ${depth}`);
}
}

View file

@ -0,0 +1,423 @@
import clsx from "clsx";
import React, { useCallback, useMemo, useState } from "react";
import { TypedEmitter } from "tiny-typed-emitter";
import { EventEpoch } from "../../engine/engine";
import { lastSubEvent } from "../../engine/event_util";
import { useFocusOutlineEvent } from "../../hooks/useFocusOutlineEvent";
import { UnificationMode, Event } from "../../schema";
import {
EventListMessage,
GlobalMessage,
GraphMessage,
LoadEpochView,
} from "../../utils/events";
import { Refine } from "../../utils/refine";
import EpochCell from "../Common/EpochCell";
import { CommonProps } from "../EventItem/types";
import { VariableEl } from "../EventItem/Variable";
import { depthToGroupInfo } from "./depthGroup";
interface EventListProps extends CommonProps {
events: Event[];
eventListEe: TypedEmitter<EventListMessage>;
globalEe: TypedEmitter<GlobalMessage>;
depth: number;
}
const MT = "my-2.5";
const LOWER_OPACITY = "opacity-40";
export default function EventList(props: EventListProps): JSX.Element {
const { events, depth } = props;
return (
<ul className={clsx(MT, "space-y-2.5", depth === 0 ? "" : "ml-[1em]")}>
{events.map((event, i) => (
<li key={i}>
<OneEvent {...props} event={event} />
</li>
))}
</ul>
);
}
interface OneEventProps extends CommonProps {
event: Event;
eventListEe: TypedEmitter<EventListMessage>;
globalEe: TypedEmitter<GlobalMessage>;
depth: number;
}
function OneEvent(props: OneEventProps): JSX.Element {
const { event } = props;
switch (event.type) {
case "Unification":
return <UnificationEvent {...props} event={event} />;
case "VariableUnified":
return <></>;
case "VariableSetDescriptor":
return <></>;
}
}
const DROPDOWN_CLOSED = "▶";
const DROPDOWN_OPEN = "▼";
const UN_UNKNOWN = "💭";
const UN_SUCCESS = "✅";
const UN_FAILURE = "❌";
function epochInRange(
epoch: EventEpoch,
[start, end]: [EventEpoch, EventEpoch]
): boolean {
return epoch >= start && epoch <= end;
}
interface UnificationProps extends CommonProps {
event: Refine<Event, "Unification">;
eventListEe: TypedEmitter<EventListMessage>;
globalEe: TypedEmitter<GlobalMessage>;
depth: number;
}
const COL_1_P = "pl-1.5";
const COL_1_OUTLINE_STYLES = clsx(COL_1_P, "outline-event-col-1");
const COL_3_OUTLINE_STYLES = clsx("outline-event-col-3");
const COL_1_ROUNDED = "rounded-l-md";
const COL_3_ROUNDED = "rounded-r-md";
const UN_EXPANDED_OUTLINE_STYLES = clsx("ring-inset ring-2 ring-blue-500");
const TRANSITION_SHADOW = "transition-shadow ease-in-out duration-500";
const TRANSITION_OPACITY = "transition-opacity ease-in-out duration-150";
const GROUP_STLYES = "relative overflow-hidden";
// Space for the hover cells at the end of line.
const EOL_SPACE = "pr-12";
function UnificationEvent(props: UnificationProps): JSX.Element {
const {
engine,
event,
selectedEpochs,
graphEe,
eventListEe,
depth,
globalEe,
} = props;
const { mode, subevents, success } = event;
const beforeUnificationEpoch = engine.getEventIndex(event);
const afterUnificationEpoch = engine.getEventIndex(lastSubEvent(event));
const containedEpoch = selectedEpochs.find((epoch) =>
epochInRange(epoch, [beforeUnificationEpoch, afterUnificationEpoch])
);
const leftVar = useMemo(
() => (epoch: EventEpoch) =>
<VariableEl {...props} epoch={epoch} variable={event.left} />,
[event.left, props]
);
const rightVar = useMemo(
() => (epoch: EventEpoch) =>
<VariableEl {...props} epoch={epoch} variable={event.right} />,
[event.right, props]
);
const [isOpen, setIsOpen] = useState(false);
const isOutlined = useFocusOutlineEvent<"focusEpoch", EventEpoch | undefined>(
{
ee: eventListEe,
value: containedEpoch,
event: "focusEpoch",
}
);
const modeIcon = useMemo(() => <UnificationModeIcon mode={mode} />, [mode]);
const resultIcon = success ? UN_SUCCESS : UN_FAILURE;
const resultHeadline = <Headline icon={resultIcon}></Headline>;
const epochCell = useMemo(() => {
if (containedEpoch === undefined) return null;
return <EventListEpochCell epoch={containedEpoch} graphEe={graphEe} />;
}, [containedEpoch, graphEe]);
const getBeforeUnificationHeadline = useCallback(
({
epoch,
collapsedMode,
}: {
epoch: EventEpoch;
collapsedMode?: boolean;
}) => {
const topHeadline = (
<Headline icon={isOpen ? UN_UNKNOWN : resultIcon}></Headline>
);
const optEpochCell =
collapsedMode && containedEpoch !== undefined && epochCell;
return (
<button
onClick={() => setIsOpen(!isOpen)}
className={clsx(
"w-full text-left whitespace-nowrap h-full overflow-scroll",
collapsedMode && COL_1_P
)}
>
<span className={EOL_SPACE}>
{optEpochCell}
<span
className={clsx(
"mr-2",
isOpen ? "text-slate-500" : "text-slate-400"
)}
>
{isOpen ? DROPDOWN_OPEN : DROPDOWN_CLOSED}
</span>
{topHeadline} {leftVar(epoch)} {modeIcon} {rightVar(epoch)}
</span>
</button>
);
},
[isOpen, resultIcon, containedEpoch, epochCell, leftVar, modeIcon, rightVar]
);
const { group, groupHover } = depthToGroupInfo(depth);
if (!isOpen) {
const headLine = getBeforeUnificationHeadline({
epoch: afterUnificationEpoch,
collapsedMode: true,
});
return (
<div
className={clsx(
"rounded-md",
TRANSITION_SHADOW,
group,
GROUP_STLYES,
containedEpoch !== undefined &&
isOutlined &&
UN_EXPANDED_OUTLINE_STYLES
)}
>
<div
className={clsx(
TRANSITION_OPACITY,
containedEpoch === undefined && LOWER_OPACITY
)}
>
{headLine}
</div>
<LoadEpochGraphLauncher
groupHover={groupHover}
epoch={afterUnificationEpoch}
globalEe={globalEe}
className="bottom-0 right-2"
/>
</div>
);
} else {
const beforeIsCurrentEpoch = beforeUnificationEpoch === containedEpoch;
const afterIsCurrentEpoch = afterUnificationEpoch === containedEpoch;
const epochCellBefore = beforeIsCurrentEpoch && epochCell;
const epochCellAfter = afterIsCurrentEpoch && epochCell;
const outlineEpochCellAfter = afterIsCurrentEpoch && isOutlined;
const outlineEpochCellBefore = beforeIsCurrentEpoch && isOutlined;
const headlineBefore = getBeforeUnificationHeadline({
epoch: beforeUnificationEpoch,
});
const dropdownTransparent = (
<span className="text-transparent mr-2">{DROPDOWN_OPEN}</span>
);
const headlineAfter = (
<div className={clsx("whitespace-nowrap")}>
{dropdownTransparent}
{resultHeadline} {leftVar(afterUnificationEpoch)} {modeIcon}{" "}
{rightVar(afterUnificationEpoch)}
</div>
);
return (
<div className={clsx(group, GROUP_STLYES)}>
<div
className={clsx(
"grid gap-0 grid-cols-[min-content_min-content_1fr_auto] opacity-100",
"overflow-scroll",
TRANSITION_OPACITY
)}
>
{/* Row 1: unification start */}
<div
className={clsx(
"row-start-1 col-start-1",
TRANSITION_SHADOW,
COL_1_ROUNDED,
outlineEpochCellBefore ? COL_1_OUTLINE_STYLES : COL_1_P
)}
>
{epochCellBefore}
</div>
<div
className={clsx(
"row-start-1 col-start-3 col-end-5",
TRANSITION_SHADOW,
COL_3_ROUNDED,
outlineEpochCellBefore && COL_3_OUTLINE_STYLES,
EOL_SPACE
)}
>
{headlineBefore}
</div>
{/* Row 2: inner traces */}
<div className={clsx("row-start-2 col-start-1")}></div>
<div className={clsx("row-start-2col-start-3 col-end-4", "w-full")}>
<EventList
{...props}
depth={depth + 1}
engine={engine}
events={subevents}
/>
</div>
{/* Row 3: unification end */}
<div
className={clsx(
"row-start-3 col-start-1",
TRANSITION_SHADOW,
COL_1_ROUNDED,
outlineEpochCellAfter ? COL_1_OUTLINE_STYLES : COL_1_P
)}
>
{epochCellAfter}
</div>
<div
className={clsx(
"row-start-3 col-start-3 col-end-5",
TRANSITION_SHADOW,
COL_3_ROUNDED,
outlineEpochCellAfter && COL_3_OUTLINE_STYLES,
EOL_SPACE
)}
>
{headlineAfter}
</div>
{/* Col 2: dropdown line */}
<div
className={clsx(
"row-start-1 row-end-4 col-start-2 h-full",
"relative z-[1] h-full",
"before:content-[''] before:border-l before:border-slate-500 before:z-[-1]",
"before:absolute before:w-0 before:h-[calc(100%-1.5rem)] before:top-[1rem] before:left-[0.3rem]"
)}
></div>
</div>
<LoadEpochGraphLauncher
groupHover={groupHover}
epoch={beforeUnificationEpoch}
globalEe={globalEe}
className="top-0 right-2"
/>
<LoadEpochGraphLauncher
groupHover={groupHover}
epoch={afterUnificationEpoch}
globalEe={globalEe}
className="bottom-0 right-2"
/>
</div>
);
}
}
function Headline({ icon }: { icon: string }): JSX.Element {
return (
<div className="inline-block align-middle">
<div className="">{icon}</div>
</div>
);
}
function UnificationModeIcon({ mode }: { mode: UnificationMode }): JSX.Element {
switch (mode.type) {
case "Eq":
return <>~</>;
case "Present":
return <>+=</>;
case "LambdaSetSpecialization":
return <>|~|</>;
}
}
interface EventListEpochCellProps {
epoch: EventEpoch;
graphEe: TypedEmitter<GraphMessage>;
}
function EventListEpochCell({
epoch,
graphEe,
}: EventListEpochCellProps): JSX.Element {
return (
<span
id={`events-${epoch}`}
className={clsx("cursor-pointer rounded-md")}
onClick={(e) => {
e.stopPropagation();
graphEe.emit("focusEpoch", epoch);
}}
>
<EpochCell>{epoch}</EpochCell>
</span>
);
}
interface LoadEpochGraphLauncherProps {
groupHover: string;
epoch: EventEpoch;
globalEe: TypedEmitter<GlobalMessage>;
className?: string;
}
function LoadEpochGraphLauncher({
groupHover,
epoch,
className,
globalEe,
}: LoadEpochGraphLauncherProps): JSX.Element {
return (
<div className={clsx("absolute opacity-0", groupHover, className)}>
<span className="space-x-0.5 bg-gray-200 ring-1 ring-slate-300 rounded-sm px-1 opacity-80 hover:opacity-100">
<span
className="text-blue-400 hover:text-blue-500 cursor-pointer"
onClick={() => {
globalEe.emit("loadEpoch", epoch, LoadEpochView.Top);
}}
>
</span>
<span
className="text-blue-400 hover:text-blue-500 cursor-pointer"
onClick={() => {
globalEe.emit("loadEpoch", epoch, LoadEpochView.Bot);
}}
>
</span>
</span>
</div>
);
}

View file

@ -0,0 +1,48 @@
import { AllEvents } from "../schema";
export type EventsOk = {
kind: "ok";
events: AllEvents;
};
export type EventsErr = {
kind: "err";
error: string;
};
export type LoadedEvents = EventsOk | EventsErr;
interface FileInputProps {
setResult(result: LoadedEvents): void;
}
export default function FileInput({ setResult }: FileInputProps) {
async function setFile(e: React.ChangeEvent<HTMLInputElement>) {
e.preventDefault();
const files = e.target.files;
if (!files) {
setResult({ kind: "err", error: "Please choose a checkmate file." });
return;
}
const file = files[0];
const buf = await file.text();
try {
const events: AllEvents = JSON.parse(buf);
setResult({ kind: "ok", events });
} catch (e) {
setResult({ kind: "err", error: "Invalid checkmate file." });
return;
}
}
return (
<input
type="file"
name="small-file-input"
id="small-file-input"
onChange={(e) => setFile(e)}
className="block w-full border border-gray-200 shadow-sm rounded-md text-sm
file:bg-roc-purple-bg file:border-0 file:mr-4 file:py-2 file:px-4 cursor-pointer"
></input>
);
}

View file

@ -0,0 +1,286 @@
import clsx from "clsx";
import { Handle, Position } from "reactflow";
import { Variable } from "../../schema";
import { assertExhaustive } from "../../utils/exhaustive";
import { contentStyles, LinkStyles } from "../Content";
import { VariableElPretty } from "../Common/Variable";
import { SubsSnapshot, TypeDescriptor } from "../../engine/subs";
import { TypedEmitter } from "tiny-typed-emitter";
import { VariableLink } from "../Common/VariableLink";
import { VariableMessage } from "../../utils/events";
import { useFocusOutlineEvent } from "../../hooks/useFocusOutlineEvent";
import { UnknownVariable } from "../Common/UnknownVariable";
type AddSubVariableLink = ({
from,
variable,
}: {
from: Variable;
variable: Variable;
}) => void;
export interface VariableNodeProps {
data: {
subs: SubsSnapshot;
rawVariable: Variable;
addSubVariableLink: AddSubVariableLink;
isOutlined: boolean;
ee: TypedEmitter<VariableMessage>;
};
targetPosition?: Position;
sourcePosition?: Position;
}
export default function VariableNode({
data,
targetPosition,
sourcePosition,
}: VariableNodeProps): JSX.Element {
const {
subs,
rawVariable,
addSubVariableLink,
isOutlined: isOutlinedProp,
ee: eeProp,
} = data;
const isOutlined = useFocusOutlineEvent({
ee: eeProp,
value: rawVariable,
event: "focus",
defaultIsOutlined: isOutlinedProp,
});
const varType = subs.get(rawVariable);
let renderContent: JSX.Element;
let bgStyles: string;
const isContent = varType?.type === "descriptor";
switch (varType?.type) {
case undefined: {
bgStyles = "bg-red-500";
renderContent = <UnknownVariable variable={rawVariable} />;
break;
}
case "link": {
bgStyles = LinkStyles.bg;
renderContent = (
<VariableLink
subs={subs}
variable={rawVariable}
onClick={() =>
addSubVariableLink({
from: rawVariable,
variable: subs.get_root_key(rawVariable),
})
}
/>
);
break;
}
case "descriptor": {
const variable = rawVariable;
const desc: TypeDescriptor = varType;
const styles = contentStyles(desc);
bgStyles = styles.bg;
const basis: BasisProps = {
subs,
origin: variable,
addSubVariableLink,
};
const content = Object.entries(
VariableNodeContent(variable, desc, basis)
).filter((el): el is [string, JSX.Element] => !!el[1]);
let expandedContent = <></>;
if (content.length > 0) {
expandedContent = (
<ul className="text-sm text-left mt-2 space-y-1">
{content.map(([key, value], i) => (
<li key={i} className="space-x-2">
{key}: {value}
</li>
))}
</ul>
);
}
renderContent = (
<>
<div>
<VariableElPretty variable={variable} subs={subs} />
</div>
{expandedContent}
</>
);
break;
}
default: {
assertExhaustive(varType);
}
}
return (
<div
className={clsx(
bgStyles,
"bg-opacity-50 rounded-md transition ease-in-out duration-700",
isContent ? "py-2 px-4 border" : "p-0",
isOutlined && "ring-2 ring-blue-500",
"text-center font-mono"
)}
>
<Handle
type="target"
position={targetPosition ?? Position.Top}
isConnectable={false}
style={{ background: "transparent", border: "none" }}
/>
{renderContent}
<Handle
type="source"
position={sourcePosition ?? Position.Bottom}
isConnectable={false}
style={{ background: "transparent", border: "none" }}
/>
</div>
);
}
function VariableNodeContent(
variable: Variable,
desc: TypeDescriptor | undefined,
basis: BasisProps
): Record<string, JSX.Element | null> {
if (!desc) return {};
const { content } = desc;
switch (content.type) {
case "Flex":
case "Rigid": {
const { name } = content;
return { name: name ? <>{name}</> : null };
}
case "FlexAble":
case "RigidAble": {
const { name, abilities } = content;
return {
name: <>{name}</>,
abilities: <>[{abilities.join(", ")}]</>,
};
}
case "Recursive": {
const { name, structure } = content;
return {
name: <>{name}</>,
structure: <SubVariable {...basis} variable={structure} />,
};
}
case "LambdaSet": {
const { ambient_function, solved, unspecialized, recursion_var } =
content;
return {
"^": <SubVariable {...basis} variable={ambient_function} />,
as: recursion_var ? (
<SubVariable {...basis} variable={recursion_var} />
) : null,
};
}
case "ErasedLambda": {
return {};
}
case "Alias": {
const { name, real_variable, variables } = content;
return {
name: <>{name}</>,
};
}
case "Apply": {
const { name, variables } = content;
return {
name: <>{name}</>,
};
}
case "Function": {
const { arguments: args, lambda_type, ret } = content;
return {
args: (
<>
{args.map((arg, i) => (
<SubVariable key={i} {...basis} variable={arg} />
))}
</>
),
"||": <SubVariable {...basis} variable={lambda_type} />,
ret: <SubVariable {...basis} variable={ret} />,
};
}
case "FunctionOrTagUnion": {
const { tags, functions, extension } = content;
return {
tags: <>[{tags.join(", ")}]</>,
fns: <>[{functions.join(", ")}]</>,
};
}
case "TagUnion": {
const { tags, extension } = content;
return {};
}
case "RecursiveTagUnion": {
const { recursion_var, extension, tags } = content;
return {
as: <SubVariable {...basis} variable={recursion_var} />,
};
}
case "Record": {
const { fields, extension } = content;
return {};
}
case "Tuple": {
const { elements, extension } = content;
return {};
}
case "RangedNumber": {
const { range } = content;
return {};
}
case "EmptyRecord":
case "EmptyTuple":
case "EmptyTagUnion":
case "Error": {
return {};
}
default: {
return assertExhaustive(content);
}
}
}
interface BasisProps {
subs: SubsSnapshot;
origin: Variable;
addSubVariableLink: AddSubVariableLink;
}
function SubVariable({
subs,
origin,
variable,
addSubVariableLink,
}: {
variable: Variable;
} & BasisProps): JSX.Element {
return (
<VariableElPretty
variable={variable}
subs={subs}
onClick={() => addSubVariableLink({ from: origin, variable })}
/>
);
}

View file

@ -0,0 +1,567 @@
import ELK, {
type ElkNode,
type LayoutOptions,
} from "elkjs/lib/elk.bundled.js";
import ReactFlow, {
Node,
Edge,
Background,
BackgroundVariant,
useReactFlow,
ReactFlowProvider,
NodeChange,
applyNodeChanges,
EdgeChange,
applyEdgeChanges,
Panel,
NodeTypes,
useStore,
ReactFlowState,
Position,
MarkerType,
EdgeMarkerType,
} from "reactflow";
import { useCallback, useEffect, useRef, useState } from "react";
import { Variable } from "../../schema";
import "reactflow/dist/style.css";
import clsx from "clsx";
import VariableNode, { VariableNodeProps } from "./VariableNode";
import { SubsSnapshot } from "../../engine/subs";
import { TypedEmitter } from "tiny-typed-emitter";
import EpochCell from "../Common/EpochCell";
import { HashLink } from "react-router-hash-link";
import {
EventListMessage,
GraphMessage,
VariableMessage,
} from "../../utils/events";
import { useFocusOutlineEvent } from "../../hooks/useFocusOutlineEvent";
export interface VariablesGraphProps {
subs: SubsSnapshot;
graphEe: TypedEmitter<GraphMessage>;
eventListEe: TypedEmitter<EventListMessage>;
}
function horizontalityToPositions(isHorizontal: boolean): {
targetPosition: Position;
sourcePosition: Position;
} {
return {
targetPosition: isHorizontal ? Position.Left : Position.Top,
sourcePosition: isHorizontal ? Position.Right : Position.Bottom,
};
}
interface LayoutedElements {
nodes: Node[];
edges: Edge[];
}
const LAYOUT_CONFIG_DOWN = {
keypress: "j",
emoji: "⬇️",
elkLayoutOptions: {
"elk.algorithm": "layered",
"elk.direction": "DOWN",
},
isHorizontal: false,
} as const;
const LAYOUT_CONFIG_RIGHT = {
keypress: "l",
emoji: "➡️",
elkLayoutOptions: {
"elk.algorithm": "layered",
"elk.direction": "RIGHT",
},
isHorizontal: true,
} as const;
const LAYOUT_CONFIG_RADIAL = {
keypress: "r",
emoji: "🌐",
elkLayoutOptions: {
"elk.algorithm": "radial",
},
isHorizontal: false,
} as const;
const LAYOUT_CONFIG_FORCE = {
keypress: "f",
emoji: "🧲",
elkLayoutOptions: {
"elk.algorithm": "force",
},
isHorizontal: false,
} as const;
type LayoutConfiguration =
| typeof LAYOUT_CONFIG_DOWN
| typeof LAYOUT_CONFIG_RIGHT
| typeof LAYOUT_CONFIG_RADIAL
| typeof LAYOUT_CONFIG_FORCE;
const LAYOUT_CONFIGURATIONS: LayoutConfiguration[] = [
LAYOUT_CONFIG_DOWN,
LAYOUT_CONFIG_RIGHT,
LAYOUT_CONFIG_RADIAL,
LAYOUT_CONFIG_FORCE,
];
type ComputeElkLayoutOptions = Pick<
LayoutConfiguration,
"elkLayoutOptions" | "isHorizontal"
>;
interface ComputeLayoutedElementsProps
extends LayoutedElements,
ComputeElkLayoutOptions {}
// Elk has a *huge* amount of options to configure. To see everything you can
// tweak check out:
//
// - https://www.eclipse.org/elk/reference/algorithms.html
// - https://www.eclipse.org/elk/reference/options.html
const baseElkOptions: LayoutOptions = {
"elk.layered.spacing.nodeNodeBetweenLayers": "100",
"elk.spacing.nodeNode": "80",
};
async function computeLayoutedElements({
nodes,
edges,
elkLayoutOptions,
isHorizontal,
}: ComputeLayoutedElementsProps): Promise<LayoutedElements> {
if (nodes.length === 0) {
return Promise.resolve({
nodes: [],
edges: [],
});
}
const elk = new ELK();
const graph: ElkNode = {
id: "root",
layoutOptions: {
...baseElkOptions,
...elkLayoutOptions,
},
//@ts-ignore
children: nodes.map((node) => ({
...node,
// Adjust the target and source handle positions based on the layout
// direction.
targetPosition: isHorizontal ? "left" : "top",
sourcePosition: isHorizontal ? "right" : "bottom",
// Hardcode a width and height for elk to use when layouting.
//width: 150,
//height: 50,
})),
//@ts-ignore
edges: edges.map((edge) => ({
...edge,
})),
};
const layoutedGraph = await elk.layout(graph);
if (!layoutedGraph.children || !layoutedGraph.edges) {
throw new Error("Elk did not return a valid graph");
}
return {
//@ts-ignore
nodes: layoutedGraph.children.map((node) => ({
...node,
// React Flow expects a position property on the node instead of `x`
// and `y` fields.
position: { x: node.x, y: node.y },
})),
//@ts-ignore
edges: layoutedGraph.edges,
};
}
const NODE_TYPES: NodeTypes = {
variable: VariableNode,
};
type VariableNodeData = VariableNodeProps["data"];
type RFVariableNode = Node<VariableNodeData>;
function newVariable(
id: string,
data: VariableNodeData,
isHorizontal: boolean
): RFVariableNode {
return {
id,
position: { x: 0, y: 0 },
type: "variable",
data,
...horizontalityToPositions(isHorizontal),
};
}
function canAddVariable(variableName: string, existingNodes: Node[]): boolean {
return !existingNodes.some((n) => n.id === variableName);
}
function canAddEdge(edgeName: string, existingEdges: Edge[]): boolean {
return !existingEdges.some((e) => e.id === edgeName);
}
function addNode(node: Node): NodeChange {
return {
type: "add",
item: node,
};
}
function addEdge(edge: Edge): EdgeChange {
return {
type: "add",
item: edge,
};
}
// Auto-layout logic due in part to the `feldera/dbsp` project, licensed under
// the MIT license.
//
// The source code for the original project can be found at
// https://github.com/feldera/dbsp/blob/585a1926a6d3a0f8176dc80db5e906ec7d095400/web-ui/src/streaming/builder/hooks/useAutoLayout.ts#L215
// and its license at
// https://github.com/feldera/dbsp/blob/585a1926a6d3a0f8176dc80db5e906ec7d095400/LICENSE
const nodeCountSelector = (state: ReactFlowState) =>
state.nodeInternals.size + state.edges.length;
const nodesSetInViewSelector = (state: ReactFlowState) =>
Array.from(state.nodeInternals.values()).every(
(node) => node.width && node.height
);
type RedoLayoutFn = () => Promise<void>;
function useRedoLayout(options: ComputeElkLayoutOptions): RedoLayoutFn {
const nodeCount = useStore(nodeCountSelector);
const nodesInitialized = useStore(nodesSetInViewSelector);
const { getNodes, setNodes, getEdges } = useReactFlow();
const instance = useReactFlow();
return useCallback(async () => {
if (!nodeCount || !nodesInitialized) {
return;
}
const { nodes } = await computeLayoutedElements({
nodes: getNodes(),
edges: getEdges(),
...options,
});
setNodes(nodes);
window.requestAnimationFrame(() => {
instance.fitView();
});
}, [
nodeCount,
nodesInitialized,
getNodes,
getEdges,
options,
setNodes,
instance,
]);
}
// Does positioning of the nodes in the graph.
function useAutoLayout(options: ComputeElkLayoutOptions) {
const redoLayout = useRedoLayout(options);
useEffect(() => {
// This wrapping is of course redundant, but exercised for the purpose of
// explicitness.
async function inner() {
await redoLayout();
}
inner();
}, [redoLayout]);
}
function useKeydown({
layoutConfig,
setLayoutConfig,
graphEe,
}: {
layoutConfig: LayoutConfiguration;
setLayoutConfig: React.Dispatch<React.SetStateAction<LayoutConfiguration>>;
graphEe: TypedEmitter<GraphMessage>;
}) {
const redoLayout = useRedoLayout(layoutConfig);
const keyDownHandler = useCallback(
async (key: string) => {
switch (key) {
case "c": {
await redoLayout();
return;
}
default: {
break;
}
}
for (const config of LAYOUT_CONFIGURATIONS) {
if (key === config.keypress) {
setLayoutConfig(config);
return;
}
}
},
[redoLayout, setLayoutConfig]
);
graphEe.on("keydown", async (key) => await keyDownHandler(key));
}
function Graph({
subs,
graphEe,
eventListEe,
}: VariablesGraphProps): JSX.Element {
const instance = useReactFlow();
// We need to reset the graph when the subs snapshot changes. I'm not sure
// why this isn't done by the existing state manager.
useEffect(() => {
instance.setNodes([]);
instance.setEdges([]);
}, [instance, subs.epoch]);
const varEe = useRef(new TypedEmitter<VariableMessage>());
// Allow an unbounded number of listeners since we attach a listener for each
// variable.
varEe.current.setMaxListeners(Infinity);
const isOutlined = useFocusOutlineEvent({
ee: graphEe,
value: subs.epoch,
event: "focusEpoch",
});
const [layoutConfig, setLayoutConfig] =
useState<LayoutConfiguration>(LAYOUT_CONFIG_DOWN);
const [elements, setElements] = useState<LayoutedElements>({
nodes: [],
edges: [],
});
const [variablesNeedingFocus, setVariablesNeedingFocus] = useState<
Set<Variable>
>(new Set());
useEffect(() => {
if (variablesNeedingFocus.size === 0) {
return;
}
for (const variable of variablesNeedingFocus) {
varEe.current.emit("focus", variable);
}
setVariablesNeedingFocus(new Set());
}, [variablesNeedingFocus]);
useAutoLayout(layoutConfig);
useKeydown({
layoutConfig,
setLayoutConfig,
graphEe,
});
const onNodesChange = useCallback((changes: NodeChange[]) => {
setElements(({ nodes, edges }) => {
return {
nodes: applyNodeChanges(changes, nodes),
edges,
};
});
}, []);
const onEdgesChange = useCallback((changes: EdgeChange[]) => {
setElements(({ nodes, edges }) => {
return {
nodes,
edges: applyEdgeChanges(changes, edges),
};
});
}, []);
interface AddNewVariableParams {
from?: Variable;
variable: Variable;
}
const addNewVariable = useCallback(
({ from, variable }: AddNewVariableParams) => {
const variablesToFocus = new Set<Variable>();
setElements(({ nodes, edges }) => {
let fromVariable: Variable | undefined = from;
let toVariable: Variable | undefined = variable;
const nodeChanges: NodeChange[] = [];
const edgeChanges: EdgeChange[] = [];
while (toVariable !== undefined) {
const toVariableName = toVariable.toString();
if (canAddVariable(toVariableName, nodes)) {
const newVariableNode = newVariable(
toVariable.toString(),
{
subs,
rawVariable: toVariable,
addSubVariableLink: addNewVariable,
isOutlined: true,
ee: varEe.current,
},
layoutConfig.isHorizontal
);
nodeChanges.push(addNode(newVariableNode));
}
if (fromVariable !== undefined) {
const edgeName = `${fromVariable}->${toVariable}`;
if (canAddEdge(edgeName, edges)) {
let markerEnd: EdgeMarkerType | undefined;
if (subs.get_root_key(fromVariable) === toVariable) {
markerEnd = {
type: MarkerType.ArrowClosed,
width: 20,
height: 20,
};
}
const newEdge = addEdge({
id: `${fromVariable}->${toVariable}`,
source: fromVariable.toString(),
target: toVariableName,
markerEnd,
});
edgeChanges.push(newEdge);
}
}
variablesToFocus.add(toVariable);
fromVariable = toVariable;
const rootToVariable = subs.get_root_key(toVariable);
if (toVariable !== rootToVariable) {
toVariable = rootToVariable;
} else {
toVariable = undefined;
}
}
const newNodes = applyNodeChanges(nodeChanges, nodes);
const newEdges = applyEdgeChanges(edgeChanges, edges);
return { nodes: newNodes, edges: newEdges };
});
setVariablesNeedingFocus(variablesToFocus);
},
[layoutConfig.isHorizontal, subs]
);
const addNewVariableNode = useCallback(
(variable: Variable) => {
addNewVariable({ variable });
},
[addNewVariable]
);
graphEe.on("focusVariable", addNewVariableNode);
return (
<ReactFlow
nodes={elements.nodes}
edges={elements.edges}
onNodesChange={(e) => onNodesChange(e)}
onEdgesChange={(e) => onEdgesChange(e)}
fitView
nodesDraggable
nodesConnectable={false}
nodeTypes={NODE_TYPES}
proOptions={{
// https://reactflow.dev/docs/guides/remove-attribution/
hideAttribution: true,
}}
className={clsx(
"ring-inset rounded-md transition ease-in-out duration-700",
isOutlined && "ring-2 ring-blue-500"
)}
>
<Panel position="top-left">
<HashLink
smooth
to={`#events-${subs.epoch}`}
onClick={() => {
eventListEe.emit("focusEpoch", subs.epoch);
}}
>
<EpochCell>Epoch {subs.epoch}</EpochCell>
</HashLink>
</Panel>
<Panel position="top-right">
<LayoutPanel
layoutConfig={layoutConfig}
setLayoutConfig={setLayoutConfig}
/>
</Panel>
<Background variant={BackgroundVariant.Dots} />
</ReactFlow>
);
}
interface LayoutPanelProps {
layoutConfig: LayoutConfiguration;
setLayoutConfig: React.Dispatch<React.SetStateAction<LayoutConfiguration>>;
}
function LayoutPanel({
layoutConfig,
setLayoutConfig,
}: LayoutPanelProps): JSX.Element {
const commonStyle = "rounded cursor-pointer text-2xl select-none";
return (
<>
{LAYOUT_CONFIGURATIONS.map((config, i) => (
<span
key={i}
className={clsx(
commonStyle,
i !== 0 ? "ml-2" : "",
config !== layoutConfig ? "opacity-50" : ""
)}
onClick={() => {
setLayoutConfig(config);
}}
>
{config.emoji}
</span>
))}
</>
);
}
export default function VariablesGraph({
subs,
graphEe,
eventListEe,
}: VariablesGraphProps) {
return (
<ReactFlowProvider>
<Graph subs={subs} graphEe={graphEe} eventListEe={eventListEe} />
</ReactFlowProvider>
);
}

View file

@ -0,0 +1,111 @@
import React, { useEffect, useState } from "react";
import { AllEvents } from "../schema";
import { Engine, EventEpoch } from "../engine/engine";
import EventList from "./EventList/index";
import VariablesGraph from "./Graph/VariablesGraph";
import { TypedEmitter } from "tiny-typed-emitter";
import {
EventListMessage,
GlobalMessage,
GraphMessage,
LoadEpochView,
} from "../utils/events";
import { assertExhaustive } from "../utils/exhaustive";
import { SubsSnapshot } from "../engine/subs";
interface UiProps {
events: AllEvents;
}
export default function Ui({ events }: UiProps): JSX.Element {
const engine = React.useRef(new Engine(events));
const graphEe = React.useRef(new TypedEmitter<GraphMessage>());
const eventListEe = React.useRef(new TypedEmitter<EventListMessage>());
const globalEe = React.useRef(new TypedEmitter<GlobalMessage>());
const [subsTop, setSubsTop] = useState<SubsSnapshot | undefined>(undefined);
const [subsBot, setSubsBot] = useState<SubsSnapshot | undefined>(undefined);
useEffect(() => {
globalEe.current.on("loadEpoch", (epoch, view) => {
switch (view) {
case LoadEpochView.Top: {
setSubsTop(engine.current.stepToSnapshot(epoch));
break;
}
case LoadEpochView.Bot: {
setSubsBot(engine.current.stepToSnapshot(epoch));
break;
}
default:
assertExhaustive(view);
}
});
}, []);
const selectedEpochs = [subsTop?.epoch, subsBot?.epoch]
.filter((x): x is EventEpoch => x !== undefined)
.sort();
return (
<div
className="flex flex-col md:flex-row gap-0 w-full h-full"
onKeyDown={(e) => {
graphEe.current.emit("keydown", e.key);
}}
>
<div className="font-mono mt-2 text-lg md:flex-1 overflow-x-hidden overflow-y-scroll">
<EventList
engine={engine.current}
depth={0}
events={events}
graphEe={graphEe.current}
eventListEe={eventListEe.current}
globalEe={globalEe.current}
selectedEpochs={selectedEpochs}
/>
</div>
<div className="flex-1 min-h-[50%] h-full flex flex-col place-content-center shadow">
{selectedEpochs.length === 0 && (
<span className="text-center">
<span className="p-2 border rounded-md bg-gray-200 inline-block">
Select an event to view.
</span>
</span>
)}
{subsTop !== undefined && (
<VariablesGraphView
subs={subsTop}
graphEe={graphEe.current}
eventListEe={eventListEe.current}
/>
)}
{/* */}
{subsBot !== undefined && (
<VariablesGraphView
subs={subsBot}
graphEe={graphEe.current}
eventListEe={eventListEe.current}
/>
)}
</div>
</div>
);
}
interface VariablesGraphViewProps {
subs: SubsSnapshot;
graphEe: TypedEmitter<GraphMessage>;
eventListEe: TypedEmitter<EventListMessage>;
}
function VariablesGraphView({
subs,
graphEe,
eventListEe,
}: VariablesGraphViewProps): JSX.Element {
return (
<VariablesGraph subs={subs} graphEe={graphEe} eventListEe={eventListEe} />
);
}

View file

@ -0,0 +1,162 @@
import { Event, Variable } from "../schema";
import { assertExhaustive } from "../utils/exhaustive";
import {
ChangeEvent,
makeDeleteVariable,
makeRevertVariable,
RollbackChange,
Subs,
SubsSnapshot,
} from "./subs";
export type EventEpoch = number & { __eventIndex: never };
function* flattenEvents(events: Event[]): Generator<Event> {
for (const event of events) {
yield event;
switch (event.type) {
case "Unification": {
yield* flattenEvents(event.subevents);
break;
}
case "VariableUnified":
case "VariableSetDescriptor":
break;
default:
assertExhaustive(event);
}
}
}
function getFlatEvents(events: Event[]): {
flatEvents: Event[];
map: Map<Event, EventEpoch>;
} {
const map = new Map<Event, EventEpoch>();
const flatEvents = Array.from(flattenEvents(events));
let i = 0;
for (const event of flatEvents) {
map.set(event, i as EventEpoch);
i++;
}
return { flatEvents, map };
}
export class Engine {
#eventIndexMap: Map<Event, EventEpoch>;
#events: Event[];
#subs: Subs = Subs.new();
#reverseEvents: Map<EventEpoch, RollbackChange> = new Map();
#nextIndexForward: EventEpoch = 0 as EventEpoch;
constructor(events: Event[]) {
const { flatEvents, map } = getFlatEvents(events);
this.#eventIndexMap = map;
this.#events = flatEvents;
}
getEventIndex(event: Event): EventEpoch {
const index = this.#eventIndexMap.get(event);
if (index === undefined) {
throw new Error("Event not found");
}
return index;
}
get step(): EventEpoch {
return this.#nextIndexForward;
}
stepTo(eventIndex: EventEpoch): void {
while (this.#nextIndexForward <= eventIndex) {
this.stepForward(this.#nextIndexForward);
++this.#nextIndexForward;
}
while (this.#nextIndexForward > eventIndex + 1) {
--this.#nextIndexForward;
this.stepBackward(this.#nextIndexForward);
}
if (this.#nextIndexForward !== eventIndex + 1) {
throw new Error("Invalid event index");
}
}
stepToSnapshot(eventIndex: EventEpoch): SubsSnapshot {
this.stepTo(eventIndex);
return this.subsSnapshot();
}
get subs(): Readonly<Subs> {
return this.#subs;
}
subsSnapshot(): SubsSnapshot {
return this.#subs.snapshot({
epoch: (this.#nextIndexForward - 1) as EventEpoch,
});
}
lastEventIndex(): EventEpoch {
return (this.#events.length - 1) as EventEpoch;
}
private stepForward(eventIndex: EventEpoch): void {
const event = this.#events[eventIndex];
if (!isApplicable(event)) {
return;
}
if (!this.#reverseEvents.has(eventIndex)) {
const variable = applicableVariable(event);
const current = this.#subs.get(variable);
let revert: RollbackChange;
if (!current) {
revert = makeDeleteVariable({ variable });
} else {
revert = makeRevertVariable({ variable, to: current });
}
this.#reverseEvents.set(eventIndex, revert);
}
this.#subs.apply(event);
}
private stepBackward(eventIndex: EventEpoch): void {
const event = this.#events[eventIndex];
if (!isApplicable(event)) {
return;
}
const revert = this.#reverseEvents.get(eventIndex);
if (!revert) {
throw new Error("No revert found");
}
this.#subs.apply(revert);
}
}
function isApplicable(event: Event): event is ChangeEvent {
switch (event.type) {
case "VariableUnified":
case "VariableSetDescriptor":
return true;
case "Unification":
return false;
default:
assertExhaustive(event);
}
}
function applicableVariable(event: ChangeEvent): Variable {
switch (event.type) {
case "VariableUnified":
return event.from;
case "VariableSetDescriptor":
return event.variable;
default:
assertExhaustive(event);
}
}

View file

@ -0,0 +1,19 @@
import { Event } from "../schema";
export function lastSubEvent(event: Event): Event {
switch (event.type) {
case "Unification": {
const subevents = event.subevents;
if (subevents.length === 0) {
return event;
}
return lastSubEvent(event.subevents[event.subevents.length - 1]);
}
case "VariableUnified": {
return event;
}
case "VariableSetDescriptor": {
return event;
}
}
}

View file

@ -0,0 +1,179 @@
import { Content, Rank, Variable, Event } from "../schema";
import { assertExhaustive } from "../utils/exhaustive";
import { Refine } from "../utils/refine";
import { EventEpoch } from "./engine";
export type TypeLink = {
type: "link";
to: Variable;
};
function link({ to }: Omit<TypeLink, "type">): TypeLink {
return { type: "link", to };
}
export type TypeDescriptor = {
type: "descriptor";
rank: Rank;
content: Content;
};
function descriptor({
rank,
content,
}: Omit<TypeDescriptor, "type">): TypeDescriptor {
return { type: "descriptor", rank, content };
}
export type VarType = TypeLink | TypeDescriptor;
export type RevertVariableChange = {
type: "revertTo";
variable: Variable;
to: VarType;
};
export type DeleteVariableChange = {
type: "delete";
variable: Variable;
};
export type RollbackChange = RevertVariableChange | DeleteVariableChange;
export function makeRevertVariable({
variable,
to,
}: Omit<RevertVariableChange, "type">): RevertVariableChange {
return { type: "revertTo", variable, to: { ...to } };
}
export function makeDeleteVariable({
variable,
}: Omit<DeleteVariableChange, "type">): DeleteVariableChange {
return { type: "delete", variable };
}
export type ChangeEvent =
| Refine<Event, "VariableUnified">
| Refine<Event, "VariableSetDescriptor">;
export type Change = ChangeEvent | RollbackChange;
export class Subs implements QuerySubs {
#map: Map<Variable, VarType>;
private constructor(map: Map<Variable, VarType>) {
this.#map = map;
}
static new(): Subs {
return new Subs(new Map());
}
get(variable: Variable): VarType | undefined {
return this.#map.get(variable);
}
get_root(variable: Variable): TypeDescriptor | undefined {
const type = this.get(variable);
if (type === undefined) {
return undefined;
}
switch (type.type) {
case "descriptor":
return type;
case "link":
return this.get_root(type.to);
default:
assertExhaustive(type);
}
}
get_root_key(variable: Variable): Variable {
const type = this.get(variable);
if (type === undefined) {
return variable;
}
switch (type.type) {
case "descriptor":
return variable;
case "link":
return this.get_root_key(type.to);
default:
assertExhaustive(type);
}
}
snapshot({ epoch }: { epoch: EventEpoch }): SubsSnapshot {
const snapshotMap = new Map<Variable, VarType>();
for (const [key, value] of this.#map) {
snapshotMap.set(key, { ...value });
}
const snapshot = new Subs(snapshotMap);
return {
epoch,
get(variable: Variable): VarType | undefined {
return snapshot.get(variable);
},
get_root(variable: Variable): TypeDescriptor | undefined {
return snapshot.get_root(variable);
},
get_root_key(variable: Variable): Variable {
return snapshot.get_root_key(variable);
},
__snapshot__: SnapshotSymbol,
};
}
apply(change: Change): void {
switch (change.type) {
case "VariableUnified": {
const { from, to } = change;
if (from !== to) {
this.#map.set(from, link({ to }));
}
break;
}
case "VariableSetDescriptor": {
const { variable, rank, content } = change;
const existing = this.get_root(variable);
if (existing !== undefined) {
const nu = descriptor({ ...existing });
if (rank) nu.rank = rank;
if (content) nu.content = content;
this.#map.set(variable, nu);
} else {
if (typeof rank !== "number") throw new Error("rank is required");
if (!content) throw new Error("content is required");
this.#map.set(variable, descriptor({ rank, content }));
}
break;
}
case "revertTo": {
const { variable, to } = change;
this.#map.set(variable, { ...to });
break;
}
case "delete": {
const { variable } = change;
this.#map.delete(variable);
break;
}
default:
assertExhaustive(change);
}
}
}
const SnapshotSymbol = Symbol("Snapshot");
export interface QuerySubs {
get(variable: Variable): VarType | undefined;
get_root(variable: Variable): TypeDescriptor | undefined;
get_root_key(variable: Variable): Variable;
}
export interface SubsSnapshot extends QuerySubs {
readonly epoch: EventEpoch;
__snapshot__: typeof SnapshotSymbol;
}

View file

@ -0,0 +1,42 @@
import { useEffect, useState } from "react";
import type { TypedEmitter } from "tiny-typed-emitter";
type Events<Name extends string, T> = {
[K in Name]: (value: T) => void;
};
interface UseFocusOutlineEventProps<Name extends string, T> {
value: T;
ee: TypedEmitter<Events<Name, T>>;
event: Name;
defaultIsOutlined?: boolean;
}
export function useFocusOutlineEvent<Name extends string, T>({
value,
ee,
event,
defaultIsOutlined = false,
}: UseFocusOutlineEventProps<Name, T>) {
const [isOutlined, setIsOutlined] = useState(defaultIsOutlined);
useEffect(() => {
ee.on(event, (focusValue: T) => {
if (focusValue !== value) return;
setIsOutlined(true);
});
}, [ee, event, value]);
useEffect(() => {
if (!isOutlined) return;
const timer = setTimeout(() => {
setIsOutlined(false);
}, 500);
return () => {
clearTimeout(timer);
};
}, [isOutlined]);
return isOutlined;
}

View file

@ -0,0 +1,17 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
.outline-event-col-1 {
box-shadow: inset 2px 0px 0px #3B82F6, /* Left */
inset -2px 0px 0px transparent, /* Right */
inset 0px 2px 0px #3B82F6, /* Bottom */
inset 0px -2px 0px #3B82F6; /* Top */
}
.outline-event-col-3 {
box-shadow: inset 2px 0px 0px transparent, /* Left */
inset -2px 0px 0px #3B82F6, /* Right */
inset 0px 2px 0px #3B82F6, /* Bottom */
inset 0px -2px 0px #3B82F6; /* Top */
}

View file

@ -0,0 +1,13 @@
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
const root = ReactDOM.createRoot(
document.getElementById("root") as HTMLElement
);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View file

@ -0,0 +1,243 @@
/* eslint-disable */
/**
* This file was automatically generated by json-schema-to-typescript.
* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,
* and run json-schema-to-typescript to regenerate this file.
*/
export type Event =
| {
left: Variable;
mode: UnificationMode;
right: Variable;
subevents: Event[];
success?: boolean | null;
type: "Unification";
[k: string]: unknown;
}
| {
from: Variable;
to: Variable;
type: "VariableUnified";
[k: string]: unknown;
}
| {
content?: Content | null;
rank?: Rank | null;
type: "VariableSetDescriptor";
variable: Variable;
[k: string]: unknown;
};
export type Variable = number;
export type UnificationMode =
| {
type: "Eq";
[k: string]: unknown;
}
| {
type: "Present";
[k: string]: unknown;
}
| {
type: "LambdaSetSpecialization";
[k: string]: unknown;
};
export type Content =
| {
name?: string | null;
type: "Flex";
[k: string]: unknown;
}
| {
name: string;
type: "Rigid";
[k: string]: unknown;
}
| {
abilities: Symbol[];
name?: string | null;
type: "FlexAble";
[k: string]: unknown;
}
| {
abilities: Symbol[];
name: string;
type: "RigidAble";
[k: string]: unknown;
}
| {
name?: string | null;
structure: Variable;
type: "Recursive";
[k: string]: unknown;
}
| {
ambient_function: Variable;
recursion_var?: Variable | null;
solved: ClosureType[];
type: "LambdaSet";
unspecialized: UnspecializedClosureType[];
[k: string]: unknown;
}
| {
type: "ErasedLambda";
[k: string]: unknown;
}
| {
kind: AliasKind;
name: Symbol;
real_variable: Variable;
type: "Alias";
variables: AliasTypeVariables;
[k: string]: unknown;
}
| {
symbol: Symbol;
type: "Apply";
variables: Variable[];
[k: string]: unknown;
}
| {
arguments: Variable[];
lambda_type: Variable;
ret: Variable;
type: "Function";
[k: string]: unknown;
}
| {
extension: Variable;
fields: {
[k: string]: RecordField;
};
type: "Record";
[k: string]: unknown;
}
| {
elements: {
[k: string]: Variable;
};
extension: Variable;
type: "Tuple";
[k: string]: unknown;
}
| {
extension: TagUnionExtension;
tags: {
[k: string]: Variable[];
};
type: "TagUnion";
[k: string]: unknown;
}
| {
extension: TagUnionExtension;
functions: Symbol[];
tags: string[];
type: "FunctionOrTagUnion";
[k: string]: unknown;
}
| {
extension: TagUnionExtension;
recursion_var: Variable;
tags: {
[k: string]: Variable[];
};
type: "RecursiveTagUnion";
[k: string]: unknown;
}
| {
type: "EmptyRecord";
[k: string]: unknown;
}
| {
type: "EmptyTuple";
[k: string]: unknown;
}
| {
type: "EmptyTagUnion";
[k: string]: unknown;
}
| {
range: NumericRange;
type: "RangedNumber";
[k: string]: unknown;
}
| {
type: "Error";
[k: string]: unknown;
};
export type Symbol = string;
export type AliasKind =
| {
type: "Structural";
[k: string]: unknown;
}
| {
type: "Opaque";
[k: string]: unknown;
};
export type RecordFieldKind =
| {
type: "Demanded";
[k: string]: unknown;
}
| {
rigid: boolean;
type: "Required";
[k: string]: unknown;
}
| {
rigid: boolean;
type: "Optional";
[k: string]: unknown;
};
export type TagUnionExtension =
| {
type: "Openness";
variable: Variable;
[k: string]: unknown;
}
| {
type: "Any";
variable: Variable;
[k: string]: unknown;
};
export type NumericRangeKind =
| {
type: "Int";
[k: string]: unknown;
}
| {
type: "AnyNum";
[k: string]: unknown;
};
export type Rank = number;
export type AllEvents = Event[];
export interface ClosureType {
environment: Variable[];
function: Symbol;
[k: string]: unknown;
}
export interface UnspecializedClosureType {
ability_member: Symbol;
lambda_set_region: number;
specialization: Variable;
[k: string]: unknown;
}
export interface AliasTypeVariables {
infer_ext_in_output_position_variables: Variable[];
lambda_set_variables: Variable[];
type_variables: Variable[];
[k: string]: unknown;
}
export interface RecordField {
field_type: Variable;
kind: RecordFieldKind;
[k: string]: unknown;
}
export interface NumericRange {
kind: NumericRangeKind;
min_width: number;
signed: boolean;
[k: string]: unknown;
}

View file

@ -0,0 +1,25 @@
import { EventEpoch } from "../engine/engine";
import { Variable } from "../schema";
export interface VariableMessage {
focus: (variable: Variable) => void;
}
export interface GraphMessage {
focusEpoch: (epoch: EventEpoch) => void;
focusVariable: (variable: Variable) => void;
keydown: (key: string) => void;
}
export interface EventListMessage {
focusEpoch: (epoch: EventEpoch) => void;
}
export enum LoadEpochView {
Top,
Bot,
}
export interface GlobalMessage {
loadEpoch: (epoch: EventEpoch, view: LoadEpochView) => void;
}

View file

@ -0,0 +1,3 @@
export function assertExhaustive(_: never): never {
throw new Error("Exhaustive switch");
}

View file

@ -0,0 +1,3 @@
export type Refine<T extends { type: string }, Type extends string> = T & {
type: Type;
};

View file

@ -0,0 +1,17 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
mode: 'jit',
content: [
"./src/**/*.{js,jsx,ts,tsx}",
],
theme: {
extend: {
colors: {
'roc-purple': '#7c38f5',
'roc-purple-bg': '#ece2fd',
},
},
},
plugins: [],
}

View file

@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "es2015",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": ["src"]
}

View file

@ -0,0 +1,13 @@
[package]
name = "roc_checkmate_schema"
description = "Schema for checkmate."
authors.workspace = true
edition.workspace = true
license.workspace = true
version.workspace = true
[dependencies]
serde.workspace = true
serde_json.workspace = true
schemars.workspace = true

View file

@ -0,0 +1,225 @@
use std::collections::HashMap;
use schemars::{schema::RootSchema, schema_for, JsonSchema};
use serde::Serialize;
#[derive(Serialize, JsonSchema, Debug)]
#[serde(tag = "type")]
pub enum Constraint {}
#[derive(Serialize, JsonSchema, Debug, PartialEq)]
pub struct Variable(pub u32);
macro_rules! impl_content {
($($name:ident { $($arg:ident: $ty:ty,)* },)*) => {
#[derive(Serialize, JsonSchema, Debug)]
#[serde(tag = "type")]
pub enum Content {
$(
$name {
$($arg: $ty),*
},
)*
}
impl Content {
$(
#[allow(non_snake_case)]
pub fn $name($($arg: $ty),*) -> Self {
Self::$name { $($arg),* }
}
)*
}
};
}
impl_content! {
Flex {
name: Option<String>,
},
Rigid {
name: String,
},
FlexAble {
name: Option<String>,
abilities: Vec<Symbol>,
},
RigidAble {
name: String,
abilities: Vec<Symbol>,
},
Recursive {
name: Option<String>,
structure: Variable,
},
LambdaSet {
solved: Vec<ClosureType>,
unspecialized: Vec<UnspecializedClosureType>,
recursion_var: Option<Variable>,
ambient_function: Variable,
},
ErasedLambda {},
Alias {
name: Symbol,
variables: AliasTypeVariables,
real_variable: Variable,
kind: AliasKind,
},
Apply {
symbol: Symbol,
variables: Vec<Variable>,
},
Function {
arguments: Vec<Variable>,
lambda_type: Variable,
ret: Variable,
},
Record {
fields: HashMap<String, RecordField>,
extension: Variable,
},
Tuple {
elements: HashMap<u32, Variable>,
extension: Variable,
},
TagUnion {
tags: HashMap<String, Vec<Variable>>,
extension: TagUnionExtension,
},
FunctionOrTagUnion {
functions: Vec<Symbol>,
tags: Vec<String>,
extension: TagUnionExtension,
},
RecursiveTagUnion {
recursion_var: Variable,
tags: HashMap<String, Vec<Variable>>,
extension: TagUnionExtension,
},
EmptyRecord {},
EmptyTuple {},
EmptyTagUnion {},
RangedNumber {
range: NumericRange,
},
Error {},
}
#[derive(Serialize, JsonSchema, Debug)]
pub struct ClosureType {
pub function: Symbol,
pub environment: Vec<Variable>,
}
#[derive(Serialize, JsonSchema, Debug)]
pub struct UnspecializedClosureType {
pub specialization: Variable,
pub ability_member: Symbol,
pub lambda_set_region: u8,
}
#[derive(Serialize, JsonSchema, Debug)]
#[serde(tag = "type")]
pub enum AliasKind {
Structural,
Opaque,
}
#[derive(Serialize, JsonSchema, Debug)]
pub struct AliasTypeVariables {
pub type_variables: Vec<Variable>,
pub lambda_set_variables: Vec<Variable>,
pub infer_ext_in_output_position_variables: Vec<Variable>,
}
#[derive(Serialize, JsonSchema, Debug)]
pub struct RecordField {
pub kind: RecordFieldKind,
pub field_type: Variable,
}
#[derive(Serialize, JsonSchema, Debug)]
#[serde(tag = "type")]
pub enum RecordFieldKind {
Demanded,
Required { rigid: bool },
Optional { rigid: bool },
}
#[derive(Serialize, JsonSchema, Debug)]
#[serde(tag = "type", content = "variable")]
pub enum TagUnionExtension {
Openness(Variable),
Any(Variable),
}
#[derive(Serialize, JsonSchema, Debug)]
pub struct NumericRange {
pub kind: NumericRangeKind,
pub signed: bool,
pub min_width: u32,
}
#[derive(Serialize, JsonSchema, Debug)]
#[serde(tag = "type")]
pub enum NumericRangeKind {
Int,
AnyNum,
}
#[derive(Serialize, JsonSchema, Debug)]
pub struct Rank(pub u32);
#[derive(Serialize, JsonSchema, Debug)]
pub struct Descriptor {
pub content: Content,
pub rank: Rank,
}
#[derive(Serialize, JsonSchema, Debug)]
pub struct Symbol(
// TODO: should this be module ID + symbol?
pub String,
);
#[derive(Serialize, JsonSchema, Debug)]
#[serde(tag = "type")]
pub enum UnificationMode {
Eq,
Present,
LambdaSetSpecialization,
}
#[derive(Serialize, JsonSchema, Debug)]
#[serde(tag = "type")]
pub enum Event {
Unification {
left: Variable,
right: Variable,
mode: UnificationMode,
success: Option<bool>,
subevents: Vec<Event>,
},
VariableUnified {
from: Variable,
to: Variable,
},
VariableSetDescriptor {
variable: Variable,
rank: Option<Rank>,
content: Option<Content>,
},
}
#[derive(Serialize, JsonSchema, Debug)]
pub struct AllEvents(pub Vec<Event>);
impl AllEvents {
pub fn schema() -> RootSchema {
schema_for!(AllEvents)
}
pub fn write(&self, writer: impl std::io::Write) -> Result<(), serde_json::Error> {
serde_json::to_writer(writer, self)
}
}

View file

@ -202,7 +202,7 @@ fn int_to_ordinal(number: usize) -> std::string::String {
},
};
format!("{}{}", number, ending)
format!("{number}{ending}")
}
#[macro_export]

View file

@ -48,9 +48,9 @@ enum Kind {
impl Debug for Kind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Generated(arg0) => write!(f, "Generated({})", arg0),
Self::Generated(arg0) => write!(f, "Generated({arg0})"),
Self::Empty => write!(f, "Empty"),
Self::Interned(arg0) => write!(f, "Interned({})", arg0),
Self::Interned(arg0) => write!(f, "Interned({arg0})"),
}
}
}
@ -155,7 +155,7 @@ impl SmallStringInterner {
let index = self.lengths.len();
let offset = self.buffer.len();
write!(self.buffer, "{}", index).unwrap();
write!(self.buffer, "{index}").unwrap();
// this is a generated name, so store it as a negative length
let length = Length(-((self.buffer.len() - offset) as i16));

View file

@ -34,6 +34,12 @@ impl<T: PartialEq> VecSet<T> {
self.elements.is_empty()
}
pub fn singleton(value: T) -> Self {
Self {
elements: vec![value],
}
}
pub fn swap_remove(&mut self, index: usize) -> T {
self.elements.swap_remove(index)
}

View file

@ -1044,8 +1044,7 @@ pub fn constrain_expr(
debug_assert!(
intersection.is_empty(),
"Two patterns introduce the same symbols - that's a bug!\n{:?}",
intersection
"Two patterns introduce the same symbols - that's a bug!\n{intersection:?}"
);
}
@ -2473,6 +2472,26 @@ fn constrain_empty_record(
constraints.equal_types(record_type_index, expected, Category::Record, region)
}
fn add_host_annotation(
types: &mut Types,
constraints: &mut Constraints,
host_exposed_annotation: Option<&(Variable, roc_can::def::Annotation)>,
constraint: Constraint,
) -> Constraint {
if let Some((var, ann)) = host_exposed_annotation {
let host_annotation = {
let type_index = types.from_old_type(&ann.signature);
constraints.push_type(types, type_index)
};
let store_constr = constraints.store(host_annotation, *var, file!(), line!());
constraints.and_constraint([store_constr, constraint])
} else {
constraint
}
}
/// Constrain top-level module declarations
#[inline(always)]
pub fn constrain_decls(
@ -2499,6 +2518,7 @@ pub fn constrain_decls(
use roc_can::expr::DeclarationTag::*;
let tag = declarations.declarations[index];
match tag {
Value => {
constraint = constrain_value_def(
@ -2509,6 +2529,77 @@ pub fn constrain_decls(
index,
constraint,
);
constraint = add_host_annotation(
types,
constraints,
declarations.host_exposed_annotations.get(&index),
constraint,
);
}
Function(function_def_index) => {
constraint = constrain_function_def(
types,
constraints,
&mut env,
declarations,
index,
function_def_index,
constraint,
);
constraint = add_host_annotation(
types,
constraints,
declarations.host_exposed_annotations.get(&index),
constraint,
);
}
Recursive(_) | TailRecursive(_) => {
constraint = add_host_annotation(
types,
constraints,
declarations.host_exposed_annotations.get(&index),
constraint,
);
// for the type it does not matter that a recursive call is a tail call
constraint = constrain_recursive_declarations(
types,
constraints,
&mut env,
declarations,
index..index + 1,
constraint,
IllegalCycleMark::empty(),
);
}
Destructure(destructure_def_index) => {
constraint = constrain_destructure_def(
types,
constraints,
&mut env,
declarations,
index,
destructure_def_index,
constraint,
);
}
MutualRecursion { length, cycle_mark } => {
// the next `length` defs belong to this group
let length = length as usize;
constraint = constrain_recursive_declarations(
types,
constraints,
&mut env,
declarations,
index + 1..index + 1 + length,
constraint,
cycle_mark,
);
index += length;
}
Expectation => {
let loc_expr = &declarations.expressions[index];
@ -2566,56 +2657,6 @@ pub fn constrain_decls(
Generalizable(false),
)
}
Function(function_def_index) => {
constraint = constrain_function_def(
types,
constraints,
&mut env,
declarations,
index,
function_def_index,
constraint,
);
}
Recursive(_) | TailRecursive(_) => {
// for the type it does not matter that a recursive call is a tail call
constraint = constrain_recursive_declarations(
types,
constraints,
&mut env,
declarations,
index..index + 1,
constraint,
IllegalCycleMark::empty(),
);
}
Destructure(destructure_def_index) => {
constraint = constrain_destructure_def(
types,
constraints,
&mut env,
declarations,
index,
destructure_def_index,
constraint,
);
}
MutualRecursion { length, cycle_mark } => {
// the next `length` defs belong to this group
let length = length as usize;
constraint = constrain_recursive_declarations(
types,
constraints,
&mut env,
declarations,
index + 1..index + 1 + length,
constraint,
cycle_mark,
);
index += length;
}
}
index += 1;

View file

@ -92,6 +92,9 @@ flags! {
/// Prints all type mismatches hit during type unification.
ROC_PRINT_MISMATCHES
/// Prints all type variables entered for fixpoint-fixing.
ROC_PRINT_FIXPOINT_FIXING
/// Verifies that after let-generalization of a def, any rigid variables in the type annotation
/// of the def are indeed generalized.
///
@ -132,6 +135,10 @@ flags! {
/// instructions.
ROC_PRINT_IR_AFTER_REFCOUNT
/// Writes a pretty-printed mono IR to stderr after the tail recursion (modulo cons)
/// has been applied.
ROC_PRINT_IR_AFTER_TRMC
/// Writes a pretty-printed mono IR to stderr after performing dropspecialization.
/// Which inlines drop functions to remove pairs of alloc/dealloc instructions of its children.
ROC_PRINT_IR_AFTER_DROP_SPECIALIZATION

View file

@ -9,11 +9,13 @@ version.workspace = true
[dependencies]
roc_can = { path = "../can" }
roc_checkmate = { path = "../checkmate" }
roc_collections = { path = "../collections" }
roc_derive_key = { path = "../derive_key" }
roc_error_macros = { path = "../../error_macros" }
roc_module = { path = "../module" }
roc_region = { path = "../region" }
roc_solve_schema = { path = "../solve_schema" }
roc_types = { path = "../types" }
roc_unify = { path = "../unify" }

View file

@ -298,7 +298,7 @@ fn hash_tag_union(
Expr::Int(
discr_num_var,
discr_precision_var,
format!("{}", discr_n).into_boxed_str(),
format!("{discr_n}").into_boxed_str(),
IntValue::I128((discr_n as i128).to_ne_bytes()),
IntBound::Exact(discr_width),
),

View file

@ -1,6 +1,8 @@
use roc_can::{abilities::SpecializationLambdaSets, module::ExposedByModule};
use roc_checkmate::with_checkmate;
use roc_error_macros::internal_error;
use roc_module::symbol::{IdentIds, Symbol};
use roc_solve_schema::UnificationMode;
use roc_types::{
subs::{instantiate_rigids, Subs, Variable},
types::Polarity,
@ -31,7 +33,7 @@ impl Env<'_> {
let name = if i == 1 {
hint.clone()
} else {
format!("{}{}", hint, i)
format!("{hint}{i}")
};
if self.derived_ident_ids.get_id(&name).is_none() {
break name;
@ -71,13 +73,20 @@ impl Env<'_> {
}
pub fn unify(&mut self, left: Variable, right: Variable) {
use roc_unify::unify::{unify, Env, Mode, Unified};
use roc_unify::{
unify::{unify, Unified},
Env,
};
let unified = unify(
&mut Env::new(self.subs),
// TODO(checkmate): pass checkmate through
&mut with_checkmate!({
on => Env::new(self.subs, None),
off => Env::new(self.subs),
}),
left,
right,
Mode::EQ,
UnificationMode::EQ,
Polarity::OF_PATTERN,
);
@ -103,15 +112,22 @@ impl Env<'_> {
specialization_type: Variable,
ability_member: Symbol,
) -> SpecializationLambdaSets {
use roc_unify::unify::{unify_introduced_ability_specialization, Env, Mode, Unified};
use roc_unify::{
unify::{unify_introduced_ability_specialization, Unified},
Env,
};
let member_signature = self.import_builtin_symbol_var(ability_member);
let unified = unify_introduced_ability_specialization(
&mut Env::new(self.subs),
// TODO(checkmate): pass checkmate through
&mut with_checkmate!({
on => Env::new(self.subs, None),
off => Env::new(self.subs),
}),
member_signature,
specialization_type,
Mode::EQ,
UnificationMode::EQ,
);
match unified {
@ -151,7 +167,7 @@ impl Env<'_> {
== self.subs.get_root_key_without_compacting(lambda_set)
});
debug_assert!(belongs_to_specialized_lambda_sets,
"Did not expect derivers to need to specialize unspecialized lambda sets, but we got one: {:?} for {:?}", lambda_set, spec_var)
"Did not expect derivers to need to specialize unspecialized lambda sets, but we got one: {lambda_set:?} for {spec_var:?}")
}
}
}

View file

@ -103,7 +103,7 @@ impl FlatDecodable {
| Content::RigidVar(_)
| Content::FlexAbleVar(_, _)
| Content::RigidAbleVar(_, _) => Err(UnboundVar),
Content::LambdaSet(_) => Err(Underivable),
Content::LambdaSet(_) | Content::ErasedLambda => Err(Underivable),
}
}

View file

@ -137,7 +137,7 @@ impl FlatEncodable {
| Content::RigidVar(_)
| Content::FlexAbleVar(_, _)
| Content::RigidAbleVar(_, _) => Err(UnboundVar),
Content::LambdaSet(_) => Err(Underivable),
Content::LambdaSet(_) | Content::ErasedLambda => Err(Underivable),
}
}

View file

@ -148,7 +148,7 @@ impl FlatHash {
| Content::RigidVar(_)
| Content::FlexAbleVar(_, _)
| Content::RigidAbleVar(_, _) => Err(UnboundVar),
Content::LambdaSet(_) => Err(Underivable),
Content::LambdaSet(_) | Content::ErasedLambda => Err(Underivable),
}
}

View file

@ -241,7 +241,7 @@ fn is_exhaustive(matrix: &RefPatternMatrix, n: usize) -> PatternMatrix {
.collect();
let rest: Vec<Vec<Pattern>> = is_exhaustive(&new_matrix, n - 1);
let last: _ = alt_list
let last = alt_list
.iter()
.filter_map(|r| is_missing(alts.clone(), &ctors, r));

View file

@ -117,7 +117,7 @@ impl<'a> Formattable for TypeDef<'a> {
buf,
Parens::NotNeeded,
Newlines::from_bool(make_multiline),
indent + 1 + INDENT,
indent + INDENT,
);
}
}

View file

@ -455,6 +455,18 @@ impl CallConv<AArch64GeneralReg, AArch64FloatReg, AArch64Assembler> for AArch64C
) {
todo!("Loading returned complex symbols for AArch64");
}
fn setjmp(_buf: &mut Vec<'_, u8>) {
todo!()
}
fn longjmp(_buf: &mut Vec<'_, u8>) {
todo!()
}
fn roc_panic(_buf: &mut Vec<'_, u8>, _relocs: &mut Vec<'_, Relocation>) {
todo!()
}
}
impl Assembler<AArch64GeneralReg, AArch64FloatReg> for AArch64Assembler {
@ -529,7 +541,17 @@ impl Assembler<AArch64GeneralReg, AArch64FloatReg> for AArch64Assembler {
_fn_name: String,
_dst: AArch64GeneralReg,
) {
todo!("calling functions literal for AArch64");
todo!("function pointer for AArch64");
}
#[inline(always)]
fn data_pointer(
_buf: &mut Vec<'_, u8>,
_relocs: &mut Vec<'_, Relocation>,
_fn_name: String,
_dst: AArch64GeneralReg,
) {
todo!("data pointer for AArch64");
}
#[inline(always)]
@ -832,6 +854,10 @@ impl Assembler<AArch64GeneralReg, AArch64FloatReg> for AArch64Assembler {
todo!("saving floating point reg to base offset for AArch64");
}
#[inline(always)]
fn mov_base32_freg32(_buf: &mut Vec<'_, u8>, _offset: i32, _src: AArch64FloatReg) {
todo!("saving floating point reg to base offset for AArch64");
}
#[inline(always)]
fn movesd_mem64_offset32_freg64(
_buf: &mut Vec<'_, u8>,
_ptr: AArch64GeneralReg,
@ -965,33 +991,32 @@ impl Assembler<AArch64GeneralReg, AArch64FloatReg> for AArch64Assembler {
}
#[inline(always)]
fn movsx_reg64_base32(buf: &mut Vec<'_, u8>, dst: AArch64GeneralReg, offset: i32, size: u8) {
debug_assert!(size <= 8);
if size == 8 {
Self::mov_reg64_base32(buf, dst, offset);
} else if size == 4 {
todo!("sign extending 4 byte values");
} else if size == 2 {
todo!("sign extending 2 byte values");
} else if size == 1 {
todo!("sign extending 1 byte values");
} else {
internal_error!("Invalid size for sign extension: {}", size);
fn movsx_reg_base32(
buf: &mut Vec<'_, u8>,
register_width: RegisterWidth,
dst: AArch64GeneralReg,
offset: i32,
) {
match register_width {
RegisterWidth::W8 => todo!("sign extend 1 byte values"),
RegisterWidth::W16 => todo!("sign extend 2 byte values"),
RegisterWidth::W32 => todo!("sign extend 4 byte values"),
RegisterWidth::W64 => Self::mov_reg64_base32(buf, dst, offset),
}
}
#[inline(always)]
fn movzx_reg64_base32(buf: &mut Vec<'_, u8>, dst: AArch64GeneralReg, offset: i32, size: u8) {
debug_assert!(size <= 8);
if size == 8 {
Self::mov_reg64_base32(buf, dst, offset);
} else if size == 4 {
todo!("zero extending 4 byte values");
} else if size == 2 {
todo!("zero extending 2 byte values");
} else if size == 1 {
todo!("zero extending 1 byte values");
} else {
internal_error!("Invalid size for zero extension: {}", size);
fn movzx_reg_base32(
buf: &mut Vec<'_, u8>,
register_width: RegisterWidth,
dst: AArch64GeneralReg,
offset: i32,
) {
match register_width {
RegisterWidth::W8 => todo!("zero extend 1 byte values"),
RegisterWidth::W16 => todo!("zero extend 2 byte values"),
RegisterWidth::W32 => todo!("zero extend 4 byte values"),
RegisterWidth::W64 => Self::mov_reg64_base32(buf, dst, offset),
}
}
@ -3161,7 +3186,7 @@ mod tests {
UsesZR => "xzr".to_owned(),
UsesSP => "sp".to_owned(),
},
_ => format!("{}", self),
_ => format!("{self}"),
}
}
}

View file

@ -5,7 +5,7 @@ use crate::{
use bumpalo::collections::{CollectIn, Vec};
use roc_builtins::bitcode::{self, FloatWidth, IntWidth};
use roc_collections::all::MutMap;
use roc_error_macros::internal_error;
use roc_error_macros::{internal_error, todo_lambda_erasure};
use roc_module::symbol::{Interns, ModuleId, Symbol};
use roc_mono::code_gen_help::{CallerProc, CodeGenHelp, HelperOp};
use roc_mono::ir::{
@ -13,7 +13,7 @@ use roc_mono::ir::{
SelfRecursive, Stmt,
};
use roc_mono::layout::{
Builtin, InLayout, Layout, LayoutIds, LayoutInterner, LayoutRepr, STLayoutInterner,
Builtin, InLayout, LambdaName, Layout, LayoutIds, LayoutInterner, LayoutRepr, STLayoutInterner,
TagIdIntType, UnionLayout,
};
use roc_mono::low_level::HigherOrder;
@ -134,6 +134,10 @@ pub trait CallConv<GeneralReg: RegTrait, FloatReg: RegTrait, ASM: Assembler<Gene
sym: &Symbol,
layout: &InLayout<'a>,
);
fn setjmp(buf: &mut Vec<'_, u8>);
fn longjmp(buf: &mut Vec<'_, u8>);
fn roc_panic(buf: &mut Vec<'_, u8>, relocs: &mut Vec<'_, Relocation>);
}
pub enum CompareOperation {
@ -238,6 +242,13 @@ pub trait Assembler<GeneralReg: RegTrait, FloatReg: RegTrait>: Sized + Copy {
dst: GeneralReg,
);
fn data_pointer(
buf: &mut Vec<'_, u8>,
relocs: &mut Vec<'_, Relocation>,
fn_name: String,
dst: GeneralReg,
);
/// Jumps by an offset of offset bytes unconditionally.
/// It should always generate the same number of bytes to enable replacement if offset changes.
/// It returns the base offset to calculate the jump from (generally the instruction after the jump).
@ -321,6 +332,7 @@ pub trait Assembler<GeneralReg: RegTrait, FloatReg: RegTrait>: Sized + Copy {
fn mov_reg8_base32(buf: &mut Vec<'_, u8>, dst: GeneralReg, offset: i32);
fn mov_base32_freg64(buf: &mut Vec<'_, u8>, offset: i32, src: FloatReg);
fn mov_base32_freg32(buf: &mut Vec<'_, u8>, offset: i32, src: FloatReg);
fn mov_base32_reg64(buf: &mut Vec<'_, u8>, offset: i32, src: GeneralReg);
fn mov_base32_reg32(buf: &mut Vec<'_, u8>, offset: i32, src: GeneralReg);
@ -391,10 +403,19 @@ pub trait Assembler<GeneralReg: RegTrait, FloatReg: RegTrait>: Sized + Copy {
/// Sign extends the data at `offset` with `size` as it copies it to `dst`
/// size must be less than or equal to 8.
fn movsx_reg64_base32(buf: &mut Vec<'_, u8>, dst: GeneralReg, offset: i32, size: u8);
/// Zero extends the data at `offset` with `size` as it copies it to `dst`
/// size must be less than or equal to 8.
fn movzx_reg64_base32(buf: &mut Vec<'_, u8>, dst: GeneralReg, offset: i32, size: u8);
fn movsx_reg_base32(
buf: &mut Vec<'_, u8>,
register_width: RegisterWidth,
dst: GeneralReg,
offset: i32,
);
fn movzx_reg_base32(
buf: &mut Vec<'_, u8>,
register_width: RegisterWidth,
dst: GeneralReg,
offset: i32,
);
fn mov_freg64_stack32(buf: &mut Vec<'_, u8>, dst: FloatReg, offset: i32);
fn mov_reg64_stack32(buf: &mut Vec<'_, u8>, dst: GeneralReg, offset: i32);
@ -704,6 +725,9 @@ impl<
fn interner(&self) -> &STLayoutInterner<'a> {
self.layout_interner
}
fn relocations_mut(&mut self) -> &mut Vec<'a, Relocation> {
&mut self.relocs
}
fn module_interns_helpers_mut(
&mut self,
) -> (
@ -877,12 +901,47 @@ impl<
(out.into_bump_slice(), offset)
}
fn build_roc_setjmp(&mut self) -> &'a [u8] {
let mut out = bumpalo::vec![in self.env.arena];
CC::setjmp(&mut out);
out.into_bump_slice()
}
fn build_roc_longjmp(&mut self) -> &'a [u8] {
let mut out = bumpalo::vec![in self.env.arena];
CC::longjmp(&mut out);
out.into_bump_slice()
}
fn build_roc_panic(&mut self) -> (&'a [u8], Vec<'a, Relocation>) {
let mut out = bumpalo::vec![in self.env.arena];
let mut relocs = bumpalo::vec![in self.env.arena];
CC::roc_panic(&mut out, &mut relocs);
(out.into_bump_slice(), relocs)
}
fn build_fn_pointer(&mut self, dst: &Symbol, fn_name: String) {
let reg = self.storage_manager.claim_general_reg(&mut self.buf, dst);
ASM::function_pointer(&mut self.buf, &mut self.relocs, fn_name, reg)
}
fn build_data_pointer(&mut self, dst: &Symbol, data_name: String) {
let reg = self.storage_manager.claim_general_reg(&mut self.buf, dst);
// now, this gives a pointer to the value
ASM::data_pointer(&mut self.buf, &mut self.relocs, data_name, reg);
// dereference
ASM::mov_reg64_mem64_offset32(&mut self.buf, reg, reg, 0);
}
fn build_fn_call(
&mut self,
dst: &Symbol,
@ -926,7 +985,8 @@ impl<
let dst_reg = self.storage_manager.claim_float_reg(&mut self.buf, dst);
ASM::mov_freg64_freg64(&mut self.buf, dst_reg, CC::FLOAT_RETURN_REGS[0]);
}
LayoutRepr::I128 | LayoutRepr::U128 => {
// Note that on windows there is only 1 general return register so we can't use this optimisation
LayoutRepr::I128 | LayoutRepr::U128 if CC::GENERAL_RETURN_REGS.len() > 1 => {
let offset = self.storage_manager.claim_stack_area(dst, 16);
ASM::mov_base32_reg64(&mut self.buf, offset, CC::GENERAL_RETURN_REGS[0]);
@ -1263,6 +1323,18 @@ impl<
}
fn build_num_mul(&mut self, dst: &Symbol, src1: &Symbol, src2: &Symbol, layout: &InLayout<'a>) {
// for the time being, `num_mul` is implemented as wrapping multiplication. In roc, the normal
// `mul` should panic on overflow, but we just don't do that yet
self.build_num_mul_wrap(dst, src1, src2, layout)
}
fn build_num_mul_wrap(
&mut self,
dst: &Symbol,
src1: &Symbol,
src2: &Symbol,
layout: &InLayout<'a>,
) {
use Builtin::Int;
match self.layout_interner.get_repr(*layout) {
@ -1324,7 +1396,7 @@ impl<
let src2_reg = self.storage_manager.load_to_float_reg(&mut self.buf, src2);
ASM::mul_freg32_freg32_freg32(&mut self.buf, dst_reg, src1_reg, src2_reg);
}
x => todo!("NumMul: layout, {:?}", x),
x => todo!("NumMulWrap: layout, {:?}", x),
}
}
@ -1578,8 +1650,8 @@ impl<
HelperOp::Eq,
);
let fn_name = self.function_symbol_to_string(
eq_symbol,
let fn_name = self.lambda_name_to_string(
LambdaName::no_niche(eq_symbol),
[*arg_layout, *arg_layout].into_iter(),
None,
Layout::U8,
@ -1803,6 +1875,34 @@ impl<
);
}
fn build_num_cmp(
&mut self,
dst: &Symbol,
src1: &Symbol,
src2: &Symbol,
arg_layout: &InLayout<'a>,
) {
// This implements the expression:
// (x != y) as u8 + (x < y) as u8
// For x==y: (false as u8) + (false as u8) = 0 = RocOrder::Eq
// For x>y: (true as u8) + (false as u8) = 1 = RocOrder::Gt
// For x<y: (true as u8) + (true as u8) = 2 = RocOrder::Lt
// u8 is represented in the stack machine as i32, but written to memory as 1 byte
let not_equal = self.debug_symbol("not_equal");
self.build_neq(&not_equal, src1, src2, arg_layout);
self.build_num_lt(dst, src1, src2, arg_layout);
let neq_reg = self
.storage_manager
.load_to_general_reg(&mut self.buf, &not_equal);
let dst_reg = self.storage_manager.load_to_general_reg(&mut self.buf, dst);
ASM::add_reg64_reg64_reg64(&mut self.buf, dst_reg, dst_reg, neq_reg);
self.free_symbol(&not_equal);
}
fn build_num_lt(
&mut self,
dst: &Symbol,
@ -1935,8 +2035,8 @@ impl<
self.load_layout_stack_size(old_element_layout, old_element_width);
self.load_layout_stack_size(new_element_layout, new_element_width);
let caller_string = self.function_symbol_to_string(
caller_proc.proc_symbol,
let caller_string = self.lambda_name_to_string(
LambdaName::no_niche(caller_proc.proc_symbol),
std::iter::empty(),
None,
Layout::UNIT,
@ -2742,14 +2842,7 @@ impl<
.storage_manager
.load_to_general_reg(&mut self.buf, structure);
let mask_symbol = self.debug_symbol("tag_id_mask");
let mask_reg = self
.storage_manager
.claim_general_reg(&mut self.buf, &mask_symbol);
ASM::mov_reg64_imm64(&mut self.buf, mask_reg, (!0b111) as _);
// mask out the tag id bits
ASM::and_reg64_reg64_reg64(&mut self.buf, ptr_reg, ptr_reg, mask_reg);
let (mask_symbol, mask_reg) = self.clear_tag_id(ptr_reg);
let mut offset = 0;
for field in &other_fields[..index as usize] {
@ -2760,11 +2853,13 @@ impl<
&mut self.buf,
&mut self.storage_manager,
self.layout_interner,
ptr_reg,
mask_reg,
offset as i32,
element_layout,
*sym,
);
self.free_symbol(&mask_symbol)
}
UnionLayout::Recursive(tag_layouts) => {
let other_fields = tag_layouts[tag_id as usize];
@ -2775,15 +2870,13 @@ impl<
.load_to_general_reg(&mut self.buf, structure);
// mask out the tag id bits
if !union_layout.stores_tag_id_as_data(self.storage_manager.target_info) {
let mask_symbol = self.debug_symbol("tag_id_mask");
let mask_reg = self
.storage_manager
.claim_general_reg(&mut self.buf, &mask_symbol);
ASM::mov_reg64_imm64(&mut self.buf, mask_reg, (!0b111) as _);
ASM::and_reg64_reg64_reg64(&mut self.buf, ptr_reg, ptr_reg, mask_reg);
}
let (unmasked_symbol, unmasked_reg) =
if union_layout.stores_tag_id_as_data(self.storage_manager.target_info) {
(None, ptr_reg)
} else {
let (mask_symbol, mask_reg) = self.clear_tag_id(ptr_reg);
(Some(mask_symbol), mask_reg)
};
let mut offset = 0;
for field in &other_fields[..index as usize] {
@ -2794,16 +2887,113 @@ impl<
&mut self.buf,
&mut self.storage_manager,
self.layout_interner,
ptr_reg,
unmasked_reg,
offset as i32,
element_layout,
*sym,
);
if let Some(unmasked_symbol) = unmasked_symbol {
self.free_symbol(&unmasked_symbol);
}
}
}
}
fn build_ptr_write(
fn load_union_field_ptr_at_index(
&mut self,
sym: &Symbol,
structure: &Symbol,
tag_id: TagIdIntType,
index: u64,
union_layout: &UnionLayout<'a>,
) {
let ptr_reg = self
.storage_manager
.load_to_general_reg(&mut self.buf, structure);
let sym_reg = self.storage_manager.claim_general_reg(&mut self.buf, sym);
match union_layout {
UnionLayout::NonRecursive(_) => {
unreachable!("operation not supported")
}
UnionLayout::NonNullableUnwrapped(field_layouts) => {
let mut offset = 0;
for field in &field_layouts[..index as usize] {
offset += self.layout_interner.stack_size(*field);
}
ASM::add_reg64_reg64_imm32(&mut self.buf, sym_reg, ptr_reg, offset as i32);
}
UnionLayout::NullableUnwrapped {
nullable_id,
other_fields,
} => {
debug_assert_ne!(tag_id, *nullable_id as TagIdIntType);
let mut offset = 0;
for field in &other_fields[..index as usize] {
offset += self.layout_interner.stack_size(*field);
}
ASM::add_reg64_reg64_imm32(&mut self.buf, sym_reg, ptr_reg, offset as i32);
}
UnionLayout::NullableWrapped {
nullable_id,
other_tags,
} => {
debug_assert_ne!(tag_id, *nullable_id as TagIdIntType);
let other_fields = if tag_id < *nullable_id {
other_tags[tag_id as usize]
} else {
other_tags[tag_id as usize - 1]
};
let (mask_symbol, mask_reg) = self.clear_tag_id(ptr_reg);
let mut offset = 0;
for field in &other_fields[..index as usize] {
offset += self.layout_interner.stack_size(*field);
}
ASM::add_reg64_reg64_imm32(&mut self.buf, sym_reg, mask_reg, offset as i32);
self.free_symbol(&mask_symbol);
}
UnionLayout::Recursive(tag_layouts) => {
let other_fields = tag_layouts[tag_id as usize];
let ptr_reg = self
.storage_manager
.load_to_general_reg(&mut self.buf, structure);
// mask out the tag id bits
let (unmasked_symbol, unmasked_reg) =
if union_layout.stores_tag_id_as_data(self.storage_manager.target_info) {
(None, ptr_reg)
} else {
let (mask_symbol, mask_reg) = self.clear_tag_id(ptr_reg);
(Some(mask_symbol), mask_reg)
};
let mut offset = 0;
for field in &other_fields[..index as usize] {
offset += self.layout_interner.stack_size(*field);
}
ASM::add_reg64_reg64_imm32(&mut self.buf, sym_reg, unmasked_reg, offset as i32);
if let Some(unmasked_symbol) = unmasked_symbol {
self.free_symbol(&unmasked_symbol);
}
}
}
}
fn build_ptr_store(
&mut self,
sym: Symbol,
ptr: Symbol,
@ -2839,6 +3029,63 @@ impl<
ASM::mov_base32_reg64(&mut self.buf, base_offset, ptr_reg);
}
fn build_ptr_load(&mut self, sym: Symbol, ptr: Symbol, element_layout: InLayout<'a>) {
let ptr_reg = self
.storage_manager
.load_to_general_reg(&mut self.buf, &ptr);
let offset = 0;
Self::ptr_read(
&mut self.buf,
&mut self.storage_manager,
self.layout_interner,
ptr_reg,
offset,
element_layout,
sym,
);
}
fn build_ptr_clear_tag_id(&mut self, sym: Symbol, ptr: Symbol) {
let buf = &mut self.buf;
let ptr_reg = self.storage_manager.load_to_general_reg(buf, &ptr);
let sym_reg = self.storage_manager.claim_general_reg(buf, &sym);
ASM::mov_reg64_imm64(buf, sym_reg, !0b111);
ASM::and_reg64_reg64_reg64(buf, sym_reg, sym_reg, ptr_reg);
}
fn build_alloca(&mut self, sym: Symbol, value: Option<Symbol>, element_layout: InLayout<'a>) {
// 1. acquire some stack space
let element_width = self.interner().stack_size(element_layout);
let allocation = self.debug_symbol("stack_allocation");
let ptr = self.debug_symbol("ptr");
if element_width == 0 {
self.storage_manager.claim_pointer_stack_area(sym);
return;
}
let base_offset = self
.storage_manager
.claim_stack_area(&allocation, element_width);
let ptr_reg = self.storage_manager.claim_general_reg(&mut self.buf, &ptr);
ASM::mov_reg64_reg64(&mut self.buf, ptr_reg, CC::BASE_PTR_REG);
ASM::add_reg64_reg64_imm32(&mut self.buf, ptr_reg, ptr_reg, base_offset);
if let Some(value) = value {
self.build_ptr_store(sym, ptr, value, element_layout);
} else {
// this is now a pointer to uninitialized memory!
let r = self.storage_manager.claim_general_reg(&mut self.buf, &sym);
ASM::mov_reg64_reg64(&mut self.buf, r, ptr_reg);
}
}
fn expr_box(
&mut self,
sym: Symbol,
@ -2871,27 +3118,13 @@ impl<
self.free_symbol(&element_width_symbol);
self.free_symbol(&element_alignment_symbol);
self.build_ptr_write(sym, allocation, value, element_layout);
self.build_ptr_store(sym, allocation, value, element_layout);
self.free_symbol(&allocation);
}
fn expr_unbox(&mut self, dst: Symbol, ptr: Symbol, element_layout: InLayout<'a>) {
let ptr_reg = self
.storage_manager
.load_to_general_reg(&mut self.buf, &ptr);
let offset = 0;
Self::ptr_read(
&mut self.buf,
&mut self.storage_manager,
self.layout_interner,
ptr_reg,
offset,
element_layout,
dst,
);
self.build_ptr_load(dst, ptr, element_layout)
}
fn get_tag_id(&mut self, sym: &Symbol, structure: &Symbol, union_layout: &UnionLayout<'a>) {
@ -3016,9 +3249,7 @@ impl<
let target_info = self.storage_manager.target_info;
if union_layout.stores_tag_id_as_data(target_info) {
let offset = union_layout
.tag_id_offset(self.interner(), target_info)
.unwrap() as i32;
let offset = union_layout.tag_id_offset(self.interner()).unwrap() as i32;
let ptr_reg = self
.storage_manager
@ -3062,13 +3293,10 @@ impl<
tag_id: TagIdIntType,
reuse: Option<Symbol>,
) {
let target_info = self.storage_manager.target_info;
let layout_interner: &mut STLayoutInterner<'a> = self.layout_interner;
let buf: &mut Vec<'a, u8> = &mut self.buf;
let (data_size, data_alignment) =
union_layout.data_size_and_alignment(layout_interner, target_info);
let (data_size, data_alignment) = union_layout.data_size_and_alignment(layout_interner);
match union_layout {
UnionLayout::NonRecursive(field_layouts) => {
@ -3464,7 +3692,8 @@ impl<
}
LayoutRepr::Union(UnionLayout::NonRecursive(_))
| LayoutRepr::Builtin(_)
| LayoutRepr::Struct(_) => {
| LayoutRepr::Struct(_)
| LayoutRepr::Erased(_) => {
internal_error!("All primitive values should fit in a single register");
}
}
@ -3806,6 +4035,19 @@ impl<
CC: CallConv<GeneralReg, FloatReg, ASM>,
> Backend64Bit<'a, 'r, GeneralReg, FloatReg, ASM, CC>
{
fn clear_tag_id(&mut self, ptr_reg: GeneralReg) -> (Symbol, GeneralReg) {
let unmasked_symbol = self.debug_symbol("unmasked");
let unmasked_reg = self
.storage_manager
.claim_general_reg(&mut self.buf, &unmasked_symbol);
ASM::mov_reg64_imm64(&mut self.buf, unmasked_reg, (!0b111) as _);
ASM::and_reg64_reg64_reg64(&mut self.buf, unmasked_reg, ptr_reg, unmasked_reg);
(unmasked_symbol, unmasked_reg)
}
fn compare(
&mut self,
op: CompareOperation,
@ -4023,7 +4265,18 @@ impl<
Builtin::Int(int_width) => match int_width {
IntWidth::I128 | IntWidth::U128 => {
// can we treat this as 2 u64's?
todo!()
storage_manager.with_tmp_general_reg(
buf,
|storage_manager, buf, tmp_reg| {
let base_offset = storage_manager.claim_stack_area(&dst, 16);
ASM::mov_reg64_mem64_offset32(buf, tmp_reg, ptr_reg, offset);
ASM::mov_base32_reg64(buf, base_offset, tmp_reg);
ASM::mov_reg64_mem64_offset32(buf, tmp_reg, ptr_reg, offset + 8);
ASM::mov_base32_reg64(buf, base_offset + 8, tmp_reg);
},
);
}
IntWidth::I64 | IntWidth::U64 => {
let dst_reg = storage_manager.claim_general_reg(buf, &dst);
@ -4035,10 +4288,12 @@ impl<
}
IntWidth::I16 | IntWidth::U16 => {
let dst_reg = storage_manager.claim_general_reg(buf, &dst);
ASM::xor_reg64_reg64_reg64(buf, dst_reg, dst_reg, dst_reg);
ASM::mov_reg16_mem16_offset32(buf, dst_reg, ptr_reg, offset);
}
IntWidth::I8 | IntWidth::U8 => {
let dst_reg = storage_manager.claim_general_reg(buf, &dst);
ASM::xor_reg64_reg64_reg64(buf, dst_reg, dst_reg, dst_reg);
ASM::mov_reg8_mem8_offset32(buf, dst_reg, ptr_reg, offset);
}
},
@ -4053,10 +4308,21 @@ impl<
Builtin::Bool => {
// the same as an 8-bit integer
let dst_reg = storage_manager.claim_general_reg(buf, &dst);
ASM::xor_reg64_reg64_reg64(buf, dst_reg, dst_reg, dst_reg);
ASM::mov_reg8_mem8_offset32(buf, dst_reg, ptr_reg, offset);
}
Builtin::Decimal => {
// same as 128-bit integer
storage_manager.with_tmp_general_reg(buf, |storage_manager, buf, tmp_reg| {
let base_offset = storage_manager.claim_stack_area(&dst, 16);
ASM::mov_reg64_mem64_offset32(buf, tmp_reg, ptr_reg, offset);
ASM::mov_base32_reg64(buf, base_offset, tmp_reg);
ASM::mov_reg64_mem64_offset32(buf, tmp_reg, ptr_reg, offset + 8);
ASM::mov_base32_reg64(buf, base_offset + 8, tmp_reg);
});
}
Builtin::Str | Builtin::List(_) => {
storage_manager.with_tmp_general_reg(buf, |storage_manager, buf, tmp_reg| {
@ -4123,6 +4389,8 @@ impl<
dst,
);
}
LayoutRepr::Erased(_) => todo_lambda_erasure!(),
}
}
@ -4336,7 +4604,7 @@ macro_rules! single_register_layouts {
#[macro_export]
macro_rules! pointer_layouts {
() => {
LayoutRepr::Boxed(_)
LayoutRepr::Ptr(_)
| LayoutRepr::RecursivePointer(_)
| LayoutRepr::Union(
UnionLayout::Recursive(_)
@ -4344,5 +4612,6 @@ macro_rules! pointer_layouts {
| UnionLayout::NullableWrapped { .. }
| UnionLayout::NullableUnwrapped { .. },
)
| LayoutRepr::FunctionPointer(_)
};
}

View file

@ -6,7 +6,7 @@ use crate::{
use bumpalo::collections::Vec;
use roc_builtins::bitcode::{FloatWidth, IntWidth};
use roc_collections::all::{MutMap, MutSet};
use roc_error_macros::internal_error;
use roc_error_macros::{internal_error, todo_lambda_erasure};
use roc_module::symbol::Symbol;
use roc_mono::{
ir::{JoinPointId, Param},
@ -23,6 +23,8 @@ use RegStorage::*;
use StackStorage::*;
use Storage::*;
use super::RegisterWidth;
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum RegStorage<GeneralReg: RegTrait, FloatReg: RegTrait> {
General(GeneralReg),
@ -343,10 +345,19 @@ impl<
sign_extend,
}) => {
let reg = self.get_general_reg(buf);
let register_width = match size {
8 => RegisterWidth::W64,
4 => RegisterWidth::W32,
2 => RegisterWidth::W16,
1 => RegisterWidth::W8,
_ => internal_error!("Invalid size: {size}"),
};
if sign_extend {
ASM::movsx_reg64_base32(buf, reg, base_offset, size as u8);
ASM::movsx_reg_base32(buf, register_width, reg, base_offset);
} else {
ASM::movzx_reg64_base32(buf, reg, base_offset, size as u8);
ASM::movzx_reg_base32(buf, register_width, reg, base_offset);
}
self.general_used_regs.push((reg, *sym));
self.symbol_storage_map.insert(*sym, Reg(General(reg)));
@ -469,10 +480,18 @@ impl<
}) => {
debug_assert!(*size <= 8);
let register_width = match size {
8 => RegisterWidth::W64,
4 => RegisterWidth::W32,
2 => RegisterWidth::W16,
1 => RegisterWidth::W8,
_ => internal_error!("Invalid size: {size}"),
};
if *sign_extend {
ASM::movsx_reg64_base32(buf, reg, *base_offset, *size as u8)
ASM::movsx_reg_base32(buf, register_width, reg, *base_offset)
} else {
ASM::movzx_reg64_base32(buf, reg, *base_offset, *size as u8)
ASM::movzx_reg_base32(buf, register_width, reg, *base_offset)
}
}
Stack(Complex { size, .. }) => {
@ -614,8 +633,7 @@ impl<
let (union_offset, _) = self.stack_offset_and_size(structure);
let (data_size, data_alignment) =
union_layout.data_size_and_alignment(layout_interner, self.target_info);
let (data_size, data_alignment) = union_layout.data_size_and_alignment(layout_interner);
let id_offset = data_size - data_alignment;
let discriminant = union_layout.discriminant();
@ -765,7 +783,11 @@ impl<
let reg = self.load_to_float_reg(buf, sym);
ASM::mov_base32_freg64(buf, to_offset, reg);
}
FloatWidth::F32 => todo!(),
FloatWidth::F32 => {
debug_assert_eq!(to_offset % 4, 0);
let reg = self.load_to_float_reg(buf, sym);
ASM::mov_base32_freg64(buf, to_offset, reg);
}
},
Builtin::Bool => {
// same as 8-bit integer, but we special-case true/false because these symbols
@ -785,7 +807,13 @@ impl<
}
}
}
Builtin::Decimal => todo!(),
Builtin::Decimal => {
let (from_offset, size) = self.stack_offset_and_size(sym);
debug_assert_eq!(from_offset % 8, 0);
debug_assert_eq!(size % 8, 0);
debug_assert_eq!(size, layout_interner.stack_size(*layout));
self.copy_to_stack_offset(buf, size, from_offset, to_offset)
}
Builtin::Str | Builtin::List(_) => {
let (from_offset, size) = self.stack_offset_and_size(sym);
debug_assert_eq!(size, layout_interner.stack_size(*layout));
@ -808,7 +836,8 @@ impl<
self.copy_to_stack_offset(buf, size, from_offset, to_offset)
}
LayoutRepr::RecursivePointer(_) | LayoutRepr::Boxed(_) | LayoutRepr::Union(_) => {
LayoutRepr::Erased(_) => todo_lambda_erasure!(),
pointer_layouts!() => {
// like a 64-bit integer
debug_assert_eq!(to_offset % 8, 0);
let reg = self.load_to_general_reg(buf, sym);
@ -1146,12 +1175,7 @@ impl<
) {
let mut param_storage = bumpalo::vec![in self.env.arena];
param_storage.reserve(params.len());
for Param {
symbol,
ownership: _,
layout,
} in params
{
for Param { symbol, layout } in params {
// Claim a location for every join point parameter to be loaded at.
// Put everything on the stack for simplicity.
self.joinpoint_argument_stack_storage(layout_interner, *symbol, *layout);

File diff suppressed because it is too large Load diff

View file

@ -10,17 +10,17 @@ use std::collections::hash_map::Entry;
use bumpalo::{collections::Vec, Bump};
use roc_builtins::bitcode::{self, FloatWidth, IntWidth};
use roc_collections::all::{MutMap, MutSet};
use roc_error_macros::internal_error;
use roc_error_macros::{internal_error, todo_lambda_erasure};
use roc_module::ident::ModuleName;
use roc_module::low_level::{LowLevel, LowLevelWrapperType};
use roc_module::symbol::{Interns, ModuleId, Symbol};
use roc_mono::code_gen_help::{CallerProc, CodeGenHelp};
use roc_mono::ir::{
BranchInfo, CallType, CrashTag, Expr, HigherOrderLowLevel, JoinPointId, ListLiteralElement,
Literal, Param, Proc, ProcLayout, SelfRecursive, Stmt,
Literal, ModifyRc, Param, Proc, ProcLayout, SelfRecursive, Stmt,
};
use roc_mono::layout::{
Builtin, InLayout, Layout, LayoutIds, LayoutInterner, LayoutRepr, STLayoutInterner,
Builtin, InLayout, LambdaName, Layout, LayoutIds, LayoutInterner, LayoutRepr, STLayoutInterner,
TagIdIntType, UnionLayout,
};
use roc_mono::list_element_layout;
@ -138,15 +138,22 @@ impl<'a> LastSeenMap<'a> {
Expr::Call(call) => self.scan_ast_call(call, stmt),
Expr::Tag { arguments, .. } => {
Expr::Tag {
arguments, reuse, ..
} => {
if let Some(ru) = reuse {
self.set_last_seen(ru.symbol, stmt);
}
for sym in *arguments {
self.set_last_seen(*sym, stmt);
}
}
Expr::ExprBox { symbol } => {
self.set_last_seen(*symbol, stmt);
Expr::ErasedMake { value, callee } => {
value.map(|v| self.set_last_seen(v, stmt));
self.set_last_seen(*callee, stmt);
}
Expr::ExprUnbox { symbol } => {
Expr::ErasedLoad { symbol, field: _ } => {
self.set_last_seen(*symbol, stmt);
}
Expr::Struct(syms) => {
@ -163,6 +170,9 @@ impl<'a> LastSeenMap<'a> {
Expr::UnionAtIndex { structure, .. } => {
self.set_last_seen(*structure, stmt);
}
Expr::UnionFieldPtrAtIndex { structure, .. } => {
self.set_last_seen(*structure, stmt);
}
Expr::Array { elems, .. } => {
for elem in *elems {
if let ListLiteralElement::Symbol(sym) = elem {
@ -170,19 +180,17 @@ impl<'a> LastSeenMap<'a> {
}
}
}
Expr::Reuse {
symbol, arguments, ..
} => {
self.set_last_seen(*symbol, stmt);
for sym in *arguments {
self.set_last_seen(*sym, stmt);
}
}
Expr::Reset { symbol, .. } | Expr::ResetRef { symbol, .. } => {
self.set_last_seen(*symbol, stmt);
}
Expr::EmptyArray => {}
Expr::Alloca { initializer, .. } => {
if let Some(initializer) = initializer {
self.set_last_seen(*initializer, stmt);
}
}
Expr::RuntimeErrorFunction(_) => {}
Expr::FunctionPointer { .. } => todo_lambda_erasure!(),
Expr::EmptyArray => {}
}
self.scan_ast_help(following);
}
@ -270,6 +278,7 @@ impl<'a> LastSeenMap<'a> {
match call_type {
CallType::ByName { .. } => {}
CallType::ByPointer { .. } => {}
CallType::LowLevel { .. } => {}
CallType::HigherOrder { .. } => {}
CallType::Foreign { .. } => {}
@ -282,6 +291,7 @@ trait Backend<'a> {
fn interns(&self) -> &Interns;
fn interns_mut(&mut self) -> &mut Interns;
fn interner(&self) -> &STLayoutInterner<'a>;
fn relocations_mut(&mut self) -> &mut Vec<'a, Relocation>;
fn interner_mut(&mut self) -> &mut STLayoutInterner<'a> {
self.module_interns_helpers_mut().1
@ -317,9 +327,9 @@ trait Backend<'a> {
&mut Vec<'a, CallerProc<'a>>,
);
fn function_symbol_to_string<'b, I>(
fn lambda_name_to_string<'b, I>(
&self,
symbol: Symbol,
name: LambdaName,
arguments: I,
_lambda_set: Option<InLayout>,
result: InLayout,
@ -327,18 +337,27 @@ trait Backend<'a> {
where
I: Iterator<Item = InLayout<'b>>,
{
use std::fmt::Write;
use std::hash::{BuildHasher, Hash, Hasher};
// NOTE: due to randomness, this will not be consistent between runs
let mut state = roc_collections::all::BuildHasher::default().build_hasher();
let symbol = name.name();
let mut buf = String::with_capacity(1024);
for a in arguments {
a.hash(&mut state);
write!(buf, "{:?}", self.interner().dbg_stable(a)).expect("capacity");
}
// lambda set should not matter; it should already be added as an argument
// lambda_set.hash(&mut state);
// but the niche of the lambda name may be the only thing differentiating two different
// implementations of a function with the same symbol
write!(buf, "{:?}", name.niche().dbg_stable(self.interner())).expect("capacity");
result.hash(&mut state);
write!(buf, "{:?}", self.interner().dbg_stable(result)).expect("capacity");
// NOTE: due to randomness, this will not be consistent between runs
let mut state = roc_collections::all::BuildHasher::default().build_hasher();
buf.hash(&mut state);
let interns = self.interns();
let ident_string = symbol.as_str(interns);
@ -347,7 +366,7 @@ trait Backend<'a> {
// the functions from the generates #help module (refcounting, equality) is always suffixed
// with 1. That is fine, they are always unique anyway.
if ident_string.contains("#help") {
format!("{}_{}_1", module_string, ident_string)
format!("{module_string}_{ident_string}_1")
} else {
format!("{}_{}_{}", module_string, ident_string, state.finish())
}
@ -384,13 +403,13 @@ trait Backend<'a> {
fn increment_fn_pointer(&mut self, layout: InLayout<'a>) -> Symbol {
let box_layout = self
.interner_mut()
.insert_direct_no_semantic(LayoutRepr::Boxed(layout));
.insert_direct_no_semantic(LayoutRepr::Ptr(layout));
let element_increment = self.debug_symbol("element_increment");
let element_increment_symbol = self.build_indirect_inc(layout);
let element_increment_string = self.function_symbol_to_string(
element_increment_symbol,
let element_increment_string = self.lambda_name_to_string(
LambdaName::no_niche(element_increment_symbol),
[box_layout].into_iter(),
None,
Layout::UNIT,
@ -404,13 +423,13 @@ trait Backend<'a> {
fn decrement_fn_pointer(&mut self, layout: InLayout<'a>) -> Symbol {
let box_layout = self
.interner_mut()
.insert_direct_no_semantic(LayoutRepr::Boxed(layout));
.insert_direct_no_semantic(LayoutRepr::Ptr(layout));
let element_decrement = self.debug_symbol("element_decrement");
let element_decrement_symbol = self.build_indirect_dec(layout);
let element_decrement_string = self.function_symbol_to_string(
element_decrement_symbol,
let element_decrement_string = self.lambda_name_to_string(
LambdaName::no_niche(element_decrement_symbol),
[box_layout].into_iter(),
None,
Layout::UNIT,
@ -445,6 +464,11 @@ trait Backend<'a> {
/// Used for generating wrappers for malloc/realloc/free
fn build_wrapped_jmp(&mut self) -> (&'a [u8], u64);
// use for roc_panic
fn build_roc_setjmp(&mut self) -> &'a [u8];
fn build_roc_longjmp(&mut self) -> &'a [u8];
fn build_roc_panic(&mut self) -> (&'a [u8], Vec<'a, Relocation>);
/// build_proc creates a procedure and outputs it to the wrapped object writer.
/// Returns the procedure bytes, its relocations, and the names of the refcounting functions it references.
fn build_proc(
@ -452,8 +476,8 @@ trait Backend<'a> {
proc: Proc<'a>,
layout_ids: &mut LayoutIds<'a>,
) -> (Vec<u8>, Vec<Relocation>, Vec<'a, (Symbol, String)>) {
let proc_name = self.function_symbol_to_string(
proc.name.name(),
let proc_name = self.lambda_name_to_string(
proc.name,
proc.args.iter().map(|t| t.0),
proc.closure_data_layout,
proc.ret_layout,
@ -513,6 +537,29 @@ trait Backend<'a> {
self.return_symbol(sym, ret_layout);
self.free_symbols(stmt);
}
Stmt::Refcounting(ModifyRc::Free(symbol), following) => {
let dst = Symbol::DEV_TMP;
let layout = *self.layout_map().get(symbol).unwrap();
let alignment_bytes = self.interner().allocation_alignment_bytes(layout);
let alignment = self.debug_symbol("alignment");
self.load_literal_i32(&alignment, alignment_bytes as i32);
// NOTE: UTILS_FREE_DATA_PTR clears any tag id bits
self.build_fn_call(
&dst,
bitcode::UTILS_FREE_DATA_PTR.to_string(),
&[*symbol, alignment],
&[Layout::I64, Layout::I32],
&Layout::UNIT,
);
self.free_symbol(&dst);
self.free_symbol(&alignment);
self.build_stmt(layout_ids, following, ret_layout)
}
Stmt::Refcounting(modify, following) => {
let sym = modify.get_symbol();
let layout = *self.layout_map().get(&sym).unwrap();
@ -683,15 +730,15 @@ trait Backend<'a> {
// implementation in `build_builtin` inlines some of the symbols.
return self.build_builtin(
sym,
func_sym.name(),
*func_sym,
arguments,
arg_layouts,
ret_layout,
);
}
let fn_name = self.function_symbol_to_string(
func_sym.name(),
let fn_name = self.lambda_name_to_string(
*func_sym,
arg_layouts.iter().copied(),
None,
*ret_layout,
@ -702,6 +749,10 @@ trait Backend<'a> {
self.build_fn_call(sym, fn_name, arguments, arg_layouts, ret_layout)
}
CallType::ByPointer { .. } => {
todo_lambda_erasure!()
}
CallType::LowLevel { op: lowlevel, .. } => {
let mut arg_layouts: bumpalo::collections::Vec<InLayout<'a>> =
bumpalo::vec![in self.env().arena];
@ -785,6 +836,14 @@ trait Backend<'a> {
} => {
self.load_union_at_index(sym, structure, *tag_id, *index, union_layout);
}
Expr::UnionFieldPtrAtIndex {
structure,
tag_id,
union_layout,
index,
} => {
self.load_union_field_ptr_at_index(sym, structure, *tag_id, *index, union_layout);
}
Expr::GetTagId {
structure,
union_layout,
@ -795,39 +854,18 @@ trait Backend<'a> {
tag_layout,
tag_id,
arguments,
..
reuse,
} => {
self.load_literal_symbols(arguments);
self.tag(sym, arguments, tag_layout, *tag_id, None);
}
Expr::ExprBox { symbol: value } => {
let element_layout = match self.interner().get_repr(*layout) {
LayoutRepr::Boxed(boxed) => boxed,
_ => unreachable!("{:?}", self.interner().dbg(*layout)),
};
self.load_literal_symbols([*value].as_slice());
self.expr_box(*sym, *value, element_layout, None)
}
Expr::ExprUnbox { symbol: ptr } => {
let element_layout = *layout;
self.load_literal_symbols([*ptr].as_slice());
self.expr_unbox(*sym, *ptr, element_layout)
let reuse = reuse.map(|ru| ru.symbol);
self.tag(sym, arguments, tag_layout, *tag_id, reuse);
}
Expr::NullPointer => {
self.load_literal_i64(sym, 0);
}
Expr::Reuse {
tag_layout,
tag_id,
symbol: reused,
arguments,
..
} => {
self.load_literal_symbols(arguments);
self.tag(sym, arguments, tag_layout, *tag_id, Some(*reused));
}
Expr::FunctionPointer { .. } => todo_lambda_erasure!(),
Expr::ErasedMake { .. } => todo_lambda_erasure!(),
Expr::ErasedLoad { .. } => todo_lambda_erasure!(),
Expr::Reset { symbol, .. } => {
let layout = *self.layout_map().get(symbol).unwrap();
@ -868,6 +906,12 @@ trait Backend<'a> {
self.build_expr(sym, &new_expr, &Layout::BOOL)
}
Expr::Alloca {
initializer,
element_layout,
} => {
self.build_alloca(*sym, *initializer, *element_layout);
}
Expr::RuntimeErrorFunction(_) => todo!(),
}
}
@ -960,7 +1004,7 @@ trait Backend<'a> {
ret_layout,
),
LowLevel::NumMul => self.build_num_mul(sym, &args[0], &args[1], ret_layout),
LowLevel::NumMulWrap => self.build_num_mul(sym, &args[0], &args[1], ret_layout),
LowLevel::NumMulWrap => self.build_num_mul_wrap(sym, &args[0], &args[1], ret_layout),
LowLevel::NumDivTruncUnchecked | LowLevel::NumDivFrac => {
debug_assert_eq!(
2,
@ -1482,16 +1526,16 @@ trait Backend<'a> {
arg_layouts,
ret_layout,
),
LowLevel::StrTrimLeft => self.build_fn_call(
LowLevel::StrTrimStart => self.build_fn_call(
sym,
bitcode::STR_TRIM_LEFT.to_string(),
bitcode::STR_TRIM_START.to_string(),
args,
arg_layouts,
ret_layout,
),
LowLevel::StrTrimRight => self.build_fn_call(
LowLevel::StrTrimEnd => self.build_fn_call(
sym,
bitcode::STR_TRIM_RIGHT.to_string(),
bitcode::STR_TRIM_END.to_string(),
args,
arg_layouts,
ret_layout,
@ -1572,14 +1616,22 @@ trait Backend<'a> {
self.build_ptr_cast(sym, &args[0])
}
LowLevel::PtrWrite => {
let element_layout = match self.interner().get_repr(*ret_layout) {
LayoutRepr::Boxed(boxed) => boxed,
LowLevel::PtrStore => {
let element_layout = match self.interner().get_repr(arg_layouts[0]) {
LayoutRepr::Ptr(inner) => inner,
_ => unreachable!("cannot write to {:?}", self.interner().dbg(*ret_layout)),
};
self.build_ptr_write(*sym, args[0], args[1], element_layout);
self.build_ptr_store(*sym, args[0], args[1], element_layout);
}
LowLevel::PtrLoad => {
self.build_ptr_load(*sym, args[0], *ret_layout);
}
LowLevel::PtrClearTagId => {
self.build_ptr_clear_tag_id(*sym, args[0]);
}
LowLevel::RefCountDecRcPtr => self.build_fn_call(
sym,
bitcode::UTILS_DECREF_RC_PTR.to_string(),
@ -1615,6 +1667,23 @@ trait Backend<'a> {
arg_layouts,
ret_layout,
),
LowLevel::SetJmp => self.build_fn_call(
sym,
String::from("roc_setjmp"),
args,
arg_layouts,
ret_layout,
),
LowLevel::LongJmp => self.build_fn_call(
sym,
String::from("roc_longjmp"),
args,
arg_layouts,
ret_layout,
),
LowLevel::SetLongJmpBuffer => {
self.build_data_pointer(sym, String::from("setlongjmp_buffer"));
}
LowLevel::DictPseudoSeed => self.build_fn_call(
sym,
bitcode::UTILS_DICT_PSEUDO_SEED.to_string(),
@ -1807,6 +1876,10 @@ trait Backend<'a> {
);
}
LowLevel::NumCompare => {
self.build_num_cmp(sym, &args[0], &args[1], &arg_layouts[0]);
}
x => todo!("low level, {:?}", x),
}
}
@ -1816,12 +1889,12 @@ trait Backend<'a> {
fn build_builtin(
&mut self,
sym: &Symbol,
func_sym: Symbol,
func_name: LambdaName,
args: &'a [Symbol],
arg_layouts: &[InLayout<'a>],
ret_layout: &InLayout<'a>,
) {
match func_sym {
match func_name.name() {
Symbol::NUM_IS_ZERO => {
debug_assert_eq!(
1,
@ -1844,8 +1917,8 @@ trait Backend<'a> {
}
Symbol::LIST_GET | Symbol::LIST_SET | Symbol::LIST_REPLACE | Symbol::LIST_APPEND => {
// TODO: This is probably simple enough to be worth inlining.
let fn_name = self.function_symbol_to_string(
func_sym,
let fn_name = self.lambda_name_to_string(
func_name,
arg_layouts.iter().copied(),
None,
*ret_layout,
@ -1876,8 +1949,8 @@ trait Backend<'a> {
}
Symbol::STR_IS_VALID_SCALAR => {
// just call the function
let fn_name = self.function_symbol_to_string(
func_sym,
let fn_name = self.lambda_name_to_string(
func_name,
arg_layouts.iter().copied(),
None,
*ret_layout,
@ -1888,8 +1961,8 @@ trait Backend<'a> {
}
_other => {
// just call the function
let fn_name = self.function_symbol_to_string(
func_sym,
let fn_name = self.lambda_name_to_string(
func_name,
arg_layouts.iter().copied(),
None,
*ret_layout,
@ -1913,6 +1986,7 @@ trait Backend<'a> {
);
fn build_fn_pointer(&mut self, dst: &Symbol, fn_name: String);
fn build_data_pointer(&mut self, dst: &Symbol, data_name: String);
/// Move a returned value into `dst`
fn move_return_value(&mut self, dst: &Symbol, ret_layout: &InLayout<'a>);
@ -1964,6 +2038,15 @@ trait Backend<'a> {
/// build_num_mul stores `src1 * src2` into dst.
fn build_num_mul(&mut self, dst: &Symbol, src1: &Symbol, src2: &Symbol, layout: &InLayout<'a>);
/// build_num_mul_wrap stores `src1 * src2` into dst.
fn build_num_mul_wrap(
&mut self,
dst: &Symbol,
src1: &Symbol,
src2: &Symbol,
layout: &InLayout<'a>,
);
/// build_num_mul stores `src1 / src2` into dst.
fn build_num_div(&mut self, dst: &Symbol, src1: &Symbol, src2: &Symbol, layout: &InLayout<'a>);
@ -2048,6 +2131,14 @@ trait Backend<'a> {
/// build_not stores the result of `!src` into dst.
fn build_not(&mut self, dst: &Symbol, src: &Symbol, arg_layout: &InLayout<'a>);
fn build_num_cmp(
&mut self,
dst: &Symbol,
src1: &Symbol,
src2: &Symbol,
arg_layout: &InLayout<'a>,
);
/// build_num_lt stores the result of `src1 < src2` into dst.
fn build_num_lt(
&mut self,
@ -2187,7 +2278,7 @@ trait Backend<'a> {
/// build_refcount_getptr loads the pointer to the reference count of src into dst.
fn build_ptr_cast(&mut self, dst: &Symbol, src: &Symbol);
fn build_ptr_write(
fn build_ptr_store(
&mut self,
sym: Symbol,
ptr: Symbol,
@ -2195,6 +2286,12 @@ trait Backend<'a> {
element_layout: InLayout<'a>,
);
fn build_ptr_load(&mut self, sym: Symbol, ptr: Symbol, element_layout: InLayout<'a>);
fn build_ptr_clear_tag_id(&mut self, sym: Symbol, ptr: Symbol);
fn build_alloca(&mut self, sym: Symbol, value: Option<Symbol>, element_layout: InLayout<'a>);
/// literal_map gets the map from symbol to literal and layout, used for lazy loading and literal folding.
fn literal_map(&mut self) -> &mut MutMap<Symbol, (*const Literal<'a>, *const InLayout<'a>)>;
@ -2272,6 +2369,16 @@ trait Backend<'a> {
union_layout: &UnionLayout<'a>,
);
/// load_union_at_index loads into `sym` the value at `index` for `tag_id`.
fn load_union_field_ptr_at_index(
&mut self,
sym: &Symbol,
structure: &Symbol,
tag_id: TagIdIntType,
index: u64,
union_layout: &UnionLayout<'a>,
);
/// get_tag_id loads the tag id from a the union.
fn get_tag_id(&mut self, sym: &Symbol, structure: &Symbol, union_layout: &UnionLayout<'a>);

View file

@ -74,6 +74,23 @@ pub fn build_module<'a, 'r>(
),
)
}
Triple {
architecture: TargetArch::X86_64,
binary_format: TargetBF::Coff,
..
} if cfg!(feature = "target-x86_64") => {
let backend = new_backend_64bit::<
x86_64::X86_64GeneralReg,
x86_64::X86_64FloatReg,
x86_64::X86_64Assembler,
x86_64::X86_64WindowsFastcall,
>(env, TargetInfo::default_x86_64(), interns, layout_interner);
build_object(
procedures,
backend,
Object::new(BinaryFormat::Coff, Architecture::X86_64, Endianness::Little),
)
}
Triple {
architecture: TargetArch::Aarch64(_),
binary_format: TargetBF::Elf,
@ -118,6 +135,111 @@ pub fn build_module<'a, 'r>(
}
}
fn define_setlongjmp_buffer(output: &mut Object) -> SymbolId {
let bss_section = output.section_id(StandardSection::Data);
// 8 registers + 3 words for a RocStr
// TODO 50 is the wrong size here, look at implementation and put correct value in here
const SIZE: usize = (8 + 50) * core::mem::size_of::<u64>();
let symbol = Symbol {
name: b"setlongjmp_buffer".to_vec(),
value: 0,
size: SIZE as u64,
kind: SymbolKind::Data,
scope: SymbolScope::Dynamic,
weak: false,
section: SymbolSection::Section(bss_section),
flags: SymbolFlags::None,
};
let symbol_id = output.add_symbol(symbol);
output.add_symbol_data(symbol_id, bss_section, &[0x00; SIZE], 8);
symbol_id
}
fn generate_setjmp<'a, B: Backend<'a>>(backend: &mut B, output: &mut Object) {
let text_section = output.section_id(StandardSection::Text);
let proc_symbol = Symbol {
name: b"roc_setjmp".to_vec(),
value: 0,
size: 0,
kind: SymbolKind::Text,
scope: SymbolScope::Dynamic,
weak: false,
section: SymbolSection::Section(text_section),
flags: SymbolFlags::None,
};
let proc_id = output.add_symbol(proc_symbol);
let proc_data = backend.build_roc_setjmp();
output.add_symbol_data(proc_id, text_section, proc_data, 16);
}
fn generate_longjmp<'a, B: Backend<'a>>(backend: &mut B, output: &mut Object) {
let text_section = output.section_id(StandardSection::Text);
let proc_symbol = Symbol {
name: b"roc_longjmp".to_vec(),
value: 0,
size: 0,
kind: SymbolKind::Text,
scope: SymbolScope::Dynamic,
weak: false,
section: SymbolSection::Section(text_section),
flags: SymbolFlags::None,
};
let proc_id = output.add_symbol(proc_symbol);
let proc_data = backend.build_roc_longjmp();
output.add_symbol_data(proc_id, text_section, proc_data, 16);
}
// a roc_panic to be used in tests; relies on setjmp/longjmp
fn generate_roc_panic<'a, B: Backend<'a>>(backend: &mut B, output: &mut Object) {
let text_section = output.section_id(StandardSection::Text);
let proc_symbol = Symbol {
name: b"roc_panic".to_vec(),
value: 0,
size: 0,
kind: SymbolKind::Text,
scope: SymbolScope::Dynamic,
weak: false,
section: SymbolSection::Section(text_section),
flags: SymbolFlags::None,
};
let proc_id = output.add_symbol(proc_symbol);
let (proc_data, relocs) = backend.build_roc_panic();
let proc_offset = output.add_symbol_data(proc_id, text_section, proc_data, 16);
for r in relocs {
let relocation = match r {
Relocation::LinkedData { offset, name } => {
if let Some(sym_id) = output.symbol_id(name.as_bytes()) {
write::Relocation {
offset: offset + proc_offset,
size: 32,
kind: RelocationKind::GotRelative,
encoding: RelocationEncoding::Generic,
symbol: sym_id,
addend: -4,
}
} else {
internal_error!("failed to find data symbol for {:?}", name);
}
}
Relocation::LocalData { .. }
| Relocation::LinkedFunction { .. }
| Relocation::JmpToReturn { .. } => {
unreachable!("not currently created by build_roc_panic")
}
};
output.add_relocation(text_section, relocation).unwrap();
}
}
fn generate_wrapper<'a, B: Backend<'a>>(
backend: &mut B,
output: &mut Object,
@ -190,6 +312,12 @@ fn build_object<'a, B: Backend<'a>>(
);
*/
define_setlongjmp_buffer(&mut output);
generate_roc_panic(&mut backend, &mut output);
generate_setjmp(&mut backend, &mut output);
generate_longjmp(&mut backend, &mut output);
if backend.env().mode.generate_allocators() {
generate_wrapper(
&mut backend,
@ -209,12 +337,7 @@ fn build_object<'a, B: Backend<'a>>(
"roc_dealloc".into(),
"free".into(),
);
generate_wrapper(
&mut backend,
&mut output,
"roc_panic".into(),
"roc_builtins.utils.test_panic".into(),
);
// Extra symbols only required on unix systems.
if matches!(output.format(), BinaryFormat::Elf | BinaryFormat::MachO) {
generate_wrapper(
@ -230,6 +353,15 @@ fn build_object<'a, B: Backend<'a>>(
"roc_shm_open".into(),
"shm_open".into(),
);
} else if matches!(output.format(), BinaryFormat::Coff) {
// TODO figure out why this symbol is required, it should not be required
// Without this it does not build on Windows
generate_wrapper(
&mut backend,
&mut output,
"roc_getppid".into(),
"malloc".into(),
);
}
}
@ -245,6 +377,18 @@ fn build_object<'a, B: Backend<'a>>(
let exposed_proc = build_exposed_proc(&mut backend, &proc);
let exposed_generic_proc = build_exposed_generic_proc(&mut backend, &proc);
let (module_id, layout_interner, interns, code_gen_help, _) =
backend.module_interns_helpers_mut();
let ident_ids = interns.all_ident_ids.get_mut(&module_id).unwrap();
let test_helper = roc_mono::code_gen_help::test_helper(
code_gen_help,
ident_ids,
layout_interner,
&proc,
);
#[cfg(debug_assertions)]
{
let module_id = exposed_generic_proc.name.name().module_id();
@ -256,6 +400,18 @@ fn build_object<'a, B: Backend<'a>>(
module_id.register_debug_idents(ident_ids);
}
// println!("{}", test_helper.to_pretty(backend.interner(), 200, true));
build_proc_symbol(
&mut output,
&mut layout_ids,
&mut procs,
&mut backend,
layout,
test_helper,
Exposed::TestMain,
);
build_proc_symbol(
&mut output,
&mut layout_ids,
@ -343,8 +499,8 @@ fn build_object<'a, B: Backend<'a>>(
for ((sym, layout), proc) in helper_symbols_and_layouts.into_iter().zip(helper_procs) {
debug_assert_eq!(sym, proc.name.name());
let fn_name = backend.function_symbol_to_string(
sym,
let fn_name = backend.lambda_name_to_string(
LambdaName::no_niche(sym),
layout.arguments.iter().copied(),
None,
layout.result,
@ -443,7 +599,7 @@ fn build_exposed_proc<'a, B: Backend<'a>>(backend: &mut B, proc: &Proc<'a>) -> P
closure_data_layout: None,
ret_layout: proc.ret_layout,
is_self_recursive: roc_mono::ir::SelfRecursive::NotSelfRecursive,
host_exposed_layouts: roc_mono::ir::HostExposedLayouts::NotHostExposed,
is_erased: proc.is_erased,
}
}
@ -464,7 +620,7 @@ fn build_exposed_generic_proc<'a, B: Backend<'a>>(backend: &mut B, proc: &Proc<'
let box_layout = backend
.interner_mut()
.insert_direct_no_semantic(roc_mono::layout::LayoutRepr::Boxed(proc.ret_layout));
.insert_direct_no_semantic(roc_mono::layout::LayoutRepr::Ptr(proc.ret_layout));
let mut args = bumpalo::collections::Vec::new_in(arena);
args.extend(proc.args);
@ -485,7 +641,7 @@ fn build_exposed_generic_proc<'a, B: Backend<'a>>(backend: &mut B, proc: &Proc<'
let box_write = Call {
call_type: roc_mono::ir::CallType::LowLevel {
op: roc_module::low_level::LowLevel::PtrWrite,
op: roc_module::low_level::LowLevel::PtrStore,
update_mode: UpdateModeId::BACKEND_DUMMY,
},
arguments: arena.alloc([arg_generic, s1]),
@ -524,7 +680,7 @@ fn build_exposed_generic_proc<'a, B: Backend<'a>>(backend: &mut B, proc: &Proc<'
closure_data_layout: None,
ret_layout: roc_mono::layout::Layout::UNIT,
is_self_recursive: roc_mono::ir::SelfRecursive::NotSelfRecursive,
host_exposed_layouts: roc_mono::ir::HostExposedLayouts::NotHostExposed,
is_erased: proc.is_erased,
}
}
@ -533,6 +689,7 @@ enum Exposed {
ExposedGeneric,
Exposed,
NotExposed,
TestMain,
}
fn build_proc_symbol<'a, B: Backend<'a>>(
@ -559,12 +716,13 @@ fn build_proc_symbol<'a, B: Backend<'a>>(
Exposed::Exposed => layout_ids
.get_toplevel(sym, &layout)
.to_exposed_symbol_string(sym, backend.interns()),
Exposed::NotExposed => backend.function_symbol_to_string(
sym,
Exposed::NotExposed => backend.lambda_name_to_string(
proc.name,
layout.arguments.iter().copied(),
None,
layout.result,
),
Exposed::TestMain => String::from("test_main"),
};
let proc_symbol = Symbol {
@ -575,7 +733,7 @@ fn build_proc_symbol<'a, B: Backend<'a>>(
// TODO: Depending on whether we are building a static or dynamic lib, this should change.
// We should use Dynamic -> anyone, Linkage -> static link, Compilation -> this module only.
scope: match exposed {
Exposed::ExposedGeneric | Exposed::Exposed => SymbolScope::Dynamic,
Exposed::ExposedGeneric | Exposed::Exposed | Exposed::TestMain => SymbolScope::Dynamic,
Exposed::NotExposed => SymbolScope::Linkage,
},
weak: false,
@ -605,7 +763,7 @@ fn build_proc<'a, B: Backend<'a>>(
let elfreloc = match reloc {
Relocation::LocalData { offset, data } => {
let data_symbol = write::Symbol {
name: format!("{}.data{}", fn_name, local_data_index)
name: format!("{fn_name}.data{local_data_index}")
.as_bytes()
.to_vec(),
value: 0,

Some files were not shown because too many files have changed in this diff Show more