Merge branch 'main' into wrapped

This commit is contained in:
Ayaz 2024-12-08 13:54:43 -05:00 committed by GitHub
commit 793a95264d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
765 changed files with 14005 additions and 2968 deletions

View file

@ -74,19 +74,29 @@ comptime {
}
}
fn testing_roc_alloc(size: usize, _: u32) callconv(.C) ?*anyopaque {
// We store an extra usize which is the size of the full allocation.
const full_size = size + @sizeOf(usize);
var raw_ptr = (std.testing.allocator.alloc(u8, full_size) catch unreachable).ptr;
@as([*]usize, @alignCast(@ptrCast(raw_ptr)))[0] = full_size;
raw_ptr += @sizeOf(usize);
const ptr = @as(?*anyopaque, @ptrCast(raw_ptr));
fn testing_roc_alloc(size: usize, nominal_alignment: u32) callconv(.C) ?*anyopaque {
const real_alignment = 16;
if (nominal_alignment > real_alignment) {
@panic("alignments larger than that of usize are not currently supported");
}
// We store an extra usize which is the size of the data plus the size of the size, directly before the data.
// We need enough clocks of the alignment size to fit this (usually this will be one)
const size_of_size = @sizeOf(usize);
const alignments_needed = size_of_size / real_alignment + comptime if (size_of_size % real_alignment == 0) 0 else 1;
const extra_bytes = alignments_needed * size_of_size;
const full_size = size + extra_bytes;
const whole_ptr = (std.testing.allocator.alignedAlloc(u8, real_alignment, full_size) catch unreachable).ptr;
const written_to_size = size + size_of_size;
@as([*]align(real_alignment) usize, @ptrCast(whole_ptr))[extra_bytes - size_of_size] = written_to_size;
const data_ptr = @as(?*anyopaque, @ptrCast(whole_ptr + extra_bytes));
if (DEBUG_TESTING_ALLOC and builtin.target.cpu.arch != .wasm32) {
std.debug.print("+ alloc {*}: {} bytes\n", .{ ptr, size });
std.debug.print("+ alloc {*}: {} bytes\n", .{ data_ptr, size });
}
return ptr;
return data_ptr;
}
fn testing_roc_realloc(c_ptr: *anyopaque, new_size: usize, old_size: usize, _: u32) callconv(.C) ?*anyopaque {
@ -107,9 +117,16 @@ fn testing_roc_realloc(c_ptr: *anyopaque, new_size: usize, old_size: usize, _: u
}
fn testing_roc_dealloc(c_ptr: *anyopaque, _: u32) callconv(.C) void {
const raw_ptr = @as([*]u8, @ptrCast(c_ptr)) - @sizeOf(usize);
const full_size = @as([*]usize, @alignCast(@ptrCast(raw_ptr)))[0];
const slice = raw_ptr[0..full_size];
const alignment = 16;
const size_of_size = @sizeOf(usize);
const alignments_needed = size_of_size / alignment + comptime if (size_of_size % alignment == 0) 0 else 1;
const extra_bytes = alignments_needed * size_of_size;
const byte_array = @as([*]u8, @ptrCast(c_ptr)) - extra_bytes;
const allocation_ptr = @as([*]align(alignment) u8, @alignCast(byte_array));
const offset_from_allocation_to_size = extra_bytes - size_of_size;
const size_of_data_and_size = @as([*]usize, @alignCast(@ptrCast(allocation_ptr)))[offset_from_allocation_to_size];
const full_size = size_of_data_and_size + offset_from_allocation_to_size;
const slice = allocation_ptr[0..full_size];
if (DEBUG_TESTING_ALLOC and builtin.target.cpu.arch != .wasm32) {
std.debug.print("💀 dealloc {*}\n", .{slice.ptr});

View file

@ -5,6 +5,7 @@ use std::sync::Arc;
use crate::abilities::SpecializationId;
use crate::exhaustive::{ExhaustiveContext, SketchedRows};
use crate::expected::{Expected, PExpected};
use crate::expr::TryKind;
use roc_collections::soa::{index_push_new, slice_extend_new};
use roc_module::ident::{IdentSuffix, TagName};
use roc_module::symbol::{ModuleId, Symbol};
@ -31,6 +32,7 @@ pub struct Constraints {
pub cycles: Vec<Cycle>,
pub fx_call_constraints: Vec<FxCallConstraint>,
pub fx_suffix_constraints: Vec<FxSuffixConstraint>,
pub try_target_constraints: Vec<TryTargetConstraint>,
}
impl std::fmt::Debug for Constraints {
@ -87,6 +89,7 @@ impl Constraints {
let cycles = Vec::new();
let fx_call_constraints = Vec::with_capacity(16);
let fx_suffix_constraints = Vec::new();
let result_type_constraints = Vec::new();
categories.extend([
Category::Record,
@ -103,7 +106,6 @@ impl Constraints {
Category::List,
Category::Str,
Category::Character,
Category::Return,
]);
pattern_categories.extend([
@ -138,6 +140,7 @@ impl Constraints {
cycles,
fx_call_constraints,
fx_suffix_constraints,
try_target_constraints: result_type_constraints,
}
}
@ -635,6 +638,27 @@ impl Constraints {
Constraint::FlexToPure(fx_var)
}
pub fn try_target(
&mut self,
result_type_index: TypeOrVar,
ok_payload_var: Variable,
err_payload_var: Variable,
region: Region,
kind: TryKind,
) -> Constraint {
let constraint = TryTargetConstraint {
target_type_index: result_type_index,
ok_payload_var,
err_payload_var,
region,
kind,
};
let constraint_index = index_push_new(&mut self.try_target_constraints, constraint);
Constraint::TryTarget(constraint_index)
}
pub fn contains_save_the_environment(&self, constraint: &Constraint) -> bool {
match constraint {
Constraint::SaveTheEnvironment => true,
@ -660,6 +684,7 @@ impl Constraints {
| Constraint::Lookup(..)
| Constraint::Pattern(..)
| Constraint::ExpectEffectful(..)
| Constraint::TryTarget(_)
| Constraint::FxCall(_)
| Constraint::FxSuffix(_)
| Constraint::FlexToPure(_)
@ -843,6 +868,8 @@ pub enum Constraint {
FlexToPure(Variable),
/// Expect statement or ignored def to be effectful
ExpectEffectful(Variable, ExpectEffectfulReason, Region),
/// Expect value to be some kind of Result
TryTarget(Index<TryTargetConstraint>),
/// Used for things that always unify, e.g. blanks and runtime errors
True,
SaveTheEnvironment,
@ -909,6 +936,15 @@ pub struct IncludesTag {
pub region: Region,
}
#[derive(Debug, Clone)]
pub struct TryTargetConstraint {
pub target_type_index: TypeOrVar,
pub ok_payload_var: Variable,
pub err_payload_var: Variable,
pub region: Region,
pub kind: TryKind,
}
#[derive(Debug, Clone, Copy)]
pub struct Cycle {
pub def_names: Slice<(Symbol, Region)>,
@ -1000,6 +1036,9 @@ impl std::fmt::Debug for Constraint {
Self::FlexToPure(arg0) => {
write!(f, "FlexToPure({arg0:?})")
}
Self::TryTarget(arg0) => {
write!(f, "ExpectResultType({arg0:?})")
}
Self::True => write!(f, "True"),
Self::SaveTheEnvironment => write!(f, "SaveTheEnvironment"),
Self::Let(arg0, arg1) => f.debug_tuple("Let").field(arg0).field(arg1).finish(),

View file

@ -479,7 +479,7 @@ fn deep_copy_expr_help<C: CopyEnv>(env: &mut C, copied: &mut Vec<Variable>, expr
fx_type: sub!(*fx_type),
early_returns: early_returns
.iter()
.map(|(var, region)| (sub!(*var), *region))
.map(|(var, region, type_)| (sub!(*var), *region, *type_))
.collect(),
name: *name,
captured_symbols: captured_symbols
@ -718,6 +718,24 @@ fn deep_copy_expr_help<C: CopyEnv>(env: &mut C, copied: &mut Vec<Variable>, expr
symbol: *symbol,
},
Try {
result_expr,
result_var,
return_var,
ok_payload_var,
err_payload_var,
err_ext_var,
kind,
} => Try {
result_expr: Box::new(result_expr.map(|e| go_help!(e))),
result_var: sub!(*result_var),
return_var: sub!(*return_var),
ok_payload_var: sub!(*ok_payload_var),
err_payload_var: sub!(*err_payload_var),
err_ext_var: sub!(*err_ext_var),
kind: *kind,
},
RuntimeError(err) => RuntimeError(err.clone()),
}
}

View file

@ -452,6 +452,7 @@ fn expr<'a>(c: &Ctx, p: EPrec, f: &'a Arena<'a>, e: &'a Expr) -> DocBuilder<'a,
),
Dbg { .. } => todo!(),
Expect { .. } => todo!(),
Try { .. } => todo!(),
Return { .. } => todo!(),
RuntimeError(_) => todo!(),
}

View file

@ -11,8 +11,8 @@ use roc_module::called_via::{BinOp, CalledVia};
use roc_module::ident::ModuleName;
use roc_parse::ast::Expr::{self, *};
use roc_parse::ast::{
is_expr_suffixed, AssignedField, Collection, Defs, ModuleImportParams, Pattern, StrLiteral,
StrSegment, TypeAnnotation, ValueDef, WhenBranch,
is_expr_suffixed, AssignedField, Collection, Defs, ModuleImportParams, Pattern, ResultTryKind,
StrLiteral, StrSegment, TryTarget, TypeAnnotation, ValueDef, WhenBranch,
};
use roc_problem::can::Problem;
use roc_region::all::{Loc, Region};
@ -44,7 +44,10 @@ fn new_op_call_expr<'a>(
match right_without_spaces {
Try => {
let desugared_left = desugar_expr(env, scope, left);
return desugar_try_expr(env, scope, desugared_left);
return Loc::at(
region,
Expr::LowLevelTry(desugared_left, ResultTryKind::KeywordPrefix),
);
}
Apply(&Loc { value: Try, .. }, arguments, _called_via) => {
let try_fn = desugar_expr(env, scope, arguments.first().unwrap());
@ -58,13 +61,73 @@ fn new_op_call_expr<'a>(
.map(|a| desugar_expr(env, scope, a)),
);
return desugar_try_expr(
env,
scope,
env.arena.alloc(Loc::at(
right.region,
Expr::Apply(try_fn, args.into_bump_slice(), CalledVia::Try),
)),
return Loc::at(
region,
Expr::LowLevelTry(
env.arena.alloc(Loc::at(
region,
Expr::Apply(try_fn, args.into_bump_slice(), CalledVia::Try),
)),
ResultTryKind::KeywordPrefix,
),
);
}
TrySuffix {
target: TryTarget::Result,
expr: fn_expr,
} => {
let loc_fn = env.arena.alloc(Loc::at(right.region, **fn_expr));
let function = desugar_expr(env, scope, loc_fn);
return Loc::at(
region,
LowLevelTry(
env.arena.alloc(Loc::at(
region,
Expr::Apply(
function,
env.arena.alloc([desugar_expr(env, scope, left)]),
CalledVia::Try,
),
)),
ResultTryKind::OperatorSuffix,
),
);
}
Apply(
&Loc {
value:
TrySuffix {
target: TryTarget::Result,
expr: fn_expr,
},
region: fn_region,
},
loc_args,
_called_via,
) => {
let loc_fn = env.arena.alloc(Loc::at(fn_region, *fn_expr));
let function = desugar_expr(env, scope, loc_fn);
let mut desugared_args = Vec::with_capacity_in(loc_args.len() + 1, env.arena);
desugared_args.push(desugar_expr(env, scope, left));
for loc_arg in &loc_args[..] {
desugared_args.push(desugar_expr(env, scope, loc_arg));
}
return Loc::at(
region,
LowLevelTry(
env.arena.alloc(Loc::at(
region,
Expr::Apply(
function,
desugared_args.into_bump_slice(),
CalledVia::Try,
),
)),
ResultTryKind::OperatorSuffix,
),
);
}
_ => {}
@ -207,7 +270,9 @@ fn desugar_value_def<'a>(
let desugared_expr = desugar_expr(env, scope, stmt_expr);
if !is_expr_suffixed(&desugared_expr.value) {
if !is_expr_suffixed(&desugared_expr.value)
&& !matches!(&desugared_expr.value, LowLevelTry(_, _))
{
env.problems.push(Problem::StmtAfterExpr(stmt_expr.region));
return ValueDef::StmtAfterExpr;
@ -457,23 +522,32 @@ pub fn desugar_expr<'a>(
env.arena.alloc(Loc { region, value })
}
// desugar the sub_expression, but leave the TrySuffix as this will
// be unwrapped later in desugar_value_def_suffixed
TrySuffix {
expr: sub_expr,
target,
} => {
let intermediate = env.arena.alloc(Loc::at(loc_expr.region, **sub_expr));
let new_sub_loc_expr = desugar_expr(env, scope, intermediate);
let new_sub_expr = env.arena.alloc(new_sub_loc_expr.value);
env.arena.alloc(Loc::at(
loc_expr.region,
TrySuffix {
expr: new_sub_expr,
target: *target,
},
))
match target {
TryTarget::Result => env.arena.alloc(Loc::at(
loc_expr.region,
LowLevelTry(new_sub_loc_expr, ResultTryKind::OperatorSuffix),
)),
TryTarget::Task => {
let new_sub_expr = env.arena.alloc(new_sub_loc_expr.value);
// desugar the sub_expression, but leave the TrySuffix as this will
// be unwrapped later in desugar_value_def_suffixed
env.arena.alloc(Loc::at(
loc_expr.region,
TrySuffix {
expr: new_sub_expr,
target: *target,
},
))
}
}
}
RecordAccess(sub_expr, paths) => {
let region = loc_expr.region;
@ -957,13 +1031,52 @@ pub fn desugar_expr<'a>(
desugared_args.push(desugar_expr(env, scope, loc_arg));
}
let args_region =
Region::span_across(&loc_args[0].region, &loc_args[loc_args.len() - 1].region);
env.arena.alloc(Loc::at(
loc_expr.region,
args_region,
Expr::Apply(function, desugared_args.into_bump_slice(), CalledVia::Try),
))
};
env.arena.alloc(desugar_try_expr(env, scope, result_expr))
env.arena.alloc(Loc::at(
loc_expr.region,
Expr::LowLevelTry(result_expr, ResultTryKind::KeywordPrefix),
))
}
Apply(
Loc {
value:
TrySuffix {
target: TryTarget::Result,
expr: fn_expr,
},
region: fn_region,
},
loc_args,
_called_via,
) => {
let loc_fn = env.arena.alloc(Loc::at(*fn_region, **fn_expr));
let function = desugar_expr(env, scope, loc_fn);
let mut desugared_args = Vec::with_capacity_in(loc_args.len(), env.arena);
for loc_arg in &loc_args[..] {
desugared_args.push(desugar_expr(env, scope, loc_arg));
}
let args_region =
Region::span_across(&loc_args[0].region, &loc_args[loc_args.len() - 1].region);
let result_expr = env.arena.alloc(Loc::at(
args_region,
Expr::Apply(function, desugared_args.into_bump_slice(), CalledVia::Try),
));
env.arena.alloc(Loc::at(
loc_expr.region,
Expr::LowLevelTry(result_expr, ResultTryKind::OperatorSuffix),
))
}
Apply(loc_fn, loc_args, called_via) => {
let mut desugared_args = Vec::with_capacity_in(loc_args.len(), env.arena);
@ -1104,10 +1217,21 @@ pub fn desugar_expr<'a>(
// Allow naked dbg, necessary for piping values into dbg with the `Pizza` binop
loc_expr
}
DbgStmt(condition, continuation) => {
DbgStmt {
first: condition,
extra_args,
continuation,
} => {
let desugared_condition = &*env.arena.alloc(desugar_expr(env, scope, condition));
let desugared_continuation = &*env.arena.alloc(desugar_expr(env, scope, continuation));
if let Some(last) = extra_args.last() {
let args_region = Region::span_across(&condition.region, &last.region);
env.problem(Problem::OverAppliedDbg {
region: args_region,
});
}
env.arena.alloc(Loc {
value: *desugar_dbg_stmt(env, desugared_condition, desugared_continuation),
region: loc_expr.region,
@ -1123,77 +1247,11 @@ pub fn desugar_expr<'a>(
})
}
// note this only exists after desugaring
LowLevelDbg(_, _, _) => loc_expr,
// note these only exist after desugaring
LowLevelDbg(_, _, _) | LowLevelTry(_, _) => loc_expr,
}
}
pub fn desugar_try_expr<'a>(
env: &mut Env<'a>,
scope: &mut Scope,
result_expr: &'a Loc<Expr<'a>>,
) -> Loc<Expr<'a>> {
let region = result_expr.region;
let ok_symbol = env.arena.alloc(scope.gen_unique_symbol_name().to_string());
let err_symbol = env.arena.alloc(scope.gen_unique_symbol_name().to_string());
let ok_branch = env.arena.alloc(WhenBranch {
patterns: env.arena.alloc([Loc::at(
region,
Pattern::Apply(
env.arena.alloc(Loc::at(region, Pattern::Tag("Ok"))),
env.arena
.alloc([Loc::at(region, Pattern::Identifier { ident: ok_symbol })]),
),
)]),
value: Loc::at(
region,
Expr::Var {
module_name: "",
ident: ok_symbol,
},
),
guard: None,
});
let err_branch = env.arena.alloc(WhenBranch {
patterns: env.arena.alloc([Loc::at(
region,
Pattern::Apply(
env.arena.alloc(Loc::at(region, Pattern::Tag("Err"))),
env.arena
.alloc([Loc::at(region, Pattern::Identifier { ident: err_symbol })]),
),
)]),
value: Loc::at(
region,
Expr::Return(
env.arena.alloc(Loc::at(
region,
Expr::Apply(
env.arena.alloc(Loc::at(region, Expr::Tag("Err"))),
&*env.arena.alloc([&*env.arena.alloc(Loc::at(
region,
Expr::Var {
module_name: "",
ident: err_symbol,
},
))]),
CalledVia::Try,
),
)),
None,
),
),
guard: None,
});
Loc::at(
region,
Expr::When(result_expr, &*env.arena.alloc([&*ok_branch, &*err_branch])),
)
}
fn desugar_str_segments<'a>(
env: &mut Env<'a>,
scope: &mut Scope,

View file

@ -19,14 +19,16 @@ use roc_module::called_via::CalledVia;
use roc_module::ident::{ForeignSymbol, Lowercase, TagName};
use roc_module::low_level::LowLevel;
use roc_module::symbol::{IdentId, ModuleId, Symbol};
use roc_parse::ast::{self, Defs, PrecedenceConflict, StrLiteral};
use roc_parse::ast::{self, Defs, PrecedenceConflict, ResultTryKind, StrLiteral};
use roc_parse::ident::Accessor;
use roc_parse::pattern::PatternType::*;
use roc_problem::can::{PrecedenceProblem, Problem, RuntimeError};
use roc_region::all::{Loc, Region};
use roc_types::num::SingleQuoteBound;
use roc_types::subs::{ExhaustiveMark, IllegalCycleMark, RedundantMark, VarStore, Variable};
use roc_types::types::{Alias, Category, IndexOrField, LambdaSet, OptAbleVar, Type};
use roc_types::types::{
Alias, Category, EarlyReturnKind, IndexOrField, LambdaSet, OptAbleVar, Type,
};
use soa::Index;
use std::fmt::{Debug, Display};
use std::path::PathBuf;
@ -328,6 +330,16 @@ pub enum Expr {
symbol: Symbol,
},
Try {
result_expr: Box<Loc<Expr>>,
result_var: Variable,
return_var: Variable,
ok_payload_var: Variable,
err_payload_var: Variable,
err_ext_var: Variable,
kind: TryKind,
},
Return {
return_value: Box<Loc<Expr>>,
return_var: Variable,
@ -337,6 +349,12 @@ pub enum Expr {
RuntimeError(RuntimeError),
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum TryKind {
KeywordPrefix,
OperatorSuffix,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ExpectLookup {
pub symbol: Symbol,
@ -403,14 +421,115 @@ impl Expr {
}
Self::Expect { .. } => Category::Expect,
Self::Crash { .. } => Category::Crash,
Self::Return { .. } => Category::Return,
Self::Return { .. } => Category::Return(EarlyReturnKind::Return),
Self::Dbg { .. } => Category::Expect,
Self::Try { .. } => Category::TrySuccess,
// these nodes place no constraints on the expression's type
Self::RuntimeError(..) => Category::Unknown,
}
}
pub fn contains_any_early_returns(&self) -> bool {
match self {
Self::Num { .. }
| Self::Int { .. }
| Self::Float { .. }
| Self::Str { .. }
| Self::IngestedFile { .. }
| Self::SingleQuote { .. }
| Self::Var { .. }
| Self::AbilityMember { .. }
| Self::ParamsVar { .. }
| Self::Closure(..)
| Self::EmptyRecord
| Self::RecordAccessor(_)
| Self::ZeroArgumentTag { .. }
| Self::OpaqueWrapFunction(_)
| Self::RuntimeError(..) => false,
Self::Return { .. } | Self::Try { .. } => true,
Self::List { loc_elems, .. } => loc_elems
.iter()
.any(|elem| elem.value.contains_any_early_returns()),
Self::When {
loc_cond, branches, ..
} => {
loc_cond.value.contains_any_early_returns()
|| branches.iter().any(|branch| {
branch
.guard
.as_ref()
.is_some_and(|guard| guard.value.contains_any_early_returns())
|| branch.value.value.contains_any_early_returns()
})
}
Self::If {
branches,
final_else,
..
} => {
final_else.value.contains_any_early_returns()
|| branches.iter().any(|(cond, then)| {
cond.value.contains_any_early_returns()
|| then.value.contains_any_early_returns()
})
}
Self::LetRec(defs, expr, _cycle_mark) => {
expr.value.contains_any_early_returns()
|| defs
.iter()
.any(|def| def.loc_expr.value.contains_any_early_returns())
}
Self::LetNonRec(def, expr) => {
def.loc_expr.value.contains_any_early_returns()
|| expr.value.contains_any_early_returns()
}
Self::Call(_func, args, _called_via) => args
.iter()
.any(|(_var, arg_expr)| arg_expr.value.contains_any_early_returns()),
Self::RunLowLevel { args, .. } | Self::ForeignCall { args, .. } => args
.iter()
.any(|(_var, arg_expr)| arg_expr.contains_any_early_returns()),
Self::Tuple { elems, .. } => elems
.iter()
.any(|(_var, loc_elem)| loc_elem.value.contains_any_early_returns()),
Self::Record { fields, .. } => fields
.iter()
.any(|(_field_name, field)| field.loc_expr.value.contains_any_early_returns()),
Self::RecordAccess { loc_expr, .. } => loc_expr.value.contains_any_early_returns(),
Self::TupleAccess { loc_expr, .. } => loc_expr.value.contains_any_early_returns(),
Self::RecordUpdate { updates, .. } => {
updates.iter().any(|(_field_name, field_update)| {
field_update.loc_expr.value.contains_any_early_returns()
})
}
Self::ImportParams(_module_id, _region, params) => params
.as_ref()
.is_some_and(|(_var, p)| p.contains_any_early_returns()),
Self::Tag { arguments, .. } => arguments
.iter()
.any(|(_var, arg)| arg.value.contains_any_early_returns()),
Self::OpaqueRef { argument, .. } => argument.1.value.contains_any_early_returns(),
Self::Crash { msg, .. } => msg.value.contains_any_early_returns(),
Self::Dbg {
loc_message,
loc_continuation,
..
} => {
loc_message.value.contains_any_early_returns()
|| loc_continuation.value.contains_any_early_returns()
}
Self::Expect {
loc_condition,
loc_continuation,
..
} => {
loc_condition.value.contains_any_early_returns()
|| loc_continuation.value.contains_any_early_returns()
}
}
}
}
/// Stores exhaustiveness-checking metadata for a closure argument that may
@ -445,7 +564,7 @@ pub struct ClosureData {
pub closure_type: Variable,
pub return_type: Variable,
pub fx_type: Variable,
pub early_returns: Vec<(Variable, Region)>,
pub early_returns: Vec<(Variable, Region, EarlyReturnKind)>,
pub name: Symbol,
pub captured_symbols: Vec<(Symbol, Variable)>,
pub recursive: Recursive,
@ -1245,7 +1364,7 @@ pub fn canonicalize_expr<'a>(
(loc_expr.value, output)
}
ast::Expr::DbgStmt(_, _) => {
ast::Expr::DbgStmt { .. } => {
internal_error!("DbgStmt should have been desugared by now")
}
ast::Expr::LowLevelDbg((source_location, source), message, continuation) => {
@ -1285,6 +1404,32 @@ pub fn canonicalize_expr<'a>(
output,
)
}
ast::Expr::LowLevelTry(loc_expr, kind) => {
let (loc_result_expr, output) =
canonicalize_expr(env, var_store, scope, loc_expr.region, &loc_expr.value);
let return_var = var_store.fresh();
scope
.early_returns
.push((return_var, loc_expr.region, EarlyReturnKind::Try));
(
Try {
result_expr: Box::new(loc_result_expr),
result_var: var_store.fresh(),
return_var,
ok_payload_var: var_store.fresh(),
err_payload_var: var_store.fresh(),
err_ext_var: var_store.fresh(),
kind: match kind {
ResultTryKind::KeywordPrefix => TryKind::KeywordPrefix,
ResultTryKind::OperatorSuffix => TryKind::OperatorSuffix,
},
},
output,
)
}
ast::Expr::Return(return_expr, after_return) => {
let mut output = Output::default();
@ -1309,7 +1454,9 @@ pub fn canonicalize_expr<'a>(
let return_var = var_store.fresh();
scope.early_returns.push((return_var, return_expr.region));
scope
.early_returns
.push((return_var, return_expr.region, EarlyReturnKind::Return));
(
Return {
@ -2251,6 +2398,31 @@ pub fn inline_calls(var_store: &mut VarStore, expr: Expr) -> Expr {
}
}
Try {
result_expr,
result_var,
return_var,
ok_payload_var,
err_payload_var,
err_ext_var,
kind,
} => {
let loc_result_expr = Loc {
region: result_expr.region,
value: inline_calls(var_store, result_expr.value),
};
Try {
result_expr: Box::new(loc_result_expr),
result_var,
return_var,
ok_payload_var,
err_payload_var,
err_ext_var,
kind,
}
}
LetRec(defs, loc_expr, mark) => {
let mut new_defs = Vec::with_capacity(defs.len());
@ -2545,8 +2717,9 @@ pub fn is_valid_interpolation(expr: &ast::Expr<'_>) -> bool {
| ast::Expr::MalformedIdent(_, _)
| ast::Expr::Tag(_)
| ast::Expr::OpaqueRef(_) => true,
ast::Expr::LowLevelTry(loc_expr, _) => is_valid_interpolation(&loc_expr.value),
// Newlines are disallowed inside interpolation, and these all require newlines
ast::Expr::DbgStmt(_, _)
ast::Expr::DbgStmt { .. }
| ast::Expr::LowLevelDbg(_, _, _)
| ast::Expr::Return(_, _)
| ast::Expr::When(_, _)
@ -3301,7 +3474,7 @@ pub struct FunctionDef {
pub closure_type: Variable,
pub return_type: Variable,
pub fx_type: Variable,
pub early_returns: Vec<(Variable, Region)>,
pub early_returns: Vec<(Variable, Region, EarlyReturnKind)>,
pub captured_symbols: Vec<(Symbol, Variable)>,
pub arguments: Vec<(Variable, AnnotatedMark, Loc<Pattern>)>,
}
@ -3443,6 +3616,9 @@ pub(crate) fn get_lookup_symbols(expr: &Expr) -> Vec<ExpectLookup> {
// Intentionally ignore the lookups in the nested `expect` condition itself,
// because they couldn't possibly influence the outcome of this `expect`!
}
Expr::Try { result_expr, .. } => {
stack.push(&result_expr.value);
}
Expr::Return { return_value, .. } => {
stack.push(&return_value.value);
}

View file

@ -371,9 +371,10 @@ pub fn canonicalize_module_defs<'a>(
PatternType::TopLevelDef,
);
for (_early_return_var, early_return_region) in &scope.early_returns {
for (_early_return_var, early_return_region, early_return_kind) in &scope.early_returns {
env.problem(Problem::ReturnOutsideOfFunction {
region: *early_return_region,
return_kind: *early_return_kind,
});
}
@ -964,6 +965,14 @@ fn fix_values_captured_in_closure_expr(
);
}
Try { result_expr, .. } => {
fix_values_captured_in_closure_expr(
&mut result_expr.value,
no_capture_symbols,
closure_captures,
);
}
Return { return_value, .. } => {
fix_values_captured_in_closure_expr(
&mut return_value.value,

View file

@ -273,6 +273,14 @@ pub fn canonicalize_def_header_pattern<'a>(
region,
) {
Ok((symbol, shadowing_ability_member)) => {
if name.contains("__") {
env.problem(Problem::RuntimeError(RuntimeError::MalformedPattern(
MalformedPatternProblem::BadIdent(
roc_parse::ident::BadIdent::TooManyUnderscores(region.start()),
),
region,
)));
}
let can_pattern = match shadowing_ability_member {
// A fresh identifier.
None => {

View file

@ -5,7 +5,7 @@ use roc_module::symbol::{IdentId, IdentIds, ModuleId, ModuleIds, Symbol};
use roc_problem::can::{RuntimeError, ScopeModuleSource};
use roc_region::all::{Loc, Region};
use roc_types::subs::Variable;
use roc_types::types::{Alias, AliasKind, AliasVar, Type};
use roc_types::types::{Alias, AliasKind, AliasVar, EarlyReturnKind, Type};
use crate::abilities::PendingAbilitiesStore;
@ -49,7 +49,7 @@ pub struct Scope {
/// We won't intern them because they're only used during canonicalization for error reporting.
ignored_locals: VecMap<String, Region>,
pub early_returns: Vec<(Variable, Region)>,
pub early_returns: Vec<(Variable, Region, EarlyReturnKind)>,
}
impl Scope {

View file

@ -164,7 +164,7 @@ pub fn unwrap_suffixed_expression<'a>(
// USEFUL TO SEE THE UNWRAPPING
// OF AST NODES AS THEY DESCEND
// if is_expr_suffixed(&loc_expr.value) {
// dbg!(&maybe_def_pat, &loc_expr, &unwrapped_expression);
// eprintln!("{:?}, {:?}, {:?}", &maybe_def_pat, &loc_expr, &unwrapped_expression);
// }
unwrapped_expression

View file

@ -400,6 +400,17 @@ pub fn walk_expr<V: Visitor>(visitor: &mut V, expr: &Expr, var: Variable) {
Variable::NULL,
);
}
Expr::Try {
result_expr,
result_var,
return_var: _,
ok_payload_var: _,
err_payload_var: _,
err_ext_var: _,
kind: _,
} => {
visitor.visit_expr(&result_expr.value, result_expr.region, *result_var);
}
Expr::Return {
return_value,
return_var,

View file

@ -143,79 +143,70 @@ Defs {
ident: "i",
},
],
@132-172 Apply(
@132-172 Var {
module_name: "Result",
ident: "try",
},
[
@132-152 Apply(
@132-152 Var {
module_name: "",
ident: "inc",
},
[
@132-133 Var {
module_name: "",
ident: "i",
@113-189 Defs(
Defs {
tags: [
EitherIndex(2147483648),
],
regions: [
@132-172,
],
space_before: [
Slice<roc_parse::ast::CommentOrNewline> { start: 0, length: 0 },
],
space_after: [
Slice<roc_parse::ast::CommentOrNewline> { start: 0, length: 0 },
],
spaces: [],
type_defs: [],
value_defs: [
Body(
@113-117 Identifier {
ident: "newi",
},
],
BinOp(
Pizza,
),
),
@132-172 Closure(
[
@132-152 Identifier {
ident: "#!0_arg",
},
],
@132-172 Apply(
@132-172 Var {
module_name: "Result",
ident: "try",
},
[
@132-172 LowLevelTry(
@132-172 Apply(
@132-172 Var {
@169-172 Var {
module_name: "",
ident: "inc",
},
[
@132-152 Var {
module_name: "",
ident: "#!0_arg",
},
],
BinOp(
Pizza,
),
),
@132-172 Closure(
[
@113-117 Identifier {
ident: "newi",
},
],
@182-189 Apply(
@182-184 Tag(
"Ok",
@132-152 LowLevelTry(
@132-152 Apply(
@149-152 Var {
module_name: "",
ident: "inc",
},
[
@132-133 Var {
module_name: "",
ident: "i",
},
],
Try,
),
OperatorSuffix,
),
[
@185-189 Var {
module_name: "",
ident: "newi",
},
],
Space,
),
],
Try,
),
],
QuestionSuffix,
OperatorSuffix,
),
),
],
},
@182-189 Apply(
@182-184 Tag(
"Ok",
),
],
QuestionSuffix,
[
@185-189 Var {
module_name: "",
ident: "newi",
},
],
Space,
),
),
),
),

View file

@ -802,6 +802,106 @@ mod test_can {
}
}
#[test]
fn question_suffix_simple() {
let src = indoc!(
r#"
(Str.toU64 "123")?
"#
);
let arena = Bump::new();
let out = can_expr_with(&arena, test_home(), src);
assert_eq!(out.problems, Vec::new());
// Assert that we desugar to:
//
// Try(Str.toU64 "123")
let cond_expr = assert_try_expr(&out.loc_expr.value);
let cond_args = assert_func_call(cond_expr, "toU64", CalledVia::Space, &out.interns);
assert_eq!(cond_args.len(), 1);
assert_str_value(&cond_args[0].1.value, "123");
}
#[test]
fn question_suffix_after_function() {
let src = indoc!(
r#"
Str.toU64? "123"
"#
);
let arena = Bump::new();
let out = can_expr_with(&arena, test_home(), src);
assert_eq!(out.problems, Vec::new());
// Assert that we desugar to:
//
// Try(Str.toU64 "123")
let cond_expr = assert_try_expr(&out.loc_expr.value);
let cond_args = assert_func_call(cond_expr, "toU64", CalledVia::Try, &out.interns);
assert_eq!(cond_args.len(), 1);
assert_str_value(&cond_args[0].1.value, "123");
}
#[test]
fn question_suffix_pipe() {
let src = indoc!(
r#"
"123" |> Str.toU64?
"#
);
let arena = Bump::new();
let out = can_expr_with(&arena, test_home(), src);
assert_eq!(out.problems, Vec::new());
// Assert that we desugar to:
//
// Try(Str.toU64 "123")
let cond_expr = assert_try_expr(&out.loc_expr.value);
let cond_args = assert_func_call(cond_expr, "toU64", CalledVia::Try, &out.interns);
assert_eq!(cond_args.len(), 1);
assert_str_value(&cond_args[0].1.value, "123");
}
#[test]
fn question_suffix_pipe_nested() {
let src = indoc!(
r#"
"123" |> Str.toU64? (Ok 123)?
"#
);
let arena = Bump::new();
let out = can_expr_with(&arena, test_home(), src);
assert_eq!(out.problems, Vec::new());
// Assert that we desugar to:
//
// Try(Str.toU64 "123" Try(Ok 123))
let cond_expr = assert_try_expr(&out.loc_expr.value);
let cond_args = assert_func_call(cond_expr, "toU64", CalledVia::Try, &out.interns);
assert_eq!(cond_args.len(), 2);
assert_str_value(&cond_args[0].1.value, "123");
let ok_tag = assert_try_expr(&cond_args[1].1.value);
let tag_args = assert_tag_application(ok_tag, "Ok");
assert_eq!(tag_args.len(), 1);
assert_num_value(&tag_args[0].1.value, 123);
}
#[test]
fn try_desugar_plain_prefix() {
let src = indoc!(
@ -816,64 +916,13 @@ mod test_can {
// Assert that we desugar to:
//
// when Str.toU64 "123" is
// Ok `0` -> `0`
// Err `1` -> return Err `1`
// Try(Str.toU64 "123")
let (cond_expr, branches) = assert_when(&out.loc_expr.value);
let cond_expr = assert_try_expr(&out.loc_expr.value);
let cond_args = assert_func_call(cond_expr, "toU64", CalledVia::Try, &out.interns);
assert_eq!(cond_args.len(), 1);
assert_str_value(&cond_args[0].1.value, "123");
assert_eq!(branches.len(), 2);
assert_eq!(branches[0].patterns.len(), 1);
assert!(!branches[0].patterns[0].degenerate);
match &branches[0].patterns[0].pattern.value {
Pattern::AppliedTag {
tag_name,
arguments,
..
} => {
assert_eq!(tag_name.0.to_string(), "Ok");
assert_eq!(arguments.len(), 1);
assert_pattern_name(&arguments[0].1.value, "0", &out.interns);
}
other => panic!("First argument was not an applied tag: {:?}", other),
}
assert!(&branches[0].guard.is_none());
assert_var_usage(&branches[0].value.value, "0", &out.interns);
assert_eq!(branches[1].patterns.len(), 1);
assert!(!branches[1].patterns[0].degenerate);
match &branches[1].patterns[0].pattern.value {
Pattern::AppliedTag {
tag_name,
arguments,
..
} => {
assert_eq!(tag_name.0.to_string(), "Err");
assert_eq!(arguments.len(), 1);
assert_pattern_name(&arguments[0].1.value, "1", &out.interns);
}
other => panic!("First argument was not an applied tag: {:?}", other),
}
match &branches[1].value.value {
Expr::Return { return_value, .. } => match &return_value.value {
Expr::Tag {
name, arguments, ..
} => {
assert_eq!(name.0.to_string(), "Err");
assert_eq!(arguments.len(), 1);
assert_var_usage(&arguments[0].1.value, "1", &out.interns);
}
other_inner => panic!("Expr was not a Tag: {:?}", other_inner),
},
other_outer => panic!("Expr was not a Return: {:?}", other_outer),
}
}
#[test]
@ -890,64 +939,13 @@ mod test_can {
// Assert that we desugar to:
//
// when Str.toU64 "123" is
// Ok `0` -> `0`
// Err `1` -> return Err `1`
// Try(Str.toU64 "123")
let (cond_expr, branches) = assert_when(&out.loc_expr.value);
let cond_expr = assert_try_expr(&out.loc_expr.value);
let cond_args = assert_func_call(cond_expr, "toU64", CalledVia::Try, &out.interns);
assert_eq!(cond_args.len(), 1);
assert_str_value(&cond_args[0].1.value, "123");
assert_eq!(branches.len(), 2);
assert_eq!(branches[0].patterns.len(), 1);
assert!(!branches[0].patterns[0].degenerate);
match &branches[0].patterns[0].pattern.value {
Pattern::AppliedTag {
tag_name,
arguments,
..
} => {
assert_eq!(tag_name.0.to_string(), "Ok");
assert_eq!(arguments.len(), 1);
assert_pattern_name(&arguments[0].1.value, "0", &out.interns);
}
other => panic!("First argument was not an applied tag: {:?}", other),
}
assert!(&branches[0].guard.is_none());
assert_var_usage(&branches[0].value.value, "0", &out.interns);
assert_eq!(branches[1].patterns.len(), 1);
assert!(!branches[1].patterns[0].degenerate);
match &branches[1].patterns[0].pattern.value {
Pattern::AppliedTag {
tag_name,
arguments,
..
} => {
assert_eq!(tag_name.0.to_string(), "Err");
assert_eq!(arguments.len(), 1);
assert_pattern_name(&arguments[0].1.value, "1", &out.interns);
}
other => panic!("First argument was not an applied tag: {:?}", other),
}
match &branches[1].value.value {
Expr::Return { return_value, .. } => match &return_value.value {
Expr::Tag {
name, arguments, ..
} => {
assert_eq!(name.0.to_string(), "Err");
assert_eq!(arguments.len(), 1);
assert_var_usage(&arguments[0].1.value, "1", &out.interns);
}
other_inner => panic!("Expr was not a Tag: {:?}", other_inner),
},
other_outer => panic!("Expr was not a Return: {:?}", other_outer),
}
}
#[test]
@ -964,64 +962,13 @@ mod test_can {
// Assert that we desugar to:
//
// when Str.toU64 "123" is
// Ok `0` -> `0`
// Err `1` -> return Err `1`
// Try(Str.toU64 "123")
let (cond_expr, branches) = assert_when(&out.loc_expr.value);
let cond_expr = assert_try_expr(&out.loc_expr.value);
let cond_args = assert_func_call(cond_expr, "toU64", CalledVia::Space, &out.interns);
assert_eq!(cond_args.len(), 1);
assert_str_value(&cond_args[0].1.value, "123");
assert_eq!(branches.len(), 2);
assert_eq!(branches[0].patterns.len(), 1);
assert!(!branches[0].patterns[0].degenerate);
match &branches[0].patterns[0].pattern.value {
Pattern::AppliedTag {
tag_name,
arguments,
..
} => {
assert_eq!(tag_name.0.to_string(), "Ok");
assert_eq!(arguments.len(), 1);
assert_pattern_name(&arguments[0].1.value, "0", &out.interns);
}
other => panic!("First argument was not an applied tag: {:?}", other),
}
assert!(&branches[0].guard.is_none());
assert_var_usage(&branches[0].value.value, "0", &out.interns);
assert_eq!(branches[1].patterns.len(), 1);
assert!(!branches[1].patterns[0].degenerate);
match &branches[1].patterns[0].pattern.value {
Pattern::AppliedTag {
tag_name,
arguments,
..
} => {
assert_eq!(tag_name.0.to_string(), "Err");
assert_eq!(arguments.len(), 1);
assert_pattern_name(&arguments[0].1.value, "1", &out.interns);
}
other => panic!("First argument was not an applied tag: {:?}", other),
}
match &branches[1].value.value {
Expr::Return { return_value, .. } => match &return_value.value {
Expr::Tag {
name, arguments, ..
} => {
assert_eq!(name.0.to_string(), "Err");
assert_eq!(arguments.len(), 1);
assert_var_usage(&arguments[0].1.value, "1", &out.interns);
}
other_inner => panic!("Expr was not a Tag: {:?}", other_inner),
},
other_outer => panic!("Expr was not a Return: {:?}", other_outer),
}
}
#[test]
@ -1126,6 +1073,16 @@ mod test_can {
}
}
fn assert_tag_application(expr: &Expr, tag_name: &str) -> Vec<(Variable, Loc<Expr>)> {
match expr {
Expr::LetNonRec(_, loc_expr) => assert_tag_application(&loc_expr.value, tag_name),
Expr::Tag {
name, arguments, ..
} if *name == tag_name.into() => arguments.clone(),
_ => panic!("Expr was not a Tag named {tag_name:?}: {expr:?}",),
}
}
fn assert_pattern_name(pattern: &Pattern, name: &str, interns: &roc_module::symbol::Interns) {
match pattern {
Pattern::Identifier(sym) => assert_eq!(sym.as_str(interns), name),
@ -1142,6 +1099,13 @@ mod test_can {
}
}
fn assert_try_expr(expr: &Expr) -> &Expr {
match expr {
Expr::Try { result_expr, .. } => &result_expr.value,
_ => panic!("Expr was not a Try: {:?}", expr),
}
}
// TAIL CALLS
fn get_closure(expr: &Expr, i: usize) -> roc_can::expr::Recursive {
match expr {

View file

@ -30,8 +30,8 @@ use roc_region::all::{Loc, Region};
use roc_types::subs::{IllegalCycleMark, Variable};
use roc_types::types::Type::{self, *};
use roc_types::types::{
AliasKind, AnnotationSource, Category, IndexOrField, OptAbleType, PReason, Reason, RecordField,
TypeExtension, TypeTag, Types,
AliasKind, AnnotationSource, Category, EarlyReturnKind, IndexOrField, OptAbleType, PReason,
Reason, RecordField, TypeExtension, TypeTag, Types,
};
use soa::{Index, Slice};
@ -152,7 +152,7 @@ fn constrain_untyped_closure(
closure_var: Variable,
ret_var: Variable,
fx_var: Variable,
early_returns: &[(Variable, Region)],
early_returns: &[(Variable, Region, EarlyReturnKind)],
arguments: &[(Variable, AnnotatedMark, Loc<Pattern>)],
loc_body_expr: &Loc<Expr>,
captured_symbols: &[(Symbol, Variable)],
@ -184,30 +184,16 @@ fn constrain_untyped_closure(
));
let returns_constraint = env.with_fx_expectation(fx_var, None, |env| {
let return_con = constrain_expr(
constrain_function_return(
types,
constraints,
env,
loc_body_expr.region,
&loc_body_expr.value,
loc_body_expr,
early_returns,
return_type_index,
);
let mut return_constraints = Vec::with_capacity(early_returns.len() + 1);
return_constraints.push(return_con);
for (early_return_variable, early_return_region) in early_returns {
let early_return_con = constraints.equal_types_var(
*early_return_variable,
return_type_index,
Category::Return,
*early_return_region,
);
return_constraints.push(early_return_con);
}
constraints.and_constraint(return_constraints)
ret_var,
false,
)
});
// make sure the captured symbols are sorted!
@ -259,6 +245,51 @@ fn constrain_untyped_closure(
constraints.exists_many(vars, cons)
}
pub fn constrain_function_return(
types: &mut Types,
constraints: &mut Constraints,
env: &mut Env,
body_expr: &Loc<Expr>,
early_returns: &[(Variable, Region, EarlyReturnKind)],
return_type_expected: ExpectedTypeIndex,
ret_var: Variable,
should_attach_res_constraints: bool,
) -> Constraint {
let return_con = constrain_expr(
types,
constraints,
env,
body_expr.region,
&body_expr.value,
return_type_expected,
);
let mut return_constraints = Vec::with_capacity(early_returns.len() + 1);
let mut return_type_vars = Vec::with_capacity(early_returns.len() + 1);
return_constraints.push(return_con);
return_type_vars.push(ret_var);
for (early_return_variable, early_return_region, early_return_kind) in early_returns {
let early_return_con = constraints.equal_types_var(
*early_return_variable,
return_type_expected,
Category::Return(*early_return_kind),
*early_return_region,
);
return_constraints.push(early_return_con);
return_type_vars.push(*early_return_variable);
}
let returns_constraint = constraints.exists_many(return_type_vars, return_constraints);
if should_attach_res_constraints {
attach_resolution_constraints(constraints, env, returns_constraint)
} else {
returns_constraint
}
}
pub fn constrain_expr(
types: &mut Types,
constraints: &mut Constraints,
@ -576,7 +607,6 @@ pub fn constrain_expr(
let mut arg_cons = Vec::with_capacity(loc_args.len());
for (index, (arg_var, loc_arg)) in loc_args.iter().enumerate() {
let region = loc_arg.region;
let arg_type = Variable(*arg_var);
let arg_type_index = constraints.push_variable(*arg_var);
@ -833,6 +863,80 @@ pub fn constrain_expr(
constraints.exists_many([*variable], [message_con, continuation_con])
}
Try {
result_expr,
result_var,
return_var,
ok_payload_var,
err_payload_var,
err_ext_var,
kind,
} => {
let result_var_index = constraints.push_variable(*result_var);
let result_expected_type = constraints.push_expected_type(ForReason(
Reason::TryResult,
result_var_index,
result_expr.region,
));
let result_constraint = constrain_expr(
types,
constraints,
env,
result_expr.region,
&result_expr.value,
result_expected_type,
);
let try_target_constraint = constraints.try_target(
result_var_index,
*ok_payload_var,
*err_payload_var,
result_expr.region,
*kind,
);
let return_type_index = constraints.push_variable(*return_var);
let expected_return_value = constraints.push_expected_type(ForReason(
Reason::TryResult,
return_type_index,
result_expr.region,
));
let try_failure_type_index = {
let typ = types.from_old_type(&Type::TagUnion(
vec![("Err".into(), vec![Type::Variable(*err_payload_var)])],
TypeExtension::from_non_annotation_type(Type::Variable(*err_ext_var)),
));
constraints.push_type(types, typ)
};
let try_failure_constraint = constraints.equal_types(
try_failure_type_index,
expected_return_value,
Category::TryFailure,
region,
);
let ok_type_index = constraints.push_variable(*ok_payload_var);
let try_success_constraint =
constraints.equal_types(ok_type_index, expected, Category::TrySuccess, region);
constraints.exists_many(
[
*return_var,
*result_var,
*ok_payload_var,
*err_payload_var,
*err_ext_var,
],
[
result_constraint,
try_target_constraint,
try_failure_constraint,
try_success_constraint,
],
)
}
If {
cond_var,
branch_var,
@ -1018,26 +1122,25 @@ pub fn constrain_expr(
Region::span_across(&loc_cond.region, &branches.last().unwrap().value.region)
};
let branch_expr_reason =
|expected: &Expected<TypeOrVar>, index, branch_region| match expected {
FromAnnotation(name, arity, ann_source, _typ) => {
// NOTE deviation from elm.
//
// in elm, `_typ` is used, but because we have this `expr_var` too
// and need to constrain it, this is what works and gives better error messages
FromAnnotation(
name.clone(),
*arity,
AnnotationSource::TypedWhenBranch {
index,
region: ann_source.region(),
},
body_type_index,
)
}
let branch_expr_reason = |expected: &Expected<TypeOrVar>, index| match expected {
FromAnnotation(name, arity, ann_source, _typ) => {
// NOTE deviation from elm.
//
// in elm, `_typ` is used, but because we have this `expr_var` too
// and need to constrain it, this is what works and gives better error messages
FromAnnotation(
name.clone(),
*arity,
AnnotationSource::TypedWhenBranch {
index,
region: ann_source.region(),
},
body_type_index,
)
}
_ => ForReason(Reason::WhenBranch { index }, body_type_index, branch_region),
};
_ => ForReason(Reason::WhenBranch { index }, body_type_index, region),
};
// Our goal is to constrain and introduce variables in all pattern when branch patterns before
// looking at their bodies.
@ -1091,11 +1194,7 @@ pub fn constrain_expr(
region,
when_branch,
expected_pattern,
branch_expr_reason(
&constraints[expected],
HumanIndex::zero_based(index),
when_branch.value.region,
),
branch_expr_reason(&constraints[expected], HumanIndex::zero_based(index)),
);
pattern_vars.extend(new_pattern_vars);
@ -1438,14 +1537,16 @@ pub fn constrain_expr(
return_value.region,
));
constrain_expr(
let return_con = constrain_expr(
types,
constraints,
env,
return_value.region,
&return_value.value,
expected_return_value,
)
);
constraints.exists([*return_var], return_con)
}
Tag {
tag_union_var: variant_var,
@ -2072,43 +2173,19 @@ fn constrain_function_def(
constraints.push_type(types, fn_type)
};
let returns_constraint = {
let return_con = constrain_expr(
types,
constraints,
env,
loc_body_expr.region,
&loc_body_expr.value,
return_type_annotation_expected,
);
let mut return_constraints =
Vec::with_capacity(function_def.early_returns.len() + 1);
return_constraints.push(return_con);
for (early_return_variable, early_return_region) in &function_def.early_returns {
let early_return_type_expected =
constraints.push_expected_type(Expected::ForReason(
Reason::FunctionOutput,
ret_type_index,
*early_return_region,
));
vars.push(*early_return_variable);
let early_return_con = constraints.equal_types_var(
*early_return_variable,
early_return_type_expected,
Category::Return,
*early_return_region,
);
return_constraints.push(early_return_con);
}
let returns_constraint = constraints.and_constraint(return_constraints);
attach_resolution_constraints(constraints, env, returns_constraint)
};
let returns_constraint =
env.with_fx_expectation(function_def.fx_type, Some(annotation.region), |env| {
constrain_function_return(
types,
constraints,
env,
loc_body_expr,
&function_def.early_returns,
return_type_annotation_expected,
function_def.return_type,
true,
)
});
vars.push(expr_var);
@ -2457,7 +2534,7 @@ fn constrain_when_branch_help(
types,
constraints,
env,
region,
when_branch.value.region,
&when_branch.value.value,
expr_expected,
);
@ -2964,32 +3041,16 @@ fn constrain_typed_def(
let returns_constraint =
env.with_fx_expectation(fx_var, Some(annotation.region), |env| {
let return_con = constrain_expr(
constrain_function_return(
types,
constraints,
env,
loc_body_expr.region,
&loc_body_expr.value,
loc_body_expr,
early_returns,
return_type,
);
let mut return_constraints = Vec::with_capacity(early_returns.len() + 1);
return_constraints.push(return_con);
for (early_return_variable, early_return_region) in early_returns {
let early_return_con = constraints.equal_types_var(
*early_return_variable,
return_type,
Category::Return,
*early_return_region,
);
return_constraints.push(early_return_con);
}
let returns_constraint = constraints.and_constraint(return_constraints);
attach_resolution_constraints(constraints, env, returns_constraint)
ret_var,
true,
)
});
vars.push(*fn_var);
@ -3435,12 +3496,18 @@ fn constrain_let_def(
)
});
// Ignored def must be effectful, otherwise it's dead code
let effectful_constraint = Constraint::ExpectEffectful(
fx_var,
ExpectEffectfulReason::Ignored,
def.loc_pattern.region,
);
let effectful_constraint = if def.loc_expr.value.contains_any_early_returns() {
// If the statement has early returns, it doesn't need to be effectful to
// potentially affect the output of the containing function
Constraint::True
} else {
// If there are no early returns, it must be effectful or else it's dead code
Constraint::ExpectEffectful(
fx_var,
ExpectEffectfulReason::Ignored,
def.loc_pattern.region,
)
};
let enclosing_fx_constraint = constraints.fx_call(
fx_var,
@ -3527,9 +3594,14 @@ fn constrain_stmt_def(
generalizable,
);
// Stmt expr must be effectful, otherwise it's dead code
let effectful_constraint =
Constraint::ExpectEffectful(fx_var, ExpectEffectfulReason::Stmt, region);
let effectful_constraint = if def.loc_expr.value.contains_any_early_returns() {
// If the statement has early returns, it doesn't need to be effectful to
// potentially affect the output of the containing function
Constraint::True
} else {
// If there are no early returns, it must be effectful or else it's dead code
Constraint::ExpectEffectful(fx_var, ExpectEffectfulReason::Stmt, region)
};
let fx_call_kind = match fn_name {
None => FxCallKind::Stmt,
@ -4012,35 +4084,22 @@ fn constraint_recursive_function(
let returns_constraint =
env.with_fx_expectation(fx_var, Some(annotation.region), |env| {
let expected = constraints.push_expected_type(NoExpectation(ret_type_index));
let return_con = constrain_expr(
let expected = constraints.push_expected_type(ForReason(
Reason::FunctionOutput,
ret_type_index,
region,
));
constrain_function_return(
types,
constraints,
env,
loc_body_expr.region,
&loc_body_expr.value,
loc_body_expr,
&function_def.early_returns,
expected,
);
let mut return_constraints =
Vec::with_capacity(function_def.early_returns.len() + 1);
return_constraints.push(return_con);
for (early_return_variable, early_return_region) in &function_def.early_returns
{
let early_return_con = constraints.equal_types_var(
*early_return_variable,
expected,
Category::Return,
*early_return_region,
);
return_constraints.push(early_return_con);
}
let returns_constraint = constraints.and_constraint(return_constraints);
attach_resolution_constraints(constraints, env, returns_constraint)
ret_var,
true,
)
});
vars.push(expr_var);
@ -4393,6 +4452,7 @@ fn is_generalizable_expr(mut expr: &Expr) -> bool {
| RecordUpdate { .. }
| Expect { .. }
| Dbg { .. }
| Try { .. }
| Return { .. }
| RuntimeError(..)
| ZeroArgumentTag { .. }
@ -4584,37 +4644,20 @@ fn rec_defs_help(
};
let returns_constraint =
env.with_fx_expectation(fx_var, Some(annotation.region), |env| {
let return_type_expected =
constraints.push_expected_type(NoExpectation(ret_type_index));
let return_type_expected = constraints.push_expected_type(
ForReason(Reason::FunctionOutput, ret_type_index, region),
);
let return_con = constrain_expr(
constrain_function_return(
types,
constraints,
env,
loc_body_expr.region,
&loc_body_expr.value,
loc_body_expr,
early_returns,
return_type_expected,
);
let mut return_constraints =
Vec::with_capacity(early_returns.len() + 1);
return_constraints.push(return_con);
for (early_return_variable, early_return_region) in early_returns {
let early_return_con = constraints.equal_types_var(
*early_return_variable,
return_type_expected,
Category::Return,
*early_return_region,
);
return_constraints.push(early_return_con);
}
let returns_constraint =
constraints.and_constraint(return_constraints);
attach_resolution_constraints(constraints, env, returns_constraint)
ret_var,
true,
)
});
vars.push(*fn_var);

View file

@ -15,3 +15,4 @@ roc_region.workspace = true
roc_error_macros.workspace = true
bumpalo.workspace = true
soa.workspace = true

File diff suppressed because it is too large Load diff

View file

@ -1,11 +1,13 @@
use roc_parse::ast::{Collection, CommentOrNewline, ExtractSpaces};
use roc_parse::{
ast::{Collection, CommentOrNewline, ExtractSpaces},
expr::merge_spaces,
};
use crate::{
annotation::{is_collection_multiline, Formattable, Newlines},
spaces::{fmt_comments_only, NewlineAt, INDENT},
Buf,
};
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
pub enum Braces {
Round,
@ -13,26 +15,36 @@ pub enum Braces {
Curly,
}
pub fn fmt_collection<'a, 'buf, T: ExtractSpaces<'a> + Formattable>(
impl Braces {
pub fn start(self) -> char {
match self {
Braces::Round => '(',
Braces::Curly => '{',
Braces::Square => '[',
}
}
pub fn end(self) -> char {
match self {
Braces::Round => ')',
Braces::Curly => '}',
Braces::Square => ']',
}
}
}
pub fn fmt_collection<'a, 'buf, T: ExtractSpaces<'a> + Formattable + std::fmt::Debug>(
buf: &mut Buf<'buf>,
indent: u16,
braces: Braces,
items: Collection<'a, T>,
newline: Newlines,
) where
<T as ExtractSpaces<'a>>::Item: Formattable,
<T as ExtractSpaces<'a>>::Item: Formattable + std::fmt::Debug,
{
let start = match braces {
Braces::Round => '(',
Braces::Curly => '{',
Braces::Square => '[',
};
let end = match braces {
Braces::Round => ')',
Braces::Curly => '}',
Braces::Square => ']',
};
let start = braces.start();
let end = braces.end();
if is_collection_multiline(&items) {
let braces_indent = indent;
@ -43,10 +55,21 @@ pub fn fmt_collection<'a, 'buf, T: ExtractSpaces<'a> + Formattable>(
buf.indent(braces_indent);
buf.push(start);
let mut last_after: &[CommentOrNewline<'_>] = &[];
for (index, item) in items.iter().enumerate() {
let is_first_item = index == 0;
let item = item.extract_spaces();
let is_only_newlines = item.before.iter().all(|s| s.is_newline());
let last_after_was_only_newlines = last_after.iter().all(|s| s.is_newline());
if !last_after.is_empty() {
if last_after.iter().any(|s| s.is_newline()) {
buf.newline();
}
fmt_comments_only(buf, last_after.iter(), NewlineAt::None, item_indent);
}
if item.before.is_empty() || is_only_newlines {
buf.ensure_ends_with_newline();
@ -64,13 +87,14 @@ pub fn fmt_collection<'a, 'buf, T: ExtractSpaces<'a> + Formattable>(
if item
.before
.starts_with(&[CommentOrNewline::Newline, CommentOrNewline::Newline])
&& last_after_was_only_newlines
{
// If there's a comment, and it's not on the first item,
// and it's preceded by at least one blank line, maintain 1 blank line.
// (We already ensured that it ends in a newline, so this will turn that
// into a blank line.)
buf.newline();
buf.ensure_ends_with_blank_line();
}
}
@ -91,32 +115,30 @@ pub fn fmt_collection<'a, 'buf, T: ExtractSpaces<'a> + Formattable>(
buf.indent(item_indent);
buf.push(',');
if !item.after.is_empty() {
if item.after.iter().any(|s| s.is_newline()) {
buf.newline();
}
last_after = item.after;
}
fmt_comments_only(buf, item.after.iter(), NewlineAt::None, item_indent);
let final_comments = if !last_after.is_empty() {
if last_after.iter().any(|s| s.is_newline()) {
buf.newline();
}
}
if items.final_comments().iter().any(|s| s.is_newline()) {
buf.newline();
}
merge_spaces(buf.text.bump(), last_after, items.final_comments())
} else {
if items.final_comments().iter().any(|s| s.is_newline()) {
buf.newline();
}
if items
.final_comments()
.starts_with(&[CommentOrNewline::Newline, CommentOrNewline::Newline])
items.final_comments()
};
if has_comments(final_comments)
&& final_comments.starts_with(&[CommentOrNewline::Newline, CommentOrNewline::Newline])
{
buf.newline();
buf.ensure_ends_with_blank_line();
}
fmt_comments_only(
buf,
items.final_comments().iter(),
NewlineAt::None,
item_indent,
);
fmt_comments_only(buf, final_comments.iter(), NewlineAt::None, item_indent);
buf.ensure_ends_with_newline();
buf.indent(braces_indent);
@ -144,3 +166,13 @@ pub fn fmt_collection<'a, 'buf, T: ExtractSpaces<'a> + Formattable>(
buf.push(end);
}
fn has_comments(spaces: &[CommentOrNewline<'_>]) -> bool {
for space in spaces {
match space {
CommentOrNewline::Newline => {}
CommentOrNewline::LineComment(_) | CommentOrNewline::DocComment(_) => return true,
}
}
false
}

View file

@ -1,15 +1,27 @@
use crate::annotation::{is_collection_multiline, Formattable, Newlines, Parens};
use crate::annotation::{
ann_lift_spaces, ann_lift_spaces_after, is_collection_multiline, ty_is_outdentable,
Formattable, Newlines, Parens,
};
use crate::collection::{fmt_collection, Braces};
use crate::expr::fmt_str_literal;
use crate::pattern::fmt_pattern;
use crate::spaces::{fmt_default_newline, fmt_default_spaces, fmt_spaces, INDENT};
use crate::expr::{
expr_lift_and_lower, expr_lift_spaces, expr_lift_spaces_after, expr_lift_spaces_before,
fmt_str_literal, is_str_multiline, sub_expr_requests_parens,
};
use crate::pattern::{fmt_pattern, pattern_lift_spaces};
use crate::pattern::{pattern_lift_spaces_before, starts_with_inline_comment};
use crate::spaces::{
fmt_comments_only, fmt_default_newline, fmt_default_spaces, fmt_spaces, NewlineAt, INDENT,
};
use crate::Buf;
use bumpalo::Bump;
use roc_error_macros::internal_error;
use roc_parse::ast::{
AbilityMember, Defs, Expr, ExtractSpaces, ImportAlias, ImportAsKeyword, ImportExposingKeyword,
ImportedModuleName, IngestedFileAnnotation, IngestedFileImport, ModuleImport,
ModuleImportParams, Pattern, Spaces, StrLiteral, TypeAnnotation, TypeDef, TypeHeader, ValueDef,
AbilityMember, CommentOrNewline, Defs, Expr, ExtractSpaces, ImportAlias, ImportAsKeyword,
ImportExposingKeyword, ImportedModuleName, IngestedFileAnnotation, IngestedFileImport,
ModuleImport, ModuleImportParams, Pattern, Spaceable, Spaces, SpacesAfter, SpacesBefore,
StrLiteral, TypeAnnotation, TypeDef, TypeHeader, ValueDef,
};
use roc_parse::expr::merge_spaces;
use roc_parse::header::Keyword;
use roc_region::all::Loc;
@ -25,21 +37,28 @@ impl<'a> Formattable for Defs<'a> {
buf: &mut Buf,
_parens: Parens,
_newlines: Newlines,
indent: u16,
) {
let mut prev_spaces = true;
let arena = buf.text.bump();
for (index, def) in self.defs().enumerate() {
let spaces_before = &self.spaces[self.space_before[index].indices()];
let spaces_after = &self.spaces[self.space_after[index].indices()];
let def = def_lift_spaces(buf.text.bump(), def);
let spaces_before = merge_spaces(arena, spaces_before, def.before);
let spaces_after = merge_spaces(arena, def.after, spaces_after);
if prev_spaces {
fmt_spaces(buf, spaces_before.iter(), indent);
} else {
fmt_default_newline(buf, spaces_before, indent);
}
match def {
match def.item {
Ok(type_def) => type_def.format(buf, indent),
Err(value_def) => value_def.format(buf, indent),
}
@ -51,6 +70,339 @@ impl<'a> Formattable for Defs<'a> {
}
}
pub fn def_lift_spaces<'a, 'b: 'a>(
arena: &'a Bump,
def: Result<&'a TypeDef<'b>, &'a ValueDef<'b>>,
) -> Spaces<'a, Result<TypeDef<'a>, ValueDef<'a>>> {
match def {
Ok(td) => {
let td = tydef_lift_spaces(arena, *td);
Spaces {
before: td.before,
item: Ok(td.item),
after: td.after,
}
}
Err(vd) => {
let vd = valdef_lift_spaces(arena, *vd);
Spaces {
before: vd.before,
item: Err(vd.item),
after: vd.after,
}
}
}
}
fn lift_spaces_after<'a, 'b: 'a, T: 'b + ExtractSpaces<'a> + Spaceable<'a>>(
arena: &'a Bump,
item: T,
) -> SpacesAfter<'a, <T as ExtractSpaces<'a>>::Item>
where
<T as ExtractSpaces<'a>>::Item: Spaceable<'a>,
{
let spaces = item.extract_spaces();
SpacesAfter {
item: spaces.item.maybe_before(arena, spaces.before),
after: spaces.after,
}
}
pub fn tydef_lift_spaces<'a, 'b: 'a>(arena: &'a Bump, def: TypeDef<'b>) -> Spaces<'a, TypeDef<'a>> {
match def {
TypeDef::Alias { header, ann } => {
let ann_lifted = ann_lift_spaces_after(arena, &ann.value);
Spaces {
before: &[],
item: TypeDef::Alias {
header,
ann: Loc::at(ann.region, ann_lifted.item),
},
after: ann_lifted.after,
}
}
TypeDef::Opaque {
header,
typ,
derived,
} => {
if let Some(derived) = derived {
let derived_lifted = lift_spaces_after(arena, derived.value);
Spaces {
before: &[],
item: TypeDef::Opaque {
header,
typ,
derived: Some(Loc::at(derived.region, derived_lifted.item)),
},
after: derived_lifted.after,
}
} else {
let typ_lifted = ann_lift_spaces_after(arena, &typ.value);
Spaces {
before: &[],
item: TypeDef::Opaque {
header,
typ: Loc::at(typ.region, typ_lifted.item),
derived,
},
after: typ_lifted.after,
}
}
}
TypeDef::Ability {
header: _,
loc_implements: _,
members: _,
} => {
// TODO: if the fuzzer ever generates examples where it's important to lift spaces from the members,
// we'll need to implement this. I'm not sure that's possible, though.
Spaces {
before: &[],
item: def,
after: &[],
}
}
}
}
pub fn valdef_lift_spaces<'a, 'b: 'a>(
arena: &'a Bump,
def: ValueDef<'b>,
) -> Spaces<'a, ValueDef<'a>> {
match def {
ValueDef::Annotation(pat, ann) => {
let pat_lifted = pattern_lift_spaces_before(arena, &pat.value);
let ann_lifted = ann_lift_spaces_after(arena, &ann.value);
Spaces {
before: pat_lifted.before,
item: ValueDef::Annotation(
Loc::at(pat.region, pat_lifted.item),
Loc::at(ann.region, ann_lifted.item),
),
after: ann_lifted.after,
}
}
ValueDef::Body(pat, expr) => {
let pat_lifted = pattern_lift_spaces_before(arena, &pat.value);
let expr_lifted = expr_lift_spaces_after(Parens::NotNeeded, arena, &expr.value);
Spaces {
before: pat_lifted.before,
item: ValueDef::Body(
arena.alloc(Loc::at(pat.region, pat_lifted.item)),
arena.alloc(Loc::at(expr.region, expr_lifted.item)),
),
after: expr_lifted.after,
}
}
ValueDef::AnnotatedBody {
ann_pattern,
ann_type,
lines_between,
body_pattern,
body_expr,
} => {
let ann_pattern_lifted = pattern_lift_spaces_before(arena, &ann_pattern.value);
let ann_type_lifted = ann_lift_spaces_after(arena, &ann_type.value);
let body_pattern_lifted = pattern_lift_spaces_before(arena, &body_pattern.value);
let body_expr_lifted =
expr_lift_spaces_after(Parens::NotNeeded, arena, &body_expr.value);
let lines_between = merge_spaces(
arena,
ann_type_lifted.after,
merge_spaces(arena, lines_between, body_pattern_lifted.before),
);
Spaces {
before: ann_pattern_lifted.before,
item: ValueDef::AnnotatedBody {
ann_pattern: arena.alloc(Loc::at(ann_pattern.region, ann_pattern_lifted.item)),
ann_type: arena.alloc(Loc::at(ann_type.region, ann_type_lifted.item)),
lines_between,
body_pattern: arena
.alloc(Loc::at(body_pattern.region, body_pattern_lifted.item)),
body_expr: arena.alloc(Loc::at(body_expr.region, body_expr_lifted.item)),
},
after: body_expr_lifted.after,
}
}
ValueDef::Dbg {
condition,
preceding_comment,
} => {
let condition_lifted =
expr_lift_spaces_after(Parens::NotNeeded, arena, &condition.value);
Spaces {
before: &[],
item: ValueDef::Dbg {
condition: arena.alloc(Loc::at(condition.region, condition_lifted.item)),
preceding_comment,
},
after: condition_lifted.after,
}
}
ValueDef::Expect {
condition,
preceding_comment,
} => {
let condition_lifted =
expr_lift_spaces_after(Parens::NotNeeded, arena, &condition.value);
Spaces {
before: &[],
item: ValueDef::Expect {
condition: arena.alloc(Loc::at(condition.region, condition_lifted.item)),
preceding_comment,
},
after: condition_lifted.after,
}
}
ValueDef::ModuleImport(module_import) => {
// Module imports begin with 'import', and end with either a ImportAlias or Collection
// No spaces in sight!
Spaces {
before: &[],
item: ValueDef::ModuleImport(module_import),
after: &[],
}
}
ValueDef::IngestedFileImport(mut ingested_file_import) => {
// Ingested file imports begin with 'import', but can end with a TypeAnnotation, which can have spaces
let after = if let Some(ann) = &mut ingested_file_import.annotation {
let lifted = ann_lift_spaces_after(arena, &ann.annotation.value);
ann.annotation.value = lifted.item;
lifted.after
} else {
&[]
};
Spaces {
before: &[],
item: ValueDef::IngestedFileImport(ingested_file_import),
after,
}
}
ValueDef::Stmt(expr) => {
let expr_lifted = expr_lift_spaces(Parens::NotNeeded, arena, &expr.value);
Spaces {
before: expr_lifted.before,
item: ValueDef::Stmt(arena.alloc(Loc::at(expr.region, expr_lifted.item))),
after: expr_lifted.after,
}
}
ValueDef::StmtAfterExpr => Spaces {
before: &[],
item: ValueDef::StmtAfterExpr,
after: &[],
},
}
}
pub fn valdef_lift_spaces_before<'a, 'b: 'a>(
arena: &'a Bump,
def: ValueDef<'b>,
) -> SpacesBefore<'a, ValueDef<'a>> {
match def {
ValueDef::Annotation(pat, ann) => {
let pat_lifted = pattern_lift_spaces_before(arena, &pat.value);
SpacesBefore {
before: pat_lifted.before,
item: ValueDef::Annotation(Loc::at(pat.region, pat_lifted.item), ann),
}
}
ValueDef::Body(pat, expr) => {
let pat_lifted = pattern_lift_spaces_before(arena, &pat.value);
SpacesBefore {
before: pat_lifted.before,
item: ValueDef::Body(arena.alloc(Loc::at(pat.region, pat_lifted.item)), expr),
}
}
ValueDef::AnnotatedBody {
ann_pattern,
ann_type,
lines_between,
body_pattern,
body_expr,
} => {
let ann_pattern_lifted = pattern_lift_spaces_before(arena, &ann_pattern.value);
let ann_type_lifted = ann_lift_spaces_after(arena, &ann_type.value);
let body_pattern_lifted = pattern_lift_spaces_before(arena, &body_pattern.value);
let lines_between = merge_spaces(
arena,
ann_type_lifted.after,
merge_spaces(arena, lines_between, body_pattern_lifted.before),
);
SpacesBefore {
before: ann_pattern_lifted.before,
item: ValueDef::AnnotatedBody {
ann_pattern: arena.alloc(Loc::at(ann_pattern.region, ann_pattern_lifted.item)),
ann_type: arena.alloc(Loc::at(ann_type.region, ann_type_lifted.item)),
lines_between,
body_pattern: arena
.alloc(Loc::at(body_pattern.region, body_pattern_lifted.item)),
body_expr,
},
}
}
ValueDef::Dbg {
condition,
preceding_comment,
} => SpacesBefore {
before: &[],
item: ValueDef::Dbg {
condition,
preceding_comment,
},
},
ValueDef::Expect {
condition,
preceding_comment,
} => SpacesBefore {
before: &[],
item: ValueDef::Expect {
condition,
preceding_comment,
},
},
ValueDef::ModuleImport(module_import) => {
// Module imports always start with 'import', no spaces
SpacesBefore {
before: &[],
item: ValueDef::ModuleImport(module_import),
}
}
ValueDef::IngestedFileImport(ingested_file_import) => {
// Similarly, ingested file imports always start with 'import', no spaces
SpacesBefore {
before: &[],
item: ValueDef::IngestedFileImport(ingested_file_import),
}
}
ValueDef::Stmt(expr) => {
let expr_lifted = expr_lift_spaces_before(Parens::NotNeeded, arena, &expr.value);
SpacesBefore {
before: expr_lifted.before,
item: ValueDef::Stmt(arena.alloc(Loc::at(expr.region, expr_lifted.item))),
}
}
ValueDef::StmtAfterExpr => SpacesBefore {
before: &[],
item: ValueDef::StmtAfterExpr,
},
}
}
impl<'a> Formattable for TypeDef<'a> {
fn is_multiline(&self) -> bool {
use roc_parse::ast::TypeDef::*;
@ -66,34 +418,23 @@ impl<'a> Formattable for TypeDef<'a> {
use roc_parse::ast::TypeDef::*;
match self {
Alias {
header: TypeHeader { name, vars },
ann,
} => {
Alias { header, ann } => {
header.format(buf, indent);
buf.indent(indent);
buf.push_str(name.value);
for var in *vars {
buf.spaces(1);
let need_parens = matches!(var.value, Pattern::Apply(..));
if need_parens {
buf.push_str("(");
}
fmt_pattern(buf, &var.value, indent, Parens::NotNeeded);
buf.indent(indent);
if need_parens {
buf.push_str(")");
}
}
buf.push_str(" :");
buf.spaces(1);
ann.format(buf, indent)
let ann = ann_lift_spaces(buf.text.bump(), &ann.value);
let inner_indent = if ty_is_outdentable(&ann.item) {
indent
} else {
indent + INDENT
};
fmt_comments_only(buf, ann.before.iter(), NewlineAt::Bottom, inner_indent);
ann.item.format(buf, inner_indent);
fmt_spaces(buf, ann.after.iter(), indent);
}
Opaque {
header,
@ -113,7 +454,15 @@ impl<'a> Formattable for TypeDef<'a> {
let make_multiline = ann.is_multiline() || has_abilities_multiline;
fmt_general_def(header, buf, indent, ":=", &ann.value, newlines);
fmt_general_def(
header,
Parens::NotNeeded,
buf,
indent,
":=",
&ann.value,
newlines,
);
if let Some(has_abilities) = has_abilities {
buf.spaces(1);
@ -127,23 +476,17 @@ impl<'a> Formattable for TypeDef<'a> {
}
}
Ability {
header: TypeHeader { name, vars },
header,
loc_implements: _,
members,
} => {
buf.indent(indent);
buf.push_str(name.value);
for var in *vars {
buf.spaces(1);
fmt_pattern(buf, &var.value, indent, Parens::NotNeeded);
buf.indent(indent);
}
header.format_with_options(buf, Parens::NotNeeded, Newlines::No, indent);
buf.spaces(1);
buf.push_str(roc_parse::keyword::IMPLEMENTS);
buf.spaces(1);
if !self.is_multiline() {
debug_assert_eq!(members.len(), 1);
buf.spaces(1);
members[0].format_with_options(
buf,
Parens::NotNeeded,
@ -175,15 +518,68 @@ impl<'a> Formattable for TypeHeader<'a> {
buf: &mut Buf,
_parens: Parens,
_newlines: Newlines,
indent: u16,
) {
buf.indent(indent);
buf.push_str(self.name.value);
let vars_indent = if self.vars.iter().any(|v| v.is_multiline()) {
indent + INDENT
} else {
indent
};
let mut last_after: &[CommentOrNewline<'_>] = &[];
let mut last_multiline = false;
for var in self.vars.iter() {
buf.spaces(1);
fmt_pattern(buf, &var.value, indent, Parens::NotNeeded);
buf.indent(indent);
let var = pattern_lift_spaces(buf.text.bump(), &var.value);
let before = if !last_after.is_empty() {
merge_spaces(buf.text.bump(), last_after, var.before)
} else {
var.before
};
if !before.is_empty() {
if !var.item.is_multiline() {
fmt_comments_only(buf, before.iter(), NewlineAt::Bottom, vars_indent)
} else {
fmt_spaces(buf, before.iter(), vars_indent);
}
}
buf.ensure_ends_with_whitespace();
last_after = var.after;
last_multiline = var.item.is_multiline();
let need_parens = matches!(var.item, Pattern::Apply(..));
if need_parens {
buf.push_str("(");
}
fmt_pattern(buf, &var.item, vars_indent, Parens::NotNeeded);
buf.indent(vars_indent);
if need_parens {
buf.push_str(")");
}
}
if !last_after.is_empty() {
if starts_with_inline_comment(last_after.iter()) {
buf.spaces(1);
}
if !last_multiline {
fmt_comments_only(buf, last_after.iter(), NewlineAt::Bottom, indent)
} else {
fmt_spaces(buf, last_after.iter(), indent);
}
}
}
}
@ -213,6 +609,7 @@ impl<'a> Formattable for ModuleImport<'a> {
buf: &mut Buf,
_parens: Parens,
_newlines: Newlines,
indent: u16,
) {
let Self {
@ -228,6 +625,7 @@ impl<'a> Formattable for ModuleImport<'a> {
let indent = if !before_name.is_empty()
|| (params.is_multiline() && exposed.is_some())
|| params.map(|p| !p.before.is_empty()).unwrap_or(false)
|| alias.is_multiline()
|| exposed.map_or(false, |e| e.keyword.is_multiline())
{
@ -280,6 +678,7 @@ impl<'a> Formattable for IngestedFileImport<'a> {
buf: &mut Buf,
_parens: Parens,
_newlines: Newlines,
indent: u16,
) {
let Self {
@ -315,6 +714,7 @@ impl<'a> Formattable for ImportedModuleName<'a> {
buf: &mut Buf,
_parens: Parens,
_newlines: Newlines,
indent: u16,
) {
buf.indent(indent);
@ -394,6 +794,7 @@ impl<'a> Formattable for IngestedFileAnnotation<'a> {
buf: &mut Buf,
_parens: Parens,
_newlines: Newlines,
indent: u16,
) {
let Self {
@ -431,8 +832,14 @@ impl<'a> Formattable for ValueDef<'a> {
use roc_parse::ast::ValueDef::*;
match self {
Annotation(loc_pattern, loc_annotation) => {
let pat_parens = if ann_pattern_needs_parens(&loc_pattern.value) {
Parens::InApply
} else {
Parens::NotNeeded
};
fmt_general_def(
loc_pattern,
pat_parens,
buf,
indent,
":",
@ -441,7 +848,7 @@ impl<'a> Formattable for ValueDef<'a> {
);
}
Body(loc_pattern, loc_expr) => {
fmt_body(buf, &loc_pattern.value, &loc_expr.value, indent);
fmt_body(buf, true, &loc_pattern.value, &loc_expr.value, indent);
}
Dbg { condition, .. } => fmt_dbg_in_def(buf, condition, self.is_multiline(), indent),
Expect { condition, .. } => fmt_expect(buf, condition, self.is_multiline(), indent),
@ -452,12 +859,25 @@ impl<'a> Formattable for ValueDef<'a> {
body_pattern,
body_expr,
} => {
fmt_general_def(ann_pattern, buf, indent, ":", &ann_type.value, newlines);
let pat_parens = if ann_pattern_needs_parens(&ann_pattern.value) {
Parens::InApply
} else {
Parens::NotNeeded
};
fmt_general_def(
ann_pattern,
pat_parens,
buf,
indent,
":",
&ann_type.value,
newlines,
);
fmt_annotated_body_comment(buf, indent, lines_between);
buf.newline();
fmt_body(buf, &body_pattern.value, &body_expr.value, indent);
fmt_body(buf, false, &body_pattern.value, &body_expr.value, indent);
}
ModuleImport(module_import) => module_import.format(buf, indent),
IngestedFileImport(ingested_file_import) => ingested_file_import.format(buf, indent),
@ -467,15 +887,27 @@ impl<'a> Formattable for ValueDef<'a> {
}
}
fn ann_pattern_needs_parens(value: &Pattern<'_>) -> bool {
match value.extract_spaces().item {
Pattern::Tag(_) => true,
Pattern::Apply(func, _args) if matches!(func.extract_spaces().item, Pattern::Tag(..)) => {
true
}
_ => false,
}
}
fn fmt_general_def<L: Formattable>(
lhs: L,
lhs_parens: Parens,
buf: &mut Buf,
indent: u16,
sep: &str,
rhs: &TypeAnnotation,
newlines: Newlines,
) {
lhs.format(buf, indent);
lhs.format_with_options(buf, lhs_parens, Newlines::Yes, indent);
buf.indent(indent);
if rhs.is_multiline() {
@ -483,20 +915,25 @@ fn fmt_general_def<L: Formattable>(
buf.push_str(sep);
buf.spaces(1);
let should_outdent = should_outdent(rhs);
let rhs_lifted = ann_lift_spaces(buf.text.bump(), rhs);
if should_outdent {
match rhs {
TypeAnnotation::SpaceBefore(sub_def, _) => {
sub_def.format_with_options(buf, Parens::NotNeeded, Newlines::No, indent);
}
_ => {
rhs.format_with_options(buf, Parens::NotNeeded, Newlines::No, indent);
}
}
if ty_is_outdentable(&rhs_lifted.item) && rhs_lifted.before.iter().all(|s| s.is_newline()) {
rhs_lifted
.item
.format_with_options(buf, Parens::NotNeeded, Newlines::No, indent);
} else {
rhs.format_with_options(buf, Parens::NotNeeded, newlines, indent + INDENT);
buf.ensure_ends_with_newline();
fmt_comments_only(
buf,
rhs_lifted.before.iter(),
NewlineAt::Bottom,
indent + INDENT,
);
rhs_lifted
.item
.format_with_options(buf, Parens::NotNeeded, newlines, indent + INDENT);
}
fmt_comments_only(buf, rhs_lifted.after.iter(), NewlineAt::Bottom, indent);
} else {
buf.spaces(1);
buf.push_str(sep);
@ -505,28 +942,6 @@ fn fmt_general_def<L: Formattable>(
}
}
fn should_outdent(mut rhs: &TypeAnnotation) -> bool {
loop {
match rhs {
TypeAnnotation::SpaceBefore(sub_def, spaces) => {
let is_only_newlines = spaces.iter().all(|s| s.is_newline());
if !is_only_newlines || !sub_def.is_multiline() {
return false;
}
rhs = sub_def;
}
TypeAnnotation::Where(ann, _clauses) => {
if !ann.is_multiline() {
return false;
}
rhs = &ann.value;
}
TypeAnnotation::Record { .. } | TypeAnnotation::TagUnion { .. } => return true,
_ => return false,
}
}
}
fn fmt_dbg_in_def<'a>(buf: &mut Buf, condition: &'a Loc<Expr<'a>>, _: bool, indent: u16) {
buf.ensure_ends_with_newline();
buf.indent(indent);
@ -608,21 +1023,44 @@ pub fn fmt_annotated_body_comment<'a>(
}
}
pub fn fmt_body<'a>(buf: &mut Buf, pattern: &'a Pattern<'a>, body: &'a Expr<'a>, indent: u16) {
pub fn fmt_body<'a>(
buf: &mut Buf,
allow_simplify_empty_record_destructure: bool,
pattern: &'a Pattern<'a>,
body: &'a Expr<'a>,
indent: u16,
) {
let pattern_extracted = pattern.extract_spaces();
// Check if this is an assignment into the unit value
let is_unit_assignment = if let Pattern::RecordDestructure(collection) = pattern {
collection.is_empty()
let is_unit_assignment = if let Pattern::RecordDestructure(collection) = pattern_extracted.item
{
allow_simplify_empty_record_destructure
&& collection.is_empty()
&& pattern_extracted.before.iter().all(|s| s.is_newline())
&& pattern_extracted.after.iter().all(|s| s.is_newline())
&& !matches!(body.extract_spaces().item, Expr::Defs(..))
} else {
false
};
// Don't format the `{} =` for defs with this pattern
if !is_unit_assignment {
pattern.format_with_options(buf, Parens::InApply, Newlines::No, indent);
buf.indent(indent);
buf.push_str(" =");
if is_unit_assignment {
return body.format_with_options(buf, Parens::NotNeeded, Newlines::No, indent);
}
pattern.format_with_options(buf, Parens::InApply, Newlines::No, indent);
if pattern.is_multiline() {
buf.ensure_ends_with_newline();
buf.indent(indent);
} else {
buf.spaces(1);
}
buf.push_str("=");
let body = expr_lift_and_lower(Parens::NotNeeded, buf.text.bump(), body);
if body.is_multiline() {
match body {
Expr::SpaceBefore(sub_def, spaces) => {
@ -634,7 +1072,10 @@ pub fn fmt_body<'a>(buf: &mut Buf, pattern: &'a Pattern<'a>, body: &'a Expr<'a>,
_ => false,
};
if should_outdent {
if is_unit_assignment {
fmt_comments_only(buf, spaces.iter(), NewlineAt::Bottom, indent);
sub_def.format_with_options(buf, Parens::NotNeeded, Newlines::Yes, indent);
} else if should_outdent {
buf.spaces(1);
sub_def.format_with_options(buf, Parens::NotNeeded, Newlines::Yes, indent);
} else {
@ -646,6 +1087,32 @@ pub fn fmt_body<'a>(buf: &mut Buf, pattern: &'a Pattern<'a>, body: &'a Expr<'a>,
);
}
}
Expr::Apply(
Loc {
value: Expr::Str(StrLiteral::Block(..)),
..
},
..,
) => {
buf.spaces(1);
body.format_with_options(buf, Parens::NotNeeded, Newlines::Yes, indent + INDENT);
}
Expr::Str(s) => {
if is_str_multiline(&s) {
buf.ensure_ends_with_newline();
} else {
buf.spaces(1);
}
body.format_with_options(buf, Parens::NotNeeded, Newlines::Yes, indent + INDENT);
}
_ if starts_with_block_string_literal(&body) => {
buf.ensure_ends_with_newline();
body.format_with_options(buf, Parens::NotNeeded, Newlines::Yes, indent + INDENT);
}
Expr::When(..) => {
buf.ensure_ends_with_newline();
body.format_with_options(buf, Parens::NotNeeded, Newlines::Yes, indent + INDENT);
}
Expr::Defs(..) | Expr::BinOps(_, _) | Expr::Backpassing(..) => {
// Binop chains always get a newline. Otherwise you can have things like:
//
@ -662,9 +1129,15 @@ pub fn fmt_body<'a>(buf: &mut Buf, pattern: &'a Pattern<'a>, body: &'a Expr<'a>,
buf.newline();
body.format_with_options(buf, Parens::NotNeeded, Newlines::Yes, indent + INDENT);
}
Expr::When(..) | Expr::Str(StrLiteral::Block(_)) => {
buf.ensure_ends_with_newline();
body.format_with_options(buf, Parens::NotNeeded, Newlines::Yes, indent + INDENT);
Expr::ParensAround(&Expr::SpaceBefore(sub_def, _)) => {
let needs_indent = !sub_expr_requests_parens(sub_def);
let indent = if needs_indent {
indent + INDENT
} else {
indent
};
buf.spaces(1);
body.format_with_options(buf, Parens::NotNeeded, Newlines::Yes, indent);
}
_ => {
buf.spaces(1);
@ -677,6 +1150,18 @@ pub fn fmt_body<'a>(buf: &mut Buf, pattern: &'a Pattern<'a>, body: &'a Expr<'a>,
}
}
pub fn starts_with_block_string_literal(expr: &Expr<'_>) -> bool {
match expr {
Expr::Str(s) => is_str_multiline(s),
Expr::SpaceAfter(inner, _) | Expr::SpaceBefore(inner, _) => {
starts_with_block_string_literal(inner)
}
Expr::Apply(inner, _, _) => starts_with_block_string_literal(&inner.value),
Expr::TrySuffix { target: _, expr } => starts_with_block_string_literal(expr),
_ => false,
}
}
impl<'a> Formattable for AbilityMember<'a> {
fn is_multiline(&self) -> bool {
self.name.value.is_multiline() || self.typ.is_multiline()
@ -687,6 +1172,7 @@ impl<'a> Formattable for AbilityMember<'a> {
buf: &mut Buf,
_parens: Parens,
_newlines: Newlines,
indent: u16,
) {
let Spaces { before, item, .. } = self.name.value.extract_spaces();

File diff suppressed because it is too large Load diff

View file

@ -84,6 +84,7 @@ impl<V: Formattable> Formattable for Option<V> {
buf: &mut Buf,
parens: crate::annotation::Parens,
newlines: Newlines,
indent: u16,
) {
if let Some(v) = self {
@ -109,6 +110,7 @@ impl<'a> Formattable for ProvidesTo<'a> {
buf: &mut Buf,
_parens: crate::annotation::Parens,
_newlines: Newlines,
indent: u16,
) {
self.provides_keyword.format(buf, indent);
@ -128,6 +130,7 @@ impl<'a> Formattable for PlatformRequires<'a> {
buf: &mut Buf,
_parens: crate::annotation::Parens,
_newlines: Newlines,
indent: u16,
) {
fmt_requires(buf, self, indent);
@ -144,6 +147,7 @@ impl<'a, V: Formattable> Formattable for Spaces<'a, V> {
buf: &mut Buf,
parens: crate::annotation::Parens,
newlines: Newlines,
indent: u16,
) {
fmt_default_spaces(buf, self.before, indent);
@ -280,6 +284,7 @@ impl<'a> Formattable for TypedIdent<'a> {
buf: &mut Buf,
_parens: Parens,
_newlines: Newlines,
indent: u16,
) {
buf.indent(indent);
@ -316,6 +321,7 @@ impl<'a, T: Formattable> Formattable for Spaced<'a, T> {
buf: &mut Buf,
parens: crate::annotation::Parens,
newlines: Newlines,
indent: u16,
) {
match self {
@ -337,6 +343,7 @@ impl<'a, T: Formattable> Formattable for Spaced<'a, T> {
fn fmt_imports<'a>(
buf: &mut Buf,
loc_entries: Collection<'a, Loc<Spaced<'a, ImportsEntry<'a>>>>,
indent: u16,
) {
fmt_collection(buf, indent, Braces::Square, loc_entries, Newlines::No)
@ -346,6 +353,7 @@ fn fmt_provides<'a>(
buf: &mut Buf,
loc_exposed_names: Collection<'a, Loc<Spaced<'a, ExposedName<'a>>>>,
loc_provided_types: Option<Collection<'a, Loc<Spaced<'a, UppercaseIdent<'a>>>>>,
indent: u16,
) {
fmt_collection(buf, indent, Braces::Square, loc_exposed_names, Newlines::No);
@ -367,6 +375,7 @@ fn fmt_to(buf: &mut Buf, to: To, indent: u16) {
fn fmt_exposes<N: Formattable + Copy + core::fmt::Debug>(
buf: &mut Buf,
loc_entries: Collection<'_, Loc<Spaced<'_, N>>>,
indent: u16,
) {
fmt_collection(buf, indent, Braces::Square, loc_entries, Newlines::No)
@ -445,6 +454,7 @@ impl<'a> Formattable for PackageEntry<'a> {
buf: &mut Buf,
_parens: Parens,
_newlines: Newlines,
indent: u16,
) {
fmt_packages_entry(buf, self, indent);
@ -461,6 +471,7 @@ impl<'a> Formattable for ImportsEntry<'a> {
buf: &mut Buf,
_parens: Parens,
_newlines: Newlines,
indent: u16,
) {
fmt_imports_entry(buf, self, indent);

View file

@ -18,18 +18,41 @@ pub struct Buf<'a> {
spaces_to_flush: usize,
newlines_to_flush: usize,
beginning_of_line: bool,
line_indent: u16,
flags: MigrationFlags,
}
#[derive(Debug, Copy, Clone)]
pub struct MigrationFlags {
pub(crate) snakify: bool,
}
impl MigrationFlags {
pub fn new(snakify: bool) -> Self {
MigrationFlags { snakify }
}
pub fn at_least_one_active(&self) -> bool {
self.snakify
}
}
impl<'a> Buf<'a> {
pub fn new_in(arena: &'a Bump) -> Buf<'a> {
pub fn new_in(arena: &'a Bump, flags: MigrationFlags) -> Buf<'a> {
Buf {
text: String::new_in(arena),
line_indent: 0,
spaces_to_flush: 0,
newlines_to_flush: 0,
beginning_of_line: true,
flags,
}
}
pub fn flags(&self) -> MigrationFlags {
self.flags
}
pub fn as_str(&'a self) -> &'a str {
self.text.as_str()
}
@ -40,11 +63,18 @@ impl<'a> Buf<'a> {
pub fn indent(&mut self, indent: u16) {
if self.beginning_of_line {
self.line_indent = indent;
self.spaces_to_flush = indent as usize;
}
self.beginning_of_line = false;
}
pub fn cur_line_indent(&self) -> u16 {
debug_assert!(!self.beginning_of_line, "cur_line_indent before indent");
self.line_indent
}
#[track_caller]
pub fn push(&mut self, ch: char) {
debug_assert!(!self.beginning_of_line);
debug_assert!(
@ -61,6 +91,7 @@ impl<'a> Buf<'a> {
self.text.push(ch);
}
#[track_caller]
pub fn push_str_allow_spaces(&mut self, s: &str) {
debug_assert!(
!self.beginning_of_line,
@ -73,6 +104,7 @@ impl<'a> Buf<'a> {
self.text.push_str(s);
}
#[track_caller]
pub fn push_str(&mut self, s: &str) {
debug_assert!(
!self.beginning_of_line,
@ -129,6 +161,12 @@ impl<'a> Buf<'a> {
}
}
pub fn ensure_ends_with_whitespace(&mut self) {
if !self.text.is_empty() && self.newlines_to_flush == 0 && self.spaces_to_flush == 0 {
self.spaces_to_flush = 1;
}
}
fn flush_spaces(&mut self) {
for _ in 0..self.newlines_to_flush {
self.text.push('\n');

View file

@ -1,8 +1,15 @@
use crate::annotation::{Formattable, Newlines, Parens};
use crate::expr::{fmt_str_literal, format_sq_literal, is_str_multiline};
use crate::expr::{
expr_is_multiline, expr_lift_spaces_after, fmt_str_literal, format_sq_literal, is_str_multiline,
};
use crate::spaces::{fmt_comments_only, fmt_spaces, NewlineAt, INDENT};
use crate::Buf;
use roc_parse::ast::{Base, CommentOrNewline, Pattern, PatternAs};
use bumpalo::Bump;
use roc_parse::ast::{
Base, CommentOrNewline, Pattern, PatternAs, Spaceable, Spaces, SpacesAfter, SpacesBefore,
};
use roc_parse::expr::merge_spaces;
use roc_region::all::Loc;
pub fn fmt_pattern<'a>(buf: &mut Buf, pattern: &'a Pattern<'a>, indent: u16, parens: Parens) {
pattern.format_with_options(buf, parens, Newlines::No, indent);
@ -54,7 +61,7 @@ impl<'a> Formattable for Pattern<'a> {
Pattern::RecordDestructure(fields) => fields.iter().any(|f| f.is_multiline()),
Pattern::RequiredField(_, subpattern) => subpattern.is_multiline(),
Pattern::OptionalField(_, expr) => expr.is_multiline(),
Pattern::OptionalField(_, expr) => expr_is_multiline(&expr.value, true),
Pattern::As(pattern, pattern_as) => pattern.is_multiline() || pattern_as.is_multiline(),
Pattern::ListRest(opt_pattern_as) => match opt_pattern_as {
@ -86,222 +93,347 @@ impl<'a> Formattable for Pattern<'a> {
}
}
fn format_with_options(&self, buf: &mut Buf, parens: Parens, newlines: Newlines, indent: u16) {
use self::Pattern::*;
fn format_with_options(&self, buf: &mut Buf, parens: Parens, _newlines: Newlines, indent: u16) {
fmt_pattern_inner(self, buf, parens, indent, self.is_multiline(), false);
}
}
match self {
Identifier { ident: string } => {
buf.indent(indent);
buf.push_str(string);
}
Tag(name) | OpaqueRef(name) => {
buf.indent(indent);
buf.push_str(name);
}
Apply(loc_pattern, loc_arg_patterns) => {
buf.indent(indent);
// Sometimes, an Apply pattern needs parens around it.
// In particular when an Apply's argument is itself an Apply (> 0) arguments
let parens = !loc_arg_patterns.is_empty() && (parens == Parens::InApply);
fn fmt_pattern_inner(
pat: &Pattern<'_>,
buf: &mut Buf,
parens: Parens,
let indent_more = if self.is_multiline() {
indent + INDENT
indent: u16,
outer_is_multiline: bool,
force_newline_at_start: bool,
) -> bool {
let me = pattern_lift_spaces(buf.text.bump(), pat);
let mut was_multiline = me.item.is_multiline();
if !me.before.is_empty() {
if !outer_is_multiline {
was_multiline |= me.before.iter().any(|s| s.is_comment());
fmt_comments_only(buf, me.before.iter(), NewlineAt::Bottom, indent)
} else {
was_multiline |= true;
fmt_spaces(buf, me.before.iter(), indent);
}
}
if force_newline_at_start {
buf.ensure_ends_with_newline();
}
let is_multiline = me.item.is_multiline();
fmt_pattern_only(me, buf, indent, parens, is_multiline);
if !me.after.is_empty() {
if starts_with_inline_comment(me.after.iter()) {
buf.spaces(1);
}
if !outer_is_multiline {
was_multiline |= me.before.iter().any(|s| s.is_comment());
fmt_comments_only(buf, me.after.iter(), NewlineAt::Bottom, indent)
} else {
was_multiline |= true;
fmt_spaces(buf, me.after.iter(), indent);
}
}
was_multiline
}
fn fmt_pattern_only(
me: Spaces<'_, Pattern<'_>>,
buf: &mut Buf<'_>,
indent: u16,
parens: Parens,
is_multiline: bool,
) {
match me.item {
Pattern::Identifier { ident: string } => {
buf.indent(indent);
snakify_camel_ident(buf, string);
}
Pattern::Tag(name) | Pattern::OpaqueRef(name) => {
buf.indent(indent);
buf.push_str(name);
}
Pattern::Apply(loc_pattern, loc_arg_patterns) => {
buf.indent(indent);
// Sometimes, an Apply pattern needs parens around it.
// In particular when an Apply's argument is itself an Apply (> 0) arguments
let parens = !loc_arg_patterns.is_empty() && (parens == Parens::InApply);
let indent_more = if is_multiline {
indent + INDENT
} else {
indent
};
if parens {
buf.push('(');
}
let pat = pattern_lift_spaces(buf.text.bump(), &loc_pattern.value);
if !pat.before.is_empty() {
if !is_multiline {
fmt_comments_only(buf, pat.before.iter(), NewlineAt::Bottom, indent)
} else {
indent
};
if parens {
buf.push('(');
}
loc_pattern.format_with_options(buf, Parens::InApply, Newlines::No, indent);
for loc_arg in loc_arg_patterns.iter() {
buf.spaces(1);
loc_arg.format_with_options(buf, Parens::InApply, Newlines::No, indent_more);
}
if parens {
buf.push(')');
fmt_spaces(buf, pat.before.iter(), indent);
}
}
RecordDestructure(loc_patterns) => {
buf.indent(indent);
buf.push_str("{");
if !loc_patterns.is_empty() {
buf.spaces(1);
let mut it = loc_patterns.iter().peekable();
while let Some(loc_pattern) = it.next() {
loc_pattern.format(buf, indent);
fmt_pattern_inner(&pat.item, buf, Parens::InApply, indent, is_multiline, false);
if it.peek().is_some() {
buf.push_str(",");
buf.spaces(1);
if !pat.after.is_empty() {
if !is_multiline {
fmt_comments_only(buf, pat.after.iter(), NewlineAt::Bottom, indent_more)
} else {
fmt_spaces(buf, pat.after.iter(), indent_more);
}
}
let mut add_newlines = false;
for loc_arg in loc_arg_patterns.iter() {
buf.spaces(1);
let was_multiline = fmt_pattern_inner(
&loc_arg.value,
buf,
Parens::InApply,
indent_more,
is_multiline,
add_newlines,
);
add_newlines |= was_multiline;
}
if parens {
buf.push(')');
}
}
Pattern::RecordDestructure(loc_patterns) => {
buf.indent(indent);
buf.push_str("{");
if !loc_patterns.is_empty() {
buf.spaces(1);
let mut it = loc_patterns.iter().peekable();
while let Some(loc_pattern) = it.next() {
let item = pattern_lift_spaces(buf.text.bump(), &loc_pattern.value);
if !item.before.is_empty() {
if !is_multiline {
fmt_comments_only(buf, item.before.iter(), NewlineAt::Bottom, indent)
} else {
fmt_spaces(buf, item.before.iter(), indent);
}
}
fmt_pattern_inner(
&item.item,
buf,
Parens::NotNeeded,
indent,
is_multiline,
false,
);
let is_multiline = item.item.is_multiline();
if it.peek().is_some() {
buf.push_str(",");
}
if !item.after.is_empty() {
if starts_with_inline_comment(item.after.iter()) {
buf.spaces(1);
}
if !is_multiline {
fmt_comments_only(buf, item.after.iter(), NewlineAt::Bottom, indent)
} else {
fmt_spaces(buf, item.after.iter(), indent);
}
}
if it.peek().is_some() {
buf.ensure_ends_with_whitespace();
}
}
buf.spaces(1);
}
buf.indent(indent);
buf.push_str("}");
}
Pattern::RequiredField(name, loc_pattern) => {
buf.indent(indent);
snakify_camel_ident(buf, name);
buf.push_str(":");
buf.spaces(1);
fmt_pattern_inner(
&loc_pattern.value,
buf,
Parens::NotNeeded,
indent,
is_multiline,
false,
);
}
Pattern::OptionalField(name, loc_pattern) => {
buf.indent(indent);
snakify_camel_ident(buf, name);
buf.push_str(" ?");
buf.spaces(1);
loc_pattern.format(buf, indent);
}
Pattern::NumLiteral(string) => {
buf.indent(indent);
buf.push_str(string);
}
Pattern::NonBase10Literal {
base,
string,
is_negative,
} => {
buf.indent(indent);
if is_negative {
buf.push('-');
}
match base {
Base::Hex => buf.push_str("0x"),
Base::Octal => buf.push_str("0o"),
Base::Binary => buf.push_str("0b"),
Base::Decimal => { /* nothing */ }
}
buf.push_str(string);
}
Pattern::FloatLiteral(string) => {
buf.indent(indent);
buf.push_str(string);
}
Pattern::StrLiteral(literal) => fmt_str_literal(buf, literal, indent),
Pattern::SingleQuote(string) => {
buf.indent(indent);
format_sq_literal(buf, string);
}
Pattern::Underscore(name) => {
buf.indent(indent);
buf.push('_');
buf.push_str(name);
}
Pattern::Tuple(loc_patterns) => {
buf.indent(indent);
buf.push_str("(");
let mut it = loc_patterns.iter().peekable();
while let Some(loc_pattern) = it.next() {
fmt_pattern_inner(
&loc_pattern.value,
buf,
Parens::NotNeeded,
indent,
is_multiline,
false,
);
if it.peek().is_some() {
buf.indent(indent);
buf.push_str(",");
buf.spaces(1);
}
buf.push_str("}");
}
RequiredField(name, loc_pattern) => {
buf.indent(indent);
buf.push_str(name);
buf.push_str(":");
buf.spaces(1);
loc_pattern.format(buf, indent);
}
buf.indent(indent);
buf.push_str(")");
}
Pattern::List(loc_patterns) => {
buf.indent(indent);
buf.push_str("[");
OptionalField(name, loc_pattern) => {
buf.indent(indent);
buf.push_str(name);
buf.push_str(" ?");
buf.spaces(1);
loc_pattern.format(buf, indent);
}
let mut it = loc_patterns.iter().peekable();
while let Some(loc_pattern) = it.next() {
fmt_pattern_inner(
&loc_pattern.value,
buf,
Parens::NotNeeded,
indent,
is_multiline,
false,
);
&NumLiteral(string) => {
buf.indent(indent);
buf.push_str(string);
}
&NonBase10Literal {
base,
string,
is_negative,
} => {
buf.indent(indent);
if is_negative {
buf.push('-');
}
match base {
Base::Hex => buf.push_str("0x"),
Base::Octal => buf.push_str("0o"),
Base::Binary => buf.push_str("0b"),
Base::Decimal => { /* nothing */ }
}
buf.push_str(string);
}
&FloatLiteral(string) => {
buf.indent(indent);
buf.push_str(string);
}
StrLiteral(literal) => fmt_str_literal(buf, *literal, indent),
SingleQuote(string) => {
buf.indent(indent);
format_sq_literal(buf, string);
}
Underscore(name) => {
buf.indent(indent);
buf.push('_');
buf.push_str(name);
}
Tuple(loc_patterns) => {
buf.indent(indent);
buf.push_str("(");
let mut it = loc_patterns.iter().peekable();
while let Some(loc_pattern) = it.next() {
loc_pattern.format(buf, indent);
if it.peek().is_some() {
buf.push_str(",");
buf.spaces(1);
}
}
buf.push_str(")");
}
List(loc_patterns) => {
buf.indent(indent);
buf.push_str("[");
let mut it = loc_patterns.iter().peekable();
while let Some(loc_pattern) = it.next() {
loc_pattern.format(buf, indent);
if it.peek().is_some() {
buf.push_str(",");
buf.spaces(1);
}
}
buf.push_str("]");
}
ListRest(opt_pattern_as) => {
buf.indent(indent);
buf.push_str("..");
if let Some((list_rest_spaces, pattern_as)) = opt_pattern_as {
// these spaces "belong" to the `..`, which can never be multiline
fmt_comments_only(buf, list_rest_spaces.iter(), NewlineAt::Bottom, indent);
pattern_as.format(buf, indent + INDENT);
if it.peek().is_some() {
buf.indent(indent);
buf.push_str(",");
buf.spaces(1);
}
}
As(pattern, pattern_as) => {
let needs_parens = parens == Parens::InAsPattern;
buf.indent(indent);
buf.push_str("]");
}
Pattern::ListRest(opt_pattern_as) => {
buf.indent(indent);
buf.push_str("..");
if needs_parens {
buf.push('(');
}
fmt_pattern(buf, &pattern.value, indent, parens);
if let Some((list_rest_spaces, pattern_as)) = opt_pattern_as {
// these spaces "belong" to the `..`, which can never be multiline
fmt_comments_only(buf, list_rest_spaces.iter(), NewlineAt::Bottom, indent);
pattern_as.format(buf, indent + INDENT);
if needs_parens {
buf.push(')');
}
}
}
// Space
SpaceBefore(sub_pattern, spaces) => {
if !sub_pattern.is_multiline() {
fmt_comments_only(buf, spaces.iter(), NewlineAt::Bottom, indent)
} else {
fmt_spaces(buf, spaces.iter(), indent);
}
Pattern::As(pattern, pattern_as) => {
let needs_parens = parens == Parens::InAsPattern;
sub_pattern.format_with_options(buf, parens, newlines, indent);
}
SpaceAfter(sub_pattern, spaces) => {
sub_pattern.format_with_options(buf, parens, newlines, indent);
if starts_with_inline_comment(spaces.iter()) {
buf.spaces(1);
}
if !sub_pattern.is_multiline() {
fmt_comments_only(buf, spaces.iter(), NewlineAt::Bottom, indent)
} else {
fmt_spaces(buf, spaces.iter(), indent);
}
}
// Malformed
Malformed(string) | MalformedIdent(string, _) => {
if needs_parens {
buf.indent(indent);
buf.push_str(string);
buf.push('(');
}
QualifiedIdentifier { module_name, ident } => {
buf.indent(indent);
if !module_name.is_empty() {
buf.push_str(module_name);
buf.push('.');
}
buf.push_str(ident);
fmt_pattern(buf, &pattern.value, indent, parens);
pattern_as.format(buf, indent + INDENT);
if needs_parens {
buf.indent(indent);
buf.push(')');
}
}
Pattern::SpaceBefore(..) | Pattern::SpaceAfter(..) => {
unreachable!("handled by lift_spaces")
}
// Malformed
Pattern::Malformed(string) | Pattern::MalformedIdent(string, _) => {
buf.indent(indent);
buf.push_str(string);
}
Pattern::QualifiedIdentifier { module_name, ident } => {
buf.indent(indent);
if !module_name.is_empty() {
buf.push_str(module_name);
buf.push('.');
}
snakify_camel_ident(buf, ident);
}
}
}
fn starts_with_inline_comment<'a, I: IntoIterator<Item = &'a CommentOrNewline<'a>>>(
pub fn starts_with_inline_comment<'a, I: IntoIterator<Item = &'a CommentOrNewline<'a>>>(
spaces: I,
) -> bool {
matches!(
@ -309,3 +441,192 @@ fn starts_with_inline_comment<'a, I: IntoIterator<Item = &'a CommentOrNewline<'a
Some(CommentOrNewline::LineComment(_))
)
}
pub fn pattern_lift_spaces<'a, 'b: 'a>(
arena: &'a Bump,
pat: &Pattern<'b>,
) -> Spaces<'a, Pattern<'a>> {
match pat {
Pattern::Apply(func, args) => {
let func_lifted = pattern_lift_spaces(arena, &func.value);
let args = arena.alloc_slice_copy(args);
let (before, func, after) = if let Some(last) = args.last_mut() {
let last_lifted = pattern_lift_spaces(arena, &last.value);
if last_lifted.before.is_empty() {
*last = Loc::at(last.region, last_lifted.item)
} else {
*last = Loc::at(
last.region,
Pattern::SpaceBefore(arena.alloc(last_lifted.item), last_lifted.before),
);
}
let f = if func_lifted.after.is_empty() {
func_lifted.item
} else {
Pattern::SpaceAfter(arena.alloc(func_lifted.item), func_lifted.after)
};
(
func_lifted.before,
Loc::at(func.region, f),
last_lifted.after,
)
} else {
(
func_lifted.before,
Loc::at(func.region, func_lifted.item),
func_lifted.after,
)
};
Spaces {
before,
item: Pattern::Apply(arena.alloc(func), args),
after,
}
}
Pattern::OptionalField(name, expr) => {
let lifted = expr_lift_spaces_after(Parens::NotNeeded, arena, &expr.value);
Spaces {
before: &[],
item: Pattern::OptionalField(name, arena.alloc(Loc::at(expr.region, lifted.item))),
after: lifted.after,
}
}
Pattern::RequiredField(name, pat) => {
let lifted = pattern_lift_spaces_after(arena, &pat.value);
Spaces {
before: &[],
item: Pattern::RequiredField(name, arena.alloc(Loc::at(pat.region, lifted.item))),
after: lifted.after,
}
}
Pattern::SpaceBefore(expr, spaces) => {
let mut inner = pattern_lift_spaces(arena, expr);
inner.before = merge_spaces(arena, spaces, inner.before);
inner
}
Pattern::SpaceAfter(expr, spaces) => {
let mut inner = pattern_lift_spaces(arena, expr);
inner.after = merge_spaces(arena, inner.after, spaces);
inner
}
_ => Spaces {
before: &[],
item: *pat,
after: &[],
},
}
}
pub fn pattern_lift_spaces_before<'a, 'b: 'a>(
arena: &'a Bump,
pat: &Pattern<'b>,
) -> SpacesBefore<'a, Pattern<'a>> {
let lifted = pattern_lift_spaces(arena, pat);
SpacesBefore {
before: lifted.before,
item: lifted.item.maybe_after(arena, lifted.after),
}
}
pub fn pattern_lift_spaces_after<'a, 'b: 'a>(
arena: &'a Bump,
pat: &Pattern<'b>,
) -> SpacesAfter<'a, Pattern<'a>> {
let lifted = pattern_lift_spaces(arena, pat);
SpacesAfter {
item: lifted.item.maybe_before(arena, lifted.before),
after: lifted.after,
}
}
/// Convert camelCase identifier to snake case
fn snakify_camel_ident(buf: &mut Buf, string: &str) {
let chars: Vec<char> = string.chars().collect();
if !buf.flags().snakify || (string.contains('_') && !string.ends_with('_')) {
buf.push_str(string);
return;
}
let mut index = 0;
let len = chars.len();
while index < len {
let prev = if index == 0 {
None
} else {
Some(chars[index - 1])
};
let c = chars[index];
let next = chars.get(index + 1);
let boundary = match (prev, c, next) {
// LUU, LUN, and LUL (simplified to LU_)
(Some(p), curr, _) if !p.is_ascii_uppercase() && curr.is_ascii_uppercase() => true,
// UUL
(Some(p), curr, Some(n))
if p.is_ascii_uppercase()
&& curr.is_ascii_uppercase()
&& n.is_ascii_lowercase() =>
{
true
}
_ => false,
};
// those are boundary transitions - should push _ and curr
if boundary {
buf.push('_');
}
buf.push(c.to_ascii_lowercase());
index += 1;
}
}
#[cfg(test)]
mod snakify_test {
use bumpalo::Bump;
use super::snakify_camel_ident;
use crate::{Buf, MigrationFlags};
fn check_snakify(arena: &Bump, original: &str) -> String {
let flags = MigrationFlags::new(true);
let mut buf = Buf::new_in(arena, flags);
buf.indent(0);
snakify_camel_ident(&mut buf, original);
buf.text.to_string()
}
#[test]
fn test_snakify_camel_ident() {
let arena = Bump::new();
assert_eq!(check_snakify(&arena, "A"), "a");
assert_eq!(check_snakify(&arena, "Ba"), "ba");
assert_eq!(check_snakify(&arena, "aB"), "a_b");
assert_eq!(check_snakify(&arena, "aBa"), "a_ba");
assert_eq!(check_snakify(&arena, "mBB"), "m_bb");
assert_eq!(check_snakify(&arena, "NbA"), "nb_a");
assert_eq!(check_snakify(&arena, "doIT"), "do_it");
assert_eq!(check_snakify(&arena, "ROC"), "roc");
assert_eq!(
check_snakify(&arena, "someHTTPRequest"),
"some_http_request"
);
assert_eq!(check_snakify(&arena, "usingXML"), "using_xml");
assert_eq!(check_snakify(&arena, "some123"), "some123");
assert_eq!(
check_snakify(&arena, "theHTTPStatus404"),
"the_http_status404"
);
assert_eq!(
check_snakify(&arena, "inThe99thPercentile"),
"in_the99th_percentile"
);
assert_eq!(
check_snakify(&arena, "all400SeriesErrorCodes"),
"all400_series_error_codes",
);
assert_eq!(check_snakify(&arena, "number4Yellow"), "number4_yellow");
assert_eq!(check_snakify(&arena, "useCases4Cobol"), "use_cases4_cobol");
assert_eq!(check_snakify(&arena, "c3PO"), "c3_po")
}
}

View file

@ -20,6 +20,43 @@ pub fn fmt_default_newline(buf: &mut Buf, spaces: &[CommentOrNewline], indent: u
}
}
pub enum SpacesNewlineMode {
Normal,
SkipNewlinesAtStart,
SkipNewlinesAtEnd,
SkipNewlinesAtBoth,
}
pub fn fmt_spaces_with_newline_mode(
buf: &mut Buf<'_>,
mut spaces: &[CommentOrNewline<'_>],
indent: u16,
mode: SpacesNewlineMode,
) {
if matches!(
mode,
SpacesNewlineMode::SkipNewlinesAtStart | SpacesNewlineMode::SkipNewlinesAtBoth
) {
let skip_count = spaces
.iter()
.take_while(|s| *s == &CommentOrNewline::Newline)
.count();
spaces = &spaces[skip_count..];
}
if matches!(
mode,
SpacesNewlineMode::SkipNewlinesAtEnd | SpacesNewlineMode::SkipNewlinesAtBoth
) {
let skip_count = spaces
.iter()
.rev()
.take_while(|s| *s == &CommentOrNewline::Newline)
.count();
spaces = &spaces[..spaces.len() - skip_count];
}
fmt_spaces(buf, spaces.iter(), indent);
}
/// Like fmt_spaces, but disallows two consecutive newlines.
pub fn fmt_spaces_no_blank_lines<'a, 'buf, I>(buf: &mut Buf<'buf>, spaces: I, indent: u16)
where

View file

@ -19,7 +19,7 @@ macro_rules! run_jit_function_raw {
let result = main();
if !$errors.is_empty() {
dbg!(&$errors);
eprintln!("{:?}", &$errors);
assert_eq!(
$errors,

View file

@ -12,8 +12,8 @@ use crate::llvm::refcounting::{
use inkwell::attributes::{Attribute, AttributeLoc};
use inkwell::types::{BasicType, BasicTypeEnum, StructType};
use inkwell::values::{
BasicValue, BasicValueEnum, CallSiteValue, FunctionValue, InstructionValue, IntValue,
PointerValue, StructValue,
BasicMetadataValueEnum, BasicValue, BasicValueEnum, CallSiteValue, FunctionValue,
InstructionValue, IntValue, PointerValue, StructValue,
};
use inkwell::AddressSpace;
use roc_error_macros::internal_error;
@ -726,13 +726,18 @@ pub fn build_compare_wrapper<'a, 'ctx>(
"load_opaque",
);
let closure_data =
env.builder
.new_build_load(closure_type, closure_cast, "load_opaque");
let closure_data: BasicMetadataValueEnum =
if layout_interner.is_passed_by_reference(closure_data_repr) {
closure_cast.into()
} else {
env.builder
.new_build_load(closure_type, closure_cast, "load_opaque")
.into()
};
env.arena
.alloc([value1.into(), value2.into(), closure_data.into()])
as &[_]
.alloc([value1.into(), value2.into(), closure_data])
as &[BasicMetadataValueEnum]
}
};

View file

@ -1147,15 +1147,37 @@ pub(crate) fn run_low_level<'a, 'ctx>(
)
}
NumIntCast => {
arguments!(arg);
arguments_with_layouts!((arg, arg_layout));
let to = basic_type_from_layout(env, layout_interner, layout_interner.get_repr(layout))
.into_int_type();
let to_signed = intwidth_from_layout(layout).is_signed();
let from_signed = intwidth_from_layout(arg_layout).is_signed();
let extend = intwidth_from_layout(layout).stack_size()
> intwidth_from_layout(arg_layout).stack_size();
//Examples given with sizes of 32, 16, and 8
let result = match (from_signed, to_signed, extend) {
//I16 -> I32
(true, true, true) => {
env.builder
.build_int_s_extend(arg.into_int_value(), to, "inc_cast")
}
//U16 -> X32
(false, _, true) => {
env.builder
.build_int_z_extend(arg.into_int_value(), to, "inc_cast")
},
//I16 -> U32
(true,false,true)
//Any case where it is not an extension, also perhaps warn here?
| (_, _, false) => {
Ok(env.builder
.new_build_int_cast_sign_flag(arg.into_int_value(), to, to_signed, "inc_cast"))
}
};
env.builder
.new_build_int_cast_sign_flag(arg.into_int_value(), to, to_signed, "inc_cast")
.into()
let Ok(value) = result else { todo!() };
value.into()
}
NumToFloatCast => {
arguments_with_layouts!((arg, arg_layout));

View file

@ -1203,7 +1203,8 @@ mod test_reporting {
@r"
TYPE MISMATCH in /code/proj/Main.roc
This expression is used in an unexpected way:
This returns something that's incompatible with the return type of the
enclosing function:
5 f = \x -> g x
^^^
@ -1212,7 +1213,7 @@ mod test_reporting {
List List a
But you are trying to use it as:
But I expected the function to have return type:
List a
@ -1239,7 +1240,8 @@ mod test_reporting {
@r"
TYPE MISMATCH in /code/proj/Main.roc
This expression is used in an unexpected way:
This returns something that's incompatible with the return type of the
enclosing function:
7 g = \x -> f [x]
^^^^^
@ -1248,7 +1250,7 @@ mod test_reporting {
List List b
But you are trying to use it as:
But I expected the function to have return type:
List b
@ -6019,25 +6021,6 @@ All branches in an `if` must have the same type!
"
);
test_report!(
closure_underscore_ident,
indoc!(
r"
\the_answer -> 100
"
),
@r"
NAMING PROBLEM in /code/proj/Main.roc
I am trying to parse an identifier here:
4 \the_answer -> 100
^
Underscores are not allowed in identifiers. Use camelCase instead!
"
);
test_report!(
#[ignore]
double_binop,
@ -6626,7 +6609,7 @@ All branches in an `if` must have the same type!
@r"
UNFINISHED FUNCTION in tmp/unfinished_closure_pattern_in_parens/Test.roc
I was partway through parsing a function, but I got stuck here:
I was partway through parsing a function, but I got stuck here:
4 x = \( a
5 )
@ -10258,16 +10241,17 @@ All branches in an `if` must have the same type!
@r"
TYPE MISMATCH in /code/proj/Main.roc
This expression is used in an unexpected way:
This returns something that's incompatible with the return type of the
enclosing function:
5 f = \_ -> if Bool.true then {} else f {}
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
It is a value of type:
It a value of type:
{}
But you are trying to use it as:
But I expected the function to have return type:
* -> Str
"
@ -10414,11 +10398,12 @@ All branches in an `if` must have the same type!
Something is off with the 2nd branch of this `when` expression:
10 olist : OList
11 olist =
12> when alist is
13> Nil -> @OList Nil
14> Cons _ lst -> lst
10 olist : OList
11 olist =
12 when alist is
13 Nil -> @OList Nil
14 Cons _ lst -> lst
^^^
This `lst` value is a:
@ -10449,6 +10434,7 @@ All branches in an `if` must have the same type!
This 2nd argument to `map` has an unexpected type:
4 A := U8
5 List.map [1u16, 2u16, 3u16] @A
^^
@ -10697,26 +10683,26 @@ All branches in an `if` must have the same type!
);
test_report!(
underscore_in_middle_of_identifier,
call_with_declared_identifier_with_more_than_one_underscore,
indoc!(
r"
f = \x, y, z -> x + y + z
f__arg = \x, y, z -> x + y + z
\a, _b -> f a var_name 1
\a, b -> f__arg a b 1
"
),
|golden| pretty_assertions::assert_eq!(
golden,
indoc!(
r"
SYNTAX PROBLEM in /code/proj/Main.roc
r"── NAMING PROBLEM in /code/proj/Main.roc ───────────────────────────────────────
Underscores are not allowed in identifier names:
I am trying to parse an identifier here:
6 \a, _b -> f a var_name 1
^^^^^^^^
4 f__arg = \x, y, z -> x + y + z
^^^^^^
I recommend using camelCase. It's the standard style in Roc code!
Snake case is allowed here, but only a single consecutive underscore
should be used.
"
),
)
@ -14556,7 +14542,7 @@ All branches in an `if` must have the same type!
@r###"
RETURN OUTSIDE OF FUNCTION in /code/proj/Main.roc
This `return` statement doesn't belong to a function:
This `return` doesn't belong to a function:
7 return x
^^^^^^^^
@ -14634,27 +14620,121 @@ All branches in an `if` must have the same type!
myFunction 3
"#
),
@r#"
TYPE MISMATCH in /code/proj/Main.roc
This returns something that's incompatible with the return type of the
enclosing function:
5 if x == 5 then
6> return "abc"
7 else
8 x
This returns a value of type:
Str
But I expected the function to have return type:
Num *
"#
);
test_report!(
try_in_bare_statement,
indoc!(
r#"
app [main!] { pf: platform "../../../../../crates/cli/tests/test-projects/test-platform-effects-zig/main.roc" }
import pf.Effect
validateNum = \num ->
if num > 5 then
Ok {}
else
Err TooBig
main! = \{} ->
Effect.putLine! "hello"
# this returns {}, so it's ignored
try validateNum 10
# this returns a value, so we are incorrectly
# dropping the parsed value
try List.get [1, 2, 3] 5
Ok {}
"#
),
@r###"
TYPE MISMATCH in /code/proj/Main.roc
This `return` statement doesn't match the return type of its enclosing
function:
5 if x == 5 then
6> return "abc"
7 else
8 x
This returns a value of type:
Str
But I expected the function to have return type:
IGNORED RESULT in /code/proj/Main.roc
The result of this expression is ignored:
19 try List.get [1, 2, 3] 5
^^^^^^^^^^^^^^^^^^^^^^^^
Standalone statements are required to produce an empty record, but the
type of this one is:
Num *
If you still want to ignore it, assign it to `_`, like this:
_ = File.delete! "data.json"
"###
);
test_report!(
return_in_bare_statement,
indoc!(
r#"
app [main!] { pf: platform "../../../../../crates/cli/tests/test-projects/test-platform-effects-zig/main.roc" }
import pf.Effect
main! = \{} ->
Effect.putLine! "hello"
# this outputs {}, so it's ignored
if 7 > 5 then
{}
else
return Err TooBig
# this outputs a value, so we are incorrectly
# dropping the parsed value
when List.get [1, 2, 3] 5 is
Ok item -> item
Err err ->
return Err err
Ok {}
"#
),
@r#"
IGNORED RESULT in /code/proj/Main.roc
The result of this expression is ignored:
16> when List.get [1, 2, 3] 5 is
17> Ok item -> item
18> Err err ->
19> return Err err
Standalone statements are required to produce an empty record, but the
type of this one is:
Num *
If you still want to ignore it, assign it to `_`, like this:
_ = File.delete! "data.json"
"#
);
test_report!(
mismatch_only_early_returns,
indoc!(
@ -14668,28 +14748,200 @@ All branches in an `if` must have the same type!
myFunction 3
"#
),
@r#"
TYPE MISMATCH in /code/proj/Main.roc
This returns something that's incompatible with the return type of the
enclosing function:
5 if x == 5 then
6 return "abc"
7 else
8 return 123
^^^^^^^^^^
This returns a value of type:
Num *
But I expected the function to have return type:
Str
"#
);
test_report!(
try_with_ignored_output,
indoc!(
r#"
app [main!] { pf: platform "../../../../../crates/cli/tests/test-projects/test-platform-effects-zig/main.roc" }
import pf.Effect
main! = \{} ->
Effect.putLine! "hello"
# not ignored, warning
try List.get [1, 2, 3] 5
# ignored, OK
_ = try List.get [1, 2, 3] 5
_ignored = try List.get [1, 2, 3] 5
Ok {}
"#
),
@r###"
TYPE MISMATCH in /code/proj/Main.roc
IGNORED RESULT in /code/proj/Main.roc
This `return` statement doesn't match the return type of its enclosing
function:
The result of this expression is ignored:
5 if x == 5 then
6 return "abc"
7 else
8 return 123
^^^^^^^^^^
9 try List.get [1, 2, 3] 5
^^^^^^^^^^^^^^^^^^^^^^^^
This returns a value of type:
Standalone statements are required to produce an empty record, but the
type of this one is:
Num *
But I expected the function to have return type:
If you still want to ignore it, assign it to `_`, like this:
Str
_ = File.delete! "data.json"
"###
);
test_report!(
return_with_ignored_output,
indoc!(
r#"
app [main!] { pf: platform "../../../../../crates/cli/tests/test-projects/test-platform-effects-zig/main.roc" }
import pf.Effect
main! = \{} ->
Effect.putLine! "hello"
# not ignored, warning
when List.get [1, 2, 3] 5 is
Ok item -> item
Err err ->
return Err err
# ignored, OK
_ =
when List.get [1, 2, 3] 5 is
Ok item -> item
Err err ->
return Err err
# also ignored, also OK
_ignored =
when List.get [1, 2, 3] 5 is
Ok item -> item
Err err ->
return Err err
Ok {}
"#
),
@r#"
IGNORED RESULT in /code/proj/Main.roc
The result of this expression is ignored:
9> when List.get [1, 2, 3] 5 is
10> Ok item -> item
11> Err err ->
12> return Err err
Standalone statements are required to produce an empty record, but the
type of this one is:
Num *
If you still want to ignore it, assign it to `_`, like this:
_ = File.delete! "data.json"
"#
);
test_report!(
no_early_return_in_bare_statement,
indoc!(
r#"
app [main!] { pf: platform "../../../../../crates/cli/tests/test-projects/test-platform-effects-zig/main.roc" }
import pf.Effect
main! = \{} ->
Effect.putLine! "hello"
Num.toStr 123
Ok {}
"#
),
@r#"
IGNORED RESULT in /code/proj/Main.roc
The result of this call to `Num.toStr` is ignored:
8 Num.toStr 123
^^^^^^^^^
Standalone statements are required to produce an empty record, but the
type of this one is:
Str
If you still want to ignore it, assign it to `_`, like this:
_ = File.delete! "data.json"
LEFTOVER STATEMENT in /code/proj/Main.roc
This statement does not produce any effects:
8 Num.toStr 123
^^^^^^^^^^^^^
Standalone statements are only useful if they call effectful
functions.
Did you forget to use its result? If not, feel free to remove it.
"#
);
test_report!(
no_early_return_in_ignored_statement,
indoc!(
r#"
app [main!] { pf: platform "../../../../../crates/cli/tests/test-projects/test-platform-effects-zig/main.roc" }
import pf.Effect
main! = \{} ->
Effect.putLine! "hello"
_ignored = Num.toStr 123
Ok {}
"#
),
@r"
UNNECESSARY DEFINITION in /code/proj/Main.roc
This assignment doesn't introduce any new variables:
8 _ignored = Num.toStr 123
^^^^^^^^
Since it doesn't call any effectful functions, this assignment cannot
affect the program's behavior. If you don't need to use the value on
the right-hand side, consider removing the assignment.
"
);
test_report!(
mismatch_early_return_annotated_function,
indoc!(
@ -14726,6 +14978,225 @@ All branches in an `if` must have the same type!
"###
);
test_report!(
function_with_early_return_generalizes,
indoc!(
r#"
parseItemsWith = \parser ->
when List.mapTry ["123", "456"] parser is
Ok ok -> Ok ok
Err err ->
return Err err
u64Nums = parseItemsWith Str.toU64
u8Nums = parseItemsWith Str.toU8
"$(Inspect.toStr u64Nums) $(Inspect.toStr u8Nums)"
"#
),
@"" // no errors
);
test_report!(
keyword_try_with_non_result_target,
indoc!(
r#"
invalidTry = \{} ->
nonResult = "abc"
x = try nonResult
Ok (x * 2)
invalidTry {}
"#
),
@r"
INVALID TRY TARGET in /code/proj/Main.roc
This expression cannot be used as a `try` target:
6 x = try nonResult
^^^^^^^^^
I expected a Result, but it actually has type:
Str
Hint: Did you forget to wrap the value with an `Ok` or an `Err` tag?
"
);
test_report!(
question_try_with_non_result_target,
indoc!(
r#"
invalidTry = \{} ->
nonResult = "abc"
x = nonResult?
Ok (x * 2)
invalidTry {}
"#
),
@r"
INVALID TRY TARGET in /code/proj/Main.roc
This expression cannot be tried with the `?` operator:
6 x = nonResult?
^^^^^^^^^^
I expected a Result, but it actually has type:
Str
Hint: Did you forget to wrap the value with an `Ok` or an `Err` tag?
"
);
test_report!(
incompatible_try_errs,
indoc!(
r#"
incompatibleTrys = \{} ->
x = try Err 123
y = try Err "abc"
Ok (x + y)
incompatibleTrys {}
"#
),
@r#"
TYPE MISMATCH in /code/proj/Main.roc
This returns something that's incompatible with the return type of the
enclosing function:
5 x = try Err 123
6
7> y = try Err "abc"
8
9 Ok (x + y)
This returns an `Err` of type:
[Err Str, ]
But I expected the function to have return type:
[Err (Num *), ]a
"#
);
test_report!(
keyword_try_prefix_in_pipe,
indoc!(
r#"
readFile : Str -> Str
getFileContents : Str -> Result Str _
getFileContents = \filePath ->
contents =
readFile filePath
|> try Result.mapErr ErrWrapper
contents
getFileContents "file.txt"
"#
),
@r"
TYPE MISMATCH in /code/proj/Main.roc
This 1st argument to this function has an unexpected type:
9> readFile filePath
10 |> try Result.mapErr ErrWrapper
This `readFile` call produces:
Str
But this function needs its 1st argument to be:
Result ok a
"
);
test_report!(
keyword_try_suffix_in_pipe,
indoc!(
r#"
readFile : Str -> Str
getFileContents : Str -> Result Str _
getFileContents = \filePath ->
contents =
readFile filePath
|> Result.mapErr ErrWrapper
|> try
contents
getFileContents "file.txt"
"#
),
@r"
TYPE MISMATCH in /code/proj/Main.roc
This 1st argument to |> has an unexpected type:
9> readFile filePath
10 |> Result.mapErr ErrWrapper
This `readFile` call produces:
Str
But |> needs its 1st argument to be:
Result ok a
"
);
test_report!(
question_try_in_pipe,
indoc!(
r#"
readFile : Str -> Str
getFileContents : Str -> Result Str _
getFileContents = \filePath ->
contents =
readFile filePath
|> Result.mapErr? ErrWrapper
contents
getFileContents "file.txt"
"#
),
@r"
TYPE MISMATCH in /code/proj/Main.roc
This 1st argument to this function has an unexpected type:
9> readFile filePath
10 |> Result.mapErr? ErrWrapper
This `readFile` call produces:
Str
But this function needs its 1st argument to be:
Result ok a
"
);
test_report!(
leftover_statement,
indoc!(

View file

@ -373,6 +373,17 @@ impl<'a> LowerParams<'a> {
expr_stack.push(&mut loc_message.value);
expr_stack.push(&mut loc_continuation.value);
}
Try {
result_expr,
result_var: _,
return_var: _,
ok_payload_var: _,
err_payload_var: _,
err_ext_var: _,
kind: _,
} => {
expr_stack.push(&mut result_expr.value);
}
Return {
return_value,
return_var: _,

View file

@ -104,7 +104,8 @@ pub fn remove_module_param_arguments(
| TypeError::FxInTopLevel(_, _)
| TypeError::ExpectedEffectful(_, _)
| TypeError::UnsuffixedEffectfulFunction(_, _)
| TypeError::SuffixedPureFunction(_, _) => {}
| TypeError::SuffixedPureFunction(_, _)
| TypeError::InvalidTryTarget(_, _, _) => {}
}
}
}
@ -188,7 +189,8 @@ fn remove_for_reason(
| Reason::CrashArg
| Reason::ImportParams(_)
| Reason::Stmt(_)
| Reason::FunctionOutput => {}
| Reason::FunctionOutput
| Reason::TryResult => {}
}
}

View file

@ -11,7 +11,7 @@ use crate::layout::{
use bumpalo::collections::{CollectIn, Vec};
use bumpalo::Bump;
use roc_can::abilities::SpecializationId;
use roc_can::expr::{AnnotatedMark, ClosureData, ExpectLookup};
use roc_can::expr::{AnnotatedMark, ClosureData, ExpectLookup, WhenBranch, WhenBranchPattern};
use roc_can::module::ExposedByModule;
use roc_collections::all::{default_hasher, BumpMap, BumpMapDefault, MutMap};
use roc_collections::VecMap;
@ -2930,7 +2930,7 @@ fn pattern_to_when_help(
body: Loc<roc_can::expr::Expr>,
symbol: Symbol,
) -> (Symbol, Loc<roc_can::expr::Expr>) {
use roc_can::expr::{Expr, WhenBranch, WhenBranchPattern};
use roc_can::expr::Expr;
let wrapped_body = Expr::When {
cond_var: pattern_var,
@ -5871,6 +5871,86 @@ pub fn with_hole<'a>(
}
}
}
Try {
result_expr,
result_var,
return_var,
ok_payload_var,
err_payload_var,
err_ext_var,
kind: _,
} => {
let ok_symbol = env.unique_symbol();
let err_symbol = env.unique_symbol();
let ok_branch = WhenBranch {
patterns: vec![WhenBranchPattern {
pattern: Loc::at_zero(roc_can::pattern::Pattern::AppliedTag {
whole_var: result_var,
ext_var: Variable::EMPTY_TAG_UNION,
tag_name: "Ok".into(),
arguments: vec![(
ok_payload_var,
Loc::at_zero(roc_can::pattern::Pattern::Identifier(ok_symbol)),
)],
}),
degenerate: false,
}],
value: Loc::at_zero(Var(ok_symbol, ok_payload_var)),
guard: None,
redundant: RedundantMark::known_non_redundant(),
};
let err_branch = WhenBranch {
patterns: vec![WhenBranchPattern {
pattern: Loc::at_zero(roc_can::pattern::Pattern::AppliedTag {
whole_var: result_var,
ext_var: err_ext_var,
tag_name: "Err".into(),
arguments: vec![(
err_payload_var,
Loc::at_zero(roc_can::pattern::Pattern::Identifier(err_symbol)),
)],
}),
degenerate: false,
}],
value: Loc::at_zero(Return {
return_var,
return_value: Box::new(Loc::at_zero(Tag {
tag_union_var: return_var,
ext_var: err_ext_var,
name: "Err".into(),
arguments: vec![(
err_payload_var,
Loc::at_zero(Var(err_symbol, err_payload_var)),
)],
})),
}),
guard: None,
redundant: RedundantMark::known_non_redundant(),
};
let result_region = result_expr.region;
let when_expr = When {
loc_cond: result_expr,
cond_var: result_var,
expr_var: ok_payload_var,
region: result_region,
branches: vec![ok_branch, err_branch],
branches_cond_var: result_var,
exhaustive: ExhaustiveMark::known_exhaustive(),
};
with_hole(
env,
when_expr,
variable,
procs,
layout_cache,
assigned,
hole,
)
}
Return {
return_value,
return_var,

View file

@ -28,12 +28,26 @@ pub struct Spaces<'a, T> {
pub after: &'a [CommentOrNewline<'a>],
}
impl<'a, T: Copy> ExtractSpaces<'a> for Spaces<'a, T> {
type Item = T;
fn extract_spaces(&self) -> Spaces<'a, T> {
*self
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct SpacesBefore<'a, T> {
pub before: &'a [CommentOrNewline<'a>],
pub item: T,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct SpacesAfter<'a, T> {
pub after: &'a [CommentOrNewline<'a>],
pub item: T,
}
#[derive(Copy, Clone, PartialEq)]
pub enum Spaced<'a, T> {
Item(T),
@ -401,6 +415,12 @@ pub enum TryTarget {
Result,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ResultTryKind {
KeywordPrefix,
OperatorSuffix,
}
/// A parsed expression. This uses lifetimes extensively for two reasons:
///
/// 1. It uses Bump::alloc for all allocations, which returns a reference.
@ -491,13 +511,19 @@ pub enum Expr<'a> {
Backpassing(&'a [Loc<Pattern<'a>>], &'a Loc<Expr<'a>>, &'a Loc<Expr<'a>>),
Dbg,
DbgStmt(&'a Loc<Expr<'a>>, &'a Loc<Expr<'a>>),
// This form of debug is a desugared call to roc_dbg
LowLevelDbg(&'a (&'a str, &'a str), &'a Loc<Expr<'a>>, &'a Loc<Expr<'a>>),
DbgStmt {
first: &'a Loc<Expr<'a>>,
extra_args: &'a [&'a Loc<Expr<'a>>],
continuation: &'a Loc<Expr<'a>>,
},
/// The `try` keyword that performs early return on errors
Try,
// This form of try is a desugared Result unwrapper
LowLevelTry(&'a Loc<Expr<'a>>, ResultTryKind),
// This form of debug is a desugared call to roc_dbg
LowLevelDbg(&'a (&'a str, &'a str), &'a Loc<Expr<'a>>, &'a Loc<Expr<'a>>),
// Application
/// To apply by name, do Apply(Var(...), ...)
@ -671,9 +697,18 @@ pub fn is_expr_suffixed(expr: &Expr) -> bool {
Expr::OpaqueRef(_) => false,
Expr::Backpassing(_, _, _) => false, // TODO: we might want to check this?
Expr::Dbg => false,
Expr::DbgStmt(a, b) => is_expr_suffixed(&a.value) || is_expr_suffixed(&b.value),
Expr::DbgStmt {
first,
extra_args,
continuation,
} => {
is_expr_suffixed(&first.value)
|| extra_args.iter().any(|a| is_expr_suffixed(&a.value))
|| is_expr_suffixed(&continuation.value)
}
Expr::LowLevelDbg(_, a, b) => is_expr_suffixed(&a.value) || is_expr_suffixed(&b.value),
Expr::Try => false,
Expr::LowLevelTry(loc_expr, _) => is_expr_suffixed(&loc_expr.value),
Expr::UnaryOp(a, _) => is_expr_suffixed(&a.value),
Expr::When(cond, branches) => {
is_expr_suffixed(&cond.value) || branches.iter().any(|x| is_when_branch_suffixed(x))
@ -930,16 +965,26 @@ impl<'a, 'b> RecursiveValueDefIter<'a, 'b> {
expr_stack.push(&a.value);
expr_stack.push(&b.value);
}
DbgStmt(condition, cont) => {
DbgStmt {
first,
extra_args,
continuation,
} => {
expr_stack.reserve(2);
expr_stack.push(&condition.value);
expr_stack.push(&cont.value);
expr_stack.push(&first.value);
for arg in extra_args.iter() {
expr_stack.push(&arg.value);
}
expr_stack.push(&continuation.value);
}
LowLevelDbg(_, condition, cont) => {
expr_stack.reserve(2);
expr_stack.push(&condition.value);
expr_stack.push(&cont.value);
}
LowLevelTry(loc_expr, _) => {
expr_stack.push(&loc_expr.value);
}
Return(return_value, after_return) => {
if let Some(after_return) = after_return {
expr_stack.reserve(2);
@ -2311,6 +2356,7 @@ impl_extract_spaces!(Tag);
impl_extract_spaces!(AssignedField<T>);
impl_extract_spaces!(TypeAnnotation);
impl_extract_spaces!(ImplementsAbility);
impl_extract_spaces!(ImplementsAbilities);
impl<'a, T: Copy> ExtractSpaces<'a> for Spaced<'a, T> {
type Item = T;
@ -2476,9 +2522,10 @@ impl<'a> Malformed for Expr<'a> {
Defs(defs, body) => defs.is_malformed() || body.is_malformed(),
Backpassing(args, call, body) => args.iter().any(|arg| arg.is_malformed()) || call.is_malformed() || body.is_malformed(),
Dbg => false,
DbgStmt(condition, continuation) => condition.is_malformed() || continuation.is_malformed(),
DbgStmt { first, extra_args, continuation } => first.is_malformed() || extra_args.iter().any(|a| a.is_malformed()) || continuation.is_malformed(),
LowLevelDbg(_, condition, continuation) => condition.is_malformed() || continuation.is_malformed(),
Try => false,
LowLevelTry(loc_expr, _) => loc_expr.is_malformed(),
Return(return_value, after_return) => return_value.is_malformed() || after_return.is_some_and(|ar| ar.is_malformed()),
Apply(func, args, _) => func.is_malformed() || args.iter().any(|arg| arg.is_malformed()),
BinOps(firsts, last) => firsts.iter().any(|(expr, _)| expr.is_malformed()) || last.is_malformed(),

View file

@ -182,26 +182,25 @@ fn record_field_access_chain<'a>() -> impl Parser<'a, Vec<'a, Suffix<'a>>, EExpr
/// pattern later
fn loc_term_or_underscore_or_conditional<'a>(
options: ExprParseOptions,
allow_conditional: bool,
) -> impl Parser<'a, Loc<Expr<'a>>, EExpr<'a>> {
move |arena: &'a Bump, state: State<'a>, min_indent: u32| {
if allow_conditional {
match loc_conditional(options).parse(arena, state.clone(), min_indent) {
Ok((_, expr, state)) => return Ok((MadeProgress, expr, state)),
Err((MadeProgress, e)) => return Err((MadeProgress, e)),
Err((NoProgress, _)) => {}
}
}
loc_term_or_underscore(options).parse(arena, state, min_indent)
}
}
fn loc_conditional<'a>(options: ExprParseOptions) -> impl Parser<'a, Loc<Expr<'a>>, EExpr<'a>> {
one_of!(
loc_expr_in_parens_etc_help(),
loc(specialize_err(EExpr::If, if_expr_help(options))),
loc(specialize_err(EExpr::When, when::when_expr_help(options))),
loc(specialize_err(EExpr::Str, string_like_literal_help())),
loc(specialize_err(
EExpr::Number,
positive_number_literal_help()
)),
loc(specialize_err(EExpr::Closure, closure_help(options))),
loc(crash_kw()),
loc(specialize_err(EExpr::Dbg, dbg_kw())),
loc(try_kw()),
loc(underscore_expression()),
loc(record_literal_help()),
loc(specialize_err(EExpr::List, list_literal_help())),
ident_seq(),
)
.trace("term_or_underscore_or_conditional")
}
/// In some contexts we want to parse the `_` as an expression, so it can then be turned into a
@ -217,6 +216,7 @@ fn loc_term_or_underscore<'a>(
positive_number_literal_help()
)),
loc(specialize_err(EExpr::Closure, closure_help(options))),
loc(crash_kw()),
loc(specialize_err(EExpr::Dbg, dbg_kw())),
loc(try_kw()),
loc(underscore_expression()),
@ -294,12 +294,21 @@ fn crash_kw<'a>() -> impl Parser<'a, Expr<'a>, EExpr<'a>> {
fn loc_possibly_negative_or_negated_term<'a>(
options: ExprParseOptions,
allow_negate: bool,
allow_conditional: bool,
) -> impl Parser<'a, Loc<Expr<'a>>, EExpr<'a>> {
let parse_unary_negate = move |arena, state: State<'a>, min_indent: u32| {
let initial = state.clone();
let (_, (loc_op, loc_expr), state) =
and(loc(unary_negate()), loc_term(options)).parse(arena, state, min_indent)?;
if !allow_negate {
return Err((NoProgress, EExpr::UnaryNegate(state.pos())));
}
let (_, (loc_op, loc_expr), state) = and(
loc(unary_negate()),
loc_possibly_negative_or_negated_term(options, true, false),
)
.parse(arena, state, min_indent)?;
let loc_expr = numeric_negate_expression(arena, initial, loc_op, loc_expr, &[]);
@ -307,20 +316,26 @@ fn loc_possibly_negative_or_negated_term<'a>(
};
one_of![
parse_unary_negate,
parse_unary_negate.trace("negate_expr"),
// this will parse negative numbers, which the unary negate thing up top doesn't (for now)
loc(specialize_err(EExpr::Number, number_literal_help())),
// loc(specialize_err(EExpr::Number, number_literal_help())),
loc(map_with_arena(
and(
loc(byte(b'!', EExpr::Start)),
space0_before_e(loc_term(options), EExpr::IndentStart)
loc(unary_not()).trace("not"),
space0_before_e(
loc_possibly_negative_or_negated_term(options, true, false),
EExpr::IndentStart
)
.trace("not_expr")
),
|arena: &'a Bump, (loc_op, loc_expr): (Loc<_>, _)| {
Expr::UnaryOp(arena.alloc(loc_expr), Loc::at(loc_op.region, UnaryOp::Not))
}
)),
loc_term_or_underscore_or_conditional(options)
))
.trace("not_expr"),
loc_term_or_underscore_or_conditional(options, allow_conditional)
]
.trace("loc_possibly_negative_or_negated_term")
}
fn fail_expr_start_e<'a, T: 'a>() -> impl Parser<'a, T, EExpr<'a>> {
@ -328,18 +343,22 @@ fn fail_expr_start_e<'a, T: 'a>() -> impl Parser<'a, T, EExpr<'a>> {
}
fn unary_negate<'a>() -> impl Parser<'a, (), EExpr<'a>> {
move |_arena: &'a Bump, state: State<'a>, _min_indent: u32| {
move |_arena: &'a Bump, state: State<'a>, min_indent: u32| {
// a minus is unary iff
//
// - it is preceded by whitespace (spaces, newlines, comments)
// - it is not followed by whitespace
let followed_by_whitespace = state
// - it is not followed by >, making ->
let followed_by_illegal_char = state
.bytes()
.get(1)
.map(|c| c.is_ascii_whitespace() || *c == b'#')
.map(|c| c.is_ascii_whitespace() || *c == b'#' || *c == b'>')
.unwrap_or(false);
if state.bytes().starts_with(b"-") && !followed_by_whitespace {
if state.bytes().starts_with(b"-")
&& !followed_by_illegal_char
&& state.column() >= min_indent
{
// the negate is only unary if it is not followed by whitespace
let state = state.advance(1);
Ok((MadeProgress, (), state))
@ -350,6 +369,19 @@ fn unary_negate<'a>() -> impl Parser<'a, (), EExpr<'a>> {
}
}
fn unary_not<'a>() -> impl Parser<'a, (), EExpr<'a>> {
move |_arena: &'a Bump, state: State<'a>, min_indent: u32| {
let followed_by_equals = state.bytes().get(1).map(|c| *c == b'=').unwrap_or(false);
if state.bytes().starts_with(b"!") && !followed_by_equals && state.column() >= min_indent {
let state = state.advance(1);
Ok((MadeProgress, (), state))
} else {
Err((NoProgress, EExpr::UnaryNot(state.pos())))
}
}
}
/// Entry point for parsing an expression.
fn expr_start<'a>(options: ExprParseOptions) -> impl Parser<'a, Loc<Expr<'a>>, EExpr<'a>> {
one_of![
@ -378,10 +410,11 @@ fn parse_expr_operator_chain<'a>(
) -> Result<(Progress, Expr<'a>, State<'a>), (Progress, EExpr<'a>)> {
let line_indent = state.line_indent();
let (_, expr, state) =
loc_possibly_negative_or_negated_term(options).parse(arena, state, min_indent)?;
let (_, expr, state) = loc_possibly_negative_or_negated_term(options, true, true)
.parse(arena, state, min_indent)?;
let mut initial_state = state.clone();
let mut end = state.pos();
let (spaces_before_op, state) =
match space0_e(EExpr::IndentEnd).parse(arena, state.clone(), min_indent) {
@ -402,10 +435,13 @@ fn parse_expr_operator_chain<'a>(
let call_min_indent = line_indent + 1;
loop {
let allow_negate = state.pos() > end;
let parser = skip_first(
crate::blankspace::check_indent(EExpr::IndentEnd),
loc_term_or_underscore(options),
);
loc_possibly_negative_or_negated_term(options, allow_negate, false),
)
.trace("term_or_underscore");
end = state.pos();
match parser.parse(arena, state.clone(), call_min_indent) {
Err((MadeProgress, f)) => return Err((MadeProgress, f)),
Err((NoProgress, _)) => {
@ -459,7 +495,7 @@ fn parse_expr_after_apply<'a>(
before_op: State<'a>,
initial_state: State<'a>,
) -> Result<(Progress, Expr<'a>, State<'a>), (Progress, EExpr<'a>)> {
match loc(bin_op(check_for_defs)).parse(arena, state.clone(), min_indent) {
match loc(bin_op(check_for_defs)).parse(arena, state.clone(), call_min_indent) {
Err((MadeProgress, f)) => Err((MadeProgress, f)),
Ok((_, loc_op, state)) => {
expr_state.consume_spaces(arena);
@ -572,11 +608,11 @@ fn parse_stmt_operator_chain<'a>(
) -> Result<(Progress, Stmt<'a>, State<'a>), (Progress, EExpr<'a>)> {
let line_indent = state.line_indent();
let (_, expr, state) =
loc_possibly_negative_or_negated_term(options).parse(arena, state, min_indent)?;
let (_, expr, state) = loc_possibly_negative_or_negated_term(options, true, true)
.parse(arena, state, min_indent)?;
let mut initial_state = state.clone();
let end = state.pos();
let mut end = state.pos();
let (spaces_before_op, state) =
match space0_e(EExpr::IndentEnd).parse(arena, state.clone(), min_indent) {
@ -597,10 +633,12 @@ fn parse_stmt_operator_chain<'a>(
let call_min_indent = line_indent + 1;
loop {
let allow_negate = state.pos() > end;
let parser = skip_first(
crate::blankspace::check_indent(EExpr::IndentEnd),
loc_term_or_underscore(options),
loc_possibly_negative_or_negated_term(options, allow_negate, false),
);
end = state.pos();
match parser.parse(arena, state.clone(), call_min_indent) {
Err((MadeProgress, f)) => return Err((MadeProgress, f)),
Ok((
@ -845,13 +883,13 @@ fn numeric_negate_expression<'a, T>(
let region = Region::new(start, expr.region.end());
let new_expr = match expr.value {
Expr::Num(string) => {
Expr::Num(string) if !string.starts_with('-') => {
let new_string =
unsafe { std::str::from_utf8_unchecked(&state.bytes()[..string.len() + 1]) };
Expr::Num(new_string)
}
Expr::Float(string) => {
Expr::Float(string) if !string.starts_with('-') => {
let new_string =
unsafe { std::str::from_utf8_unchecked(&state.bytes()[..string.len() + 1]) };
@ -860,11 +898,11 @@ fn numeric_negate_expression<'a, T>(
Expr::NonBase10Int {
string,
base,
is_negative,
is_negative: false,
} => {
// don't include the minus sign here; it will not be parsed right
Expr::NonBase10Int {
is_negative: !is_negative,
is_negative: true,
string,
base,
}
@ -1151,9 +1189,7 @@ fn parse_stmt_alias_or_opaque<'a>(
AliasOrOpaque::Alias => {
let (_, signature, state) = alias_signature().parse(arena, state, min_indent)?;
// TODO: this code used to be broken and it dropped the spaces after the operator.
// The formatter is not expecting this, so let's keep it as is for now.
// let signature = signature.map(|v| v.maybe_before(arena, spaces_after_operator));
let signature = signature.map(|v| v.maybe_before(arena, spaces_after_operator));
let header = TypeHeader {
name: Loc::at(expr.region, name),
@ -1172,9 +1208,7 @@ fn parse_stmt_alias_or_opaque<'a>(
let (_, (signature, derived), state) =
opaque_signature().parse(arena, state, indented_more)?;
// TODO: this code used to be broken and it dropped the spaces after the operator.
// The formatter is not expecting this, so let's keep it as is for now.
// let signature = signature.map(|v| v.maybe_before(arena, spaces_after_operator));
let signature = signature.map(|v| v.maybe_before(arena, spaces_after_operator));
let header = TypeHeader {
name: Loc::at(expr.region, name),
@ -1560,10 +1594,10 @@ fn parse_after_binop<'a>(
mut expr_state: ExprState<'a>,
loc_op: Loc<BinOp>,
) -> ParseResult<'a, Expr<'a>, EExpr<'a>> {
match loc_possibly_negative_or_negated_term(options).parse(
match loc_possibly_negative_or_negated_term(options, true, true).parse(
arena,
state.clone(),
call_min_indent,
min_indent,
) {
Err((MadeProgress, f)) => Err((MadeProgress, f)),
Ok((_, mut new_expr, state)) => {
@ -1855,7 +1889,7 @@ fn parse_expr_end<'a>(
Err((NoProgress, _)) => {
let before_op = state.clone();
// try an operator
match loc(bin_op(check_for_defs)).parse(arena, state.clone(), min_indent) {
match loc(bin_op(check_for_defs)).parse(arena, state.clone(), call_min_indent) {
Err((MadeProgress, f)) => Err((MadeProgress, f)),
Ok((_, loc_op, state)) => {
expr_state.consume_spaces(arena);
@ -1899,7 +1933,7 @@ fn parse_stmt_after_apply<'a>(
initial_state: State<'a>,
) -> ParseResult<'a, Stmt<'a>, EExpr<'a>> {
let before_op = state.clone();
match loc(operator()).parse(arena, state.clone(), min_indent) {
match loc(operator()).parse(arena, state.clone(), call_min_indent) {
Err((MadeProgress, f)) => Err((MadeProgress, f)),
Ok((_, loc_op, state)) => {
expr_state.consume_spaces(arena);
@ -2172,8 +2206,9 @@ fn expr_to_pattern_help<'a>(arena: &'a Bump, expr: &Expr<'a>) -> Result<Pattern<
| Expr::If { .. }
| Expr::When(_, _)
| Expr::Dbg
| Expr::DbgStmt(_, _)
| Expr::DbgStmt { .. }
| Expr::LowLevelDbg(_, _, _)
| Expr::LowLevelTry(_, _)
| Expr::Return(_, _)
| Expr::MalformedSuffixed(..)
| Expr::PrecedenceConflict { .. }
@ -2589,7 +2624,8 @@ fn if_branch<'a>() -> impl Parser<'a, (Loc<Expr<'a>>, Loc<Expr<'a>>), EIf<'a>> {
specialize_err_ref(EIf::Condition, loc_expr(true)),
EIf::IndentCondition,
EIf::IndentThenToken,
),
)
.trace("if_condition"),
parser::keyword(keyword::THEN, EIf::Then),
),
map_with_arena(
@ -2607,6 +2643,7 @@ fn if_branch<'a>() -> impl Parser<'a, (Loc<Expr<'a>>, Loc<Expr<'a>>), EIf<'a>> {
),
parser::keyword(keyword::ELSE, EIf::Else),
)
.trace("if_branch")
}
fn expect_help<'a>(
@ -2686,18 +2723,19 @@ fn try_kw<'a>() -> impl Parser<'a, Expr<'a>, EExpr<'a>> {
fn import<'a>() -> impl Parser<'a, ValueDef<'a>, EImport<'a>> {
skip_second(
skip_first(
indented_seq_skip_first(
parser::keyword(keyword::IMPORT, EImport::Import),
increment_min_indent(one_of!(import_body(), import_ingested_file_body())),
one_of!(import_body(), import_ingested_file_body()),
),
require_newline_or_eof(EImport::EndNewline),
)
}
fn if_expr_help<'a>(options: ExprParseOptions) -> impl Parser<'a, Expr<'a>, EIf<'a>> {
move |arena: &'a Bump, state, min_indent| {
let (_, _, state) =
parser::keyword(keyword::IF, EIf::If).parse(arena, state, min_indent)?;
(move |arena: &'a Bump, state, min_indent| {
let (_, _, state) = parser::keyword(keyword::IF, EIf::If)
.trace("if_kw")
.parse(arena, state, min_indent)?;
let if_indent = state.line_indent();
@ -2706,8 +2744,9 @@ fn if_expr_help<'a>(options: ExprParseOptions) -> impl Parser<'a, Expr<'a>, EIf<
let mut loop_state = state;
let state_final_else = loop {
let (_, (cond, then_branch), state) =
if_branch().parse(arena, loop_state, min_indent)?;
let (_, (cond, then_branch), state) = if_branch()
.parse(arena, loop_state, min_indent)
.map_err(|(_p, err)| (MadeProgress, err))?;
branches.push((cond, then_branch));
@ -2727,8 +2766,12 @@ fn if_expr_help<'a>(options: ExprParseOptions) -> impl Parser<'a, Expr<'a>, EIf<
}
};
let has_newline_next = require_newline_or_eof(EExpr::IndentEnd)
.parse(arena, state_final_else.clone(), min_indent)
.is_ok();
let else_indent = state_final_else.line_indent();
let indented_else = else_indent > if_indent;
let indented_else = else_indent > if_indent && has_newline_next;
let min_indent = if !indented_else {
else_indent + 1
@ -2760,7 +2803,8 @@ fn if_expr_help<'a>(options: ExprParseOptions) -> impl Parser<'a, Expr<'a>, EIf<
};
Ok((MadeProgress, expr, state))
}
})
.trace("if")
}
/// Parse a block of statements (parser combinator version of `parse_block`)
@ -3086,16 +3130,13 @@ fn stmts_to_defs<'a>(
_,
) = e
{
if args.len() != 1 {
// TODO: this should be done in can, not parsing!
return Err(EExpr::Dbg(
EExpect::DbgArity(sp_stmt.item.region.start()),
sp_stmt.item.region.start(),
));
}
let condition = &args[0];
let rest = stmts_to_expr(&stmts[i + 1..], arena)?;
let e = Expr::DbgStmt(condition, arena.alloc(rest));
let e = Expr::DbgStmt {
first: condition,
extra_args: &args[1..],
continuation: arena.alloc(rest),
};
let e = if sp_stmt.before.is_empty() {
e
@ -3160,7 +3201,7 @@ fn stmts_to_defs<'a>(
)),
) = (td, stmts.get(i + 1).map(|s| (s.before, s.item.value)))
{
if spaces_middle.len() <= 1
if (spaces_middle.len() <= 1 && !ends_with_spaces_conservative(&ann_type.value))
|| header
.vars
.first()
@ -3211,7 +3252,11 @@ fn stmts_to_defs<'a>(
if exprify_dbg {
let e = if i + 1 < stmts.len() {
let rest = stmts_to_expr(&stmts[i + 1..], arena)?;
Expr::DbgStmt(arena.alloc(condition), arena.alloc(rest))
Expr::DbgStmt {
first: arena.alloc(condition),
extra_args: &[],
continuation: arena.alloc(rest),
}
} else {
Expr::Apply(
arena.alloc(Loc {
@ -3244,7 +3289,8 @@ fn stmts_to_defs<'a>(
)),
) = (vd, stmts.get(i + 1).map(|s| (s.before, s.item.value)))
{
if spaces_middle.len() <= 1 || ann_pattern.value.equivalent(&loc_pattern.value)
if (spaces_middle.len() <= 1 && !ends_with_spaces_conservative(&ann_type.value))
|| ann_pattern.value.equivalent(&loc_pattern.value)
{
let region = Region::span_across(&loc_pattern.region, &loc_def_expr.region);
@ -3277,6 +3323,64 @@ fn stmts_to_defs<'a>(
Ok((defs, last_expr))
}
fn ends_with_spaces_conservative(ty: &TypeAnnotation<'_>) -> bool {
match ty {
TypeAnnotation::Function(_, _, res) => ends_with_spaces_conservative(&res.value),
TypeAnnotation::Apply(_, _, args) => args
.last()
.map_or(false, |a| ends_with_spaces_conservative(&a.value)),
TypeAnnotation::As(_, _, type_header) => type_header
.vars
.last()
.map_or(false, |v| pat_ends_with_spaces_conservative(&v.value)),
TypeAnnotation::Record { fields: _, ext }
| TypeAnnotation::Tuple { elems: _, ext }
| TypeAnnotation::TagUnion { ext, tags: _ } => {
ext.map_or(false, |e| ends_with_spaces_conservative(&e.value))
}
TypeAnnotation::BoundVariable(_) | TypeAnnotation::Inferred | TypeAnnotation::Wildcard => {
false
}
TypeAnnotation::Where(_, clauses) => clauses.last().map_or(false, |c| {
c.value
.abilities
.last()
.map_or(false, |a| ends_with_spaces_conservative(&a.value))
}),
TypeAnnotation::SpaceBefore(inner, _) => ends_with_spaces_conservative(inner),
TypeAnnotation::SpaceAfter(_, _) => true,
TypeAnnotation::Malformed(_) => false,
}
}
fn pat_ends_with_spaces_conservative(pat: &Pattern<'_>) -> bool {
match pat {
Pattern::Identifier { .. }
| Pattern::QualifiedIdentifier { .. }
| Pattern::Tag(_)
| Pattern::NumLiteral(_)
| Pattern::FloatLiteral(_)
| Pattern::StrLiteral(_)
| Pattern::Underscore(_)
| Pattern::SingleQuote(_)
| Pattern::Tuple(_)
| Pattern::List(_)
| Pattern::NonBase10Literal { .. }
| Pattern::ListRest(_)
| Pattern::As(_, _)
| Pattern::OpaqueRef(_) => false,
Pattern::Apply(_, args) => args
.last()
.map_or(false, |a| pat_ends_with_spaces_conservative(&a.value)),
Pattern::RecordDestructure(_) => false,
Pattern::RequiredField(_, _) => unreachable!(),
Pattern::OptionalField(_, _) => unreachable!(),
Pattern::SpaceBefore(inner, _) => pat_ends_with_spaces_conservative(inner),
Pattern::SpaceAfter(_, _) => true,
Pattern::Malformed(_) | Pattern::MalformedIdent(_, _) => false,
}
}
/// Given a type alias and a value definition, join them into a AnnotatedBody
pub fn join_alias_to_body<'a>(
arena: &'a Bump,
@ -3753,26 +3857,6 @@ fn positive_number_literal_help<'a>() -> impl Parser<'a, Expr<'a>, ENumber> {
)
}
fn number_literal_help<'a>() -> impl Parser<'a, Expr<'a>, ENumber> {
map(crate::number_literal::number_literal(), |literal| {
use crate::number_literal::NumLiteral::*;
match literal {
Num(s) => Expr::Num(s),
Float(s) => Expr::Float(s),
NonBase10Int {
string,
base,
is_negative,
} => Expr::NonBase10Int {
string,
base,
is_negative,
},
}
})
}
const BINOP_CHAR_SET: &[u8] = b"+-/*=.<>:&|^?%!";
const BINOP_CHAR_MASK: [bool; 125] = {
@ -3799,9 +3883,9 @@ enum OperatorOrDef {
}
fn bin_op<'a>(check_for_defs: bool) -> impl Parser<'a, BinOp, EExpr<'a>> {
move |_, state: State<'a>, _m| {
move |_, state: State<'a>, min_indent| {
let start = state.pos();
let (_, op, state) = operator_help(EExpr::Start, EExpr::BadOperator, state)?;
let (_, op, state) = operator_help(EExpr::Start, EExpr::BadOperator, state, min_indent)?;
let err_progress = if check_for_defs {
MadeProgress
} else {
@ -3822,7 +3906,8 @@ fn bin_op<'a>(check_for_defs: bool) -> impl Parser<'a, BinOp, EExpr<'a>> {
}
fn operator<'a>() -> impl Parser<'a, OperatorOrDef, EExpr<'a>> {
(move |_, state, _m| operator_help(EExpr::Start, EExpr::BadOperator, state)).trace("operator")
(move |_, state, min_indent| operator_help(EExpr::Start, EExpr::BadOperator, state, min_indent))
.trace("operator")
}
#[inline(always)]
@ -3830,6 +3915,7 @@ fn operator_help<'a, F, G, E>(
to_expectation: F,
to_error: G,
mut state: State<'a>,
min_indent: u32,
) -> ParseResult<'a, OperatorOrDef, E>
where
F: Fn(Position) -> E,
@ -3855,7 +3941,21 @@ where
match chomped {
"" => Err((NoProgress, to_expectation(state.pos()))),
"+" => good!(OperatorOrDef::BinOp(BinOp::Plus), 1),
"-" => good!(OperatorOrDef::BinOp(BinOp::Minus), 1),
"-" => {
// A unary minus must only match if we are at the correct indent level; indent level doesn't
// matter for the rest of the operators.
// Note that a unary minus is distinguished by not having a space after it
let has_whitespace = matches!(
state.bytes().get(1),
Some(b' ' | b'#' | b'\n' | b'\r' | b'\t') | None
);
if !has_whitespace && state.column() < min_indent {
return Err((NoProgress, to_expectation(state.pos())));
}
good!(OperatorOrDef::BinOp(BinOp::Minus), 1)
}
"*" => good!(OperatorOrDef::BinOp(BinOp::Star), 1),
"/" => good!(OperatorOrDef::BinOp(BinOp::Slash), 1),
"%" => good!(OperatorOrDef::BinOp(BinOp::Percent), 1),
@ -3883,6 +3983,10 @@ where
}
"<-" => good!(OperatorOrDef::Backpassing, 2),
"!" => Err((NoProgress, to_error("!", state.pos()))),
"&" => {
// makes no progress, so it does not interfere with record updaters / `&foo`
Err((NoProgress, to_error("&", state.pos())))
}
_ => bad_made_progress!(chomped),
}
}

View file

@ -231,6 +231,7 @@ pub enum BadIdent {
UnderscoreAlone(Position),
UnderscoreInMiddle(Position),
TooManyUnderscores(Position),
UnderscoreAtStart {
position: Position,
/// If this variable was already declared in a pattern (e.g. \_x -> _x),
@ -252,11 +253,21 @@ fn is_alnum(ch: char) -> bool {
}
fn chomp_lowercase_part(buffer: &[u8]) -> Result<&str, Progress> {
chomp_part(char::is_lowercase, is_alnum, true, buffer)
chomp_part(
char::is_lowercase,
is_plausible_ident_continue,
true,
buffer,
)
}
fn chomp_uppercase_part(buffer: &[u8]) -> Result<&str, Progress> {
chomp_part(char::is_uppercase, is_alnum, false, buffer)
chomp_part(
char::is_uppercase,
is_plausible_ident_continue,
false,
buffer,
)
}
fn chomp_anycase_part(buffer: &[u8]) -> Result<&str, Progress> {
@ -265,7 +276,12 @@ fn chomp_anycase_part(buffer: &[u8]) -> Result<&str, Progress> {
let allow_bang =
char::from_utf8_slice_start(buffer).map_or(false, |(leading, _)| leading.is_lowercase());
chomp_part(char::is_alphabetic, is_alnum, allow_bang, buffer)
chomp_part(
char::is_alphabetic,
is_plausible_ident_continue,
allow_bang,
buffer,
)
}
fn chomp_integer_part(buffer: &[u8]) -> Result<&str, Progress> {
@ -429,7 +445,14 @@ fn chomp_opaque_ref(buffer: &[u8], pos: Position) -> Result<&str, BadIdent> {
Err(bad_ident(pos.bump_column(width as u32)))
} else {
let value = unsafe { std::str::from_utf8_unchecked(&buffer[..width]) };
Ok(value)
if value.contains('_') {
// we don't allow underscores in the middle of a type identifier
// but still parse them (and generate a malformed identifier)
// to give good error messages for this case
Err(BadIdent::UnderscoreInMiddle(pos.bump_column(width as u32)))
} else {
Ok(value)
}
}
}
Err(_) => Err(bad_ident(pos.bump_column(1))),
@ -486,7 +509,7 @@ fn chomp_identifier_chain<'a>(
}
while let Ok((ch, width)) = char::from_utf8_slice_start(&buffer[chomped..]) {
if ch.is_alphabetic() || ch.is_ascii_digit() {
if ch.is_alphabetic() || ch.is_ascii_digit() || ch == '_' {
chomped += width;
} else if ch == '!' && !first_is_uppercase {
chomped += width;
@ -556,19 +579,20 @@ fn chomp_identifier_chain<'a>(
BadIdent::WeirdDotAccess(pos.bump_column(chomped as u32 + width)),
)),
}
} else if let Ok(('_', _)) = char::from_utf8_slice_start(&buffer[chomped..]) {
// we don't allow underscores in the middle of an identifier
// but still parse them (and generate a malformed identifier)
// to give good error messages for this case
Err((
chomped as u32 + 1,
BadIdent::UnderscoreInMiddle(pos.bump_column(chomped as u32 + 1)),
))
} else if first_is_uppercase {
// just one segment, starting with an uppercase letter; that's a tag
let value = unsafe { std::str::from_utf8_unchecked(&buffer[..chomped]) };
Ok((chomped as u32, Ident::Tag(value)))
if value.contains('_') {
// we don't allow underscores in the middle of a tag identifier
// but still parse them (and generate a malformed identifier)
// to give good error messages for this case
Err((
chomped as u32,
BadIdent::UnderscoreInMiddle(pos.bump_column(chomped as u32)),
))
} else {
Ok((chomped as u32, Ident::Tag(value)))
}
} else {
// just one segment, starting with a lowercase letter; that's a normal identifier
let value = unsafe { std::str::from_utf8_unchecked(&buffer[..chomped]) };
@ -689,3 +713,121 @@ fn chomp_access_chain<'a>(buffer: &'a [u8], parts: &mut Vec<'a, Accessor<'a>>) -
Ok(chomped as u32)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn assert_ident_parses<'a>(arena: &'a Bump, ident: &str, expected: Ident<'a>) {
let s = State::new(ident.as_bytes());
let (_, id, _) = parse_ident(arena, s, 0).unwrap();
assert_eq!(id, expected);
}
fn assert_ident_parses_tag(arena: &Bump, ident: &str) {
assert_ident_parses(arena, ident, Ident::Tag(ident));
}
fn assert_ident_parses_opaque(arena: &Bump, ident: &str) {
assert_ident_parses(arena, ident, Ident::OpaqueRef(ident));
}
fn assert_ident_parses_simple_access(arena: &Bump, ident: &str) {
assert_ident_parses(
arena,
ident,
Ident::Access {
module_name: "",
parts: arena.alloc([Accessor::RecordField(ident)]),
},
);
}
fn assert_ident_parses_malformed(arena: &Bump, ident: &str, pos: Position) {
assert_ident_parses(
arena,
ident,
Ident::Malformed(ident, BadIdent::UnderscoreInMiddle(pos)),
);
}
#[test]
fn test_parse_ident_lowercase_camel() {
let arena = Bump::new();
assert_ident_parses_simple_access(&arena, "hello");
assert_ident_parses_simple_access(&arena, "hello23");
assert_ident_parses_simple_access(&arena, "helloWorld");
assert_ident_parses_simple_access(&arena, "helloWorld23");
assert_ident_parses_simple_access(&arena, "helloWorldThisIsQuiteATag");
assert_ident_parses_simple_access(&arena, "helloWorldThisIsQuiteATag_");
assert_ident_parses_simple_access(&arena, "helloworldthisisquiteatag_");
assert_ident_parses_simple_access(&arena, "helloWorldThisIsQuiteATag23");
assert_ident_parses_simple_access(&arena, "helloWorldThisIsQuiteATag23_");
assert_ident_parses_simple_access(&arena, "helloworldthisisquiteatag23_");
}
#[test]
fn test_parse_ident_lowercase_snake() {
let arena = Bump::new();
assert_ident_parses_simple_access(&arena, "hello_world");
assert_ident_parses_simple_access(&arena, "hello_world23");
assert_ident_parses_simple_access(&arena, "hello_world_this_is_quite_a_var");
assert_ident_parses_simple_access(&arena, "hello_world_this_is_quite_a_var_");
assert_ident_parses_simple_access(&arena, "hello_world_this_is_quite_a_var23");
assert_ident_parses_simple_access(&arena, "hello_world_this_is_quite_a_var23_");
}
#[test]
fn test_parse_tag_camel() {
let arena = Bump::new();
assert_ident_parses_tag(&arena, "Hello");
assert_ident_parses_tag(&arena, "Hello23");
assert_ident_parses_tag(&arena, "HelloWorld");
assert_ident_parses_tag(&arena, "HelloWorld23");
assert_ident_parses_tag(&arena, "HelloWorldThisIsQuiteATag");
assert_ident_parses_tag(&arena, "HelloWorldThisIsQuiteATag23");
}
#[test]
fn test_parse_tag_snake_is_malformed() {
let arena = Bump::new();
assert_ident_parses_malformed(&arena, "Hello_World", Position { offset: 11 });
assert_ident_parses_malformed(&arena, "Hello_World23", Position { offset: 13 });
assert_ident_parses_malformed(
&arena,
"Hello_World_This_Is_Quite_A_Tag",
Position { offset: 31 },
);
assert_ident_parses_malformed(
&arena,
"Hello_World_This_Is_Quite_A_Tag23",
Position { offset: 33 },
);
}
#[test]
fn test_parse_opaque_ref_camel() {
let arena = Bump::new();
assert_ident_parses_opaque(&arena, "@Hello");
assert_ident_parses_opaque(&arena, "@Hello23");
assert_ident_parses_opaque(&arena, "@HelloWorld");
assert_ident_parses_opaque(&arena, "@HelloWorld23");
assert_ident_parses_opaque(&arena, "@HelloWorldThisIsQuiteARef");
assert_ident_parses_opaque(&arena, "@HelloWorldThisIsQuiteARef23");
}
#[test]
fn test_parse_opaque_ref_snake_is_malformed() {
let arena = Bump::new();
assert_ident_parses_malformed(&arena, "@Hello_World", Position { offset: 12 });
assert_ident_parses_malformed(&arena, "@Hello_World23", Position { offset: 14 });
assert_ident_parses_malformed(
&arena,
"@Hello_World_This_Is_Quite_A_Ref",
Position { offset: 32 },
);
assert_ident_parses_malformed(
&arena,
"@Hello_World_This_Is_Quite_A_Ref23",
Position { offset: 34 },
);
}
}

View file

@ -396,10 +396,22 @@ impl<'a> Normalize<'a> for ValueDef<'a> {
match *self {
Annotation(a, b) => Annotation(a.normalize(arena), b.normalize(arena)),
Body(a, b) => Body(
arena.alloc(a.normalize(arena)),
arena.alloc(b.normalize(arena)),
),
Body(a, b) => {
let a = a.normalize(arena);
let b = b.normalize(arena);
let is_unit_assignment = if let Pattern::RecordDestructure(collection) = a.value {
collection.is_empty()
} else {
false
};
if is_unit_assignment {
Stmt(arena.alloc(b))
} else {
Body(arena.alloc(a), arena.alloc(b))
}
}
AnnotatedBody {
ann_pattern,
ann_type,
@ -560,26 +572,6 @@ impl<'a> Normalize<'a> for StrLiteral<'a> {
match *self {
StrLiteral::PlainLine(t) => StrLiteral::PlainLine(t),
StrLiteral::Line(t) => {
let mut needs_merge = false;
let mut last_was_mergable = false;
for segment in t.iter() {
let mergable = matches!(
segment,
StrSegment::Plaintext(_)
| StrSegment::Unicode(_)
| StrSegment::EscapedChar(_)
);
if mergable && last_was_mergable {
needs_merge = true;
break;
}
last_was_mergable = mergable;
}
if !needs_merge {
return StrLiteral::Line(t.normalize(arena));
}
let mut new_segments = Vec::new_in(arena);
let mut last_text = String::new_in(arena);
@ -713,39 +705,29 @@ impl<'a> Normalize<'a> for Expr<'a> {
arena.alloc(b.normalize(arena)),
),
Expr::Crash => Expr::Crash,
Expr::Defs(a, b) => {
let mut defs = a.clone();
defs.space_before = vec![Default::default(); defs.len()];
defs.space_after = vec![Default::default(); defs.len()];
defs.regions = vec![Region::zero(); defs.len()];
defs.spaces.clear();
for type_def in defs.type_defs.iter_mut() {
*type_def = type_def.normalize(arena);
}
for value_def in defs.value_defs.iter_mut() {
*value_def = value_def.normalize(arena);
}
Expr::Defs(arena.alloc(defs), arena.alloc(b.normalize(arena)))
}
Expr::Defs(a, b) => fold_defs(arena, a.defs(), b.value.normalize(arena)),
Expr::Backpassing(a, b, c) => Expr::Backpassing(
arena.alloc(a.normalize(arena)),
arena.alloc(b.normalize(arena)),
arena.alloc(c.normalize(arena)),
),
Expr::Dbg => Expr::Dbg,
Expr::DbgStmt(a, b) => Expr::DbgStmt(
arena.alloc(a.normalize(arena)),
arena.alloc(b.normalize(arena)),
),
Expr::DbgStmt {
first,
extra_args,
continuation,
} => Expr::DbgStmt {
first: arena.alloc(first.normalize(arena)),
extra_args: extra_args.normalize(arena),
continuation: arena.alloc(continuation.normalize(arena)),
},
Expr::LowLevelDbg(x, a, b) => Expr::LowLevelDbg(
x,
arena.alloc(a.normalize(arena)),
arena.alloc(b.normalize(arena)),
),
Expr::Try => Expr::Try,
Expr::LowLevelTry(a, kind) => Expr::LowLevelTry(arena.alloc(a.normalize(arena)), kind),
Expr::Return(a, b) => Expr::Return(
arena.alloc(a.normalize(arena)),
b.map(|loc_b| &*arena.alloc(loc_b.normalize(arena))),
@ -755,7 +737,22 @@ impl<'a> Normalize<'a> for Expr<'a> {
}
Expr::BinOps(a, b) => Expr::BinOps(a.normalize(arena), arena.alloc(b.normalize(arena))),
Expr::UnaryOp(a, b) => {
Expr::UnaryOp(arena.alloc(a.normalize(arena)), b.normalize(arena))
let a = a.normalize(arena);
match (a.value, b.value) {
(Expr::Num(text), UnaryOp::Negate) if !text.starts_with('-') => {
let mut res = String::new_in(arena);
res.push('-');
res.push_str(text);
Expr::Num(res.into_bump_str())
}
(Expr::Float(text), UnaryOp::Negate) if !text.starts_with('-') => {
let mut res = String::new_in(arena);
res.push('-');
res.push_str(text);
Expr::Float(res.into_bump_str())
}
_ => Expr::UnaryOp(arena.alloc(a), b.normalize(arena)),
}
}
Expr::If {
if_thens,
@ -776,7 +773,7 @@ impl<'a> Normalize<'a> for Expr<'a> {
Expr::PrecedenceConflict(a) => Expr::PrecedenceConflict(a),
Expr::SpaceBefore(a, _) => a.normalize(arena),
Expr::SpaceAfter(a, _) => a.normalize(arena),
Expr::SingleQuote(a) => Expr::Num(a),
Expr::SingleQuote(a) => Expr::SingleQuote(a),
Expr::EmptyRecordBuilder(a) => {
Expr::EmptyRecordBuilder(arena.alloc(a.normalize(arena)))
}
@ -791,6 +788,61 @@ impl<'a> Normalize<'a> for Expr<'a> {
}
}
fn fold_defs<'a>(
arena: &'a Bump,
mut defs: impl Iterator<Item = Result<&'a TypeDef<'a>, &'a ValueDef<'a>>>,
final_expr: Expr<'a>,
) -> Expr<'a> {
let mut new_defs = Defs::default();
while let Some(def) = defs.next() {
match def {
Ok(td) => {
let td = td.normalize(arena);
new_defs.push_type_def(td, Region::zero(), &[], &[]);
}
Err(vd) => {
let vd = vd.normalize(arena);
match vd {
ValueDef::Stmt(&Loc {
value:
Expr::Apply(
&Loc {
value: Expr::Dbg, ..
},
args,
_,
),
..
}) => {
let rest = fold_defs(arena, defs, final_expr);
let new_final = Expr::DbgStmt {
first: args[0],
extra_args: &args[1..],
continuation: arena.alloc(Loc::at_zero(rest)),
};
if new_defs.is_empty() {
return new_final;
}
return Expr::Defs(
arena.alloc(new_defs),
arena.alloc(Loc::at_zero(new_final)),
);
}
_ => {
new_defs.push_value_def(vd, Region::zero(), &[], &[]);
}
}
}
}
}
if new_defs.is_empty() {
return final_expr;
}
Expr::Defs(arena.alloc(new_defs), arena.alloc(Loc::at_zero(final_expr)))
}
fn remove_spaces_bad_ident(ident: BadIdent) -> BadIdent {
match ident {
BadIdent::Start(_) => BadIdent::Start(Position::zero()),
@ -804,6 +856,7 @@ fn remove_spaces_bad_ident(ident: BadIdent) -> BadIdent {
position: Position::zero(),
declaration_region,
},
BadIdent::TooManyUnderscores(_) => BadIdent::TooManyUnderscores(Position::zero()),
BadIdent::QualifiedTag(_) => BadIdent::QualifiedTag(Position::zero()),
BadIdent::WeirdAccessor(_) => BadIdent::WeirdAccessor(Position::zero()),
BadIdent::WeirdDotAccess(_) => BadIdent::WeirdDotAccess(Position::zero()),
@ -847,7 +900,7 @@ impl<'a> Normalize<'a> for Pattern<'a> {
is_negative,
},
Pattern::FloatLiteral(a) => Pattern::FloatLiteral(a),
Pattern::StrLiteral(a) => Pattern::StrLiteral(a),
Pattern::StrLiteral(a) => Pattern::StrLiteral(a.normalize(arena)),
Pattern::Underscore(a) => Pattern::Underscore(a),
Pattern::Malformed(a) => Pattern::Malformed(a),
Pattern::MalformedIdent(a, b) => Pattern::MalformedIdent(a, remove_spaces_bad_ident(b)),
@ -1465,7 +1518,6 @@ impl<'a> Normalize<'a> for EExpect<'a> {
EExpect::Continuation(arena.alloc(inner_err.normalize(arena)), Position::zero())
}
EExpect::IndentCondition(_) => EExpect::IndentCondition(Position::zero()),
EExpect::DbgArity(_) => EExpect::DbgArity(Position::zero()),
}
}
}

View file

@ -513,7 +513,6 @@ pub enum EExpect<'a> {
Condition(&'a EExpr<'a>, Position),
Continuation(&'a EExpr<'a>, Position),
IndentCondition(Position),
DbgArity(Position),
}
#[derive(Debug, Clone, PartialEq, Eq)]
@ -836,7 +835,7 @@ where
}
// This should be enough for anyone. Right? RIGHT?
let indent_text = "| ; : ! ".repeat(20);
let indent_text = "| ; : ! ".repeat(100);
let cur_indent = INDENT.with(|i| *i.borrow());
@ -1060,7 +1059,7 @@ where
Some(
b' ' | b'#' | b'\n' | b'\r' | b'\t' | b',' | b'(' | b')' | b'[' | b']' | b'{'
| b'}' | b'"' | b'\'' | b'/' | b'\\' | b'+' | b'*' | b'%' | b'^' | b'&' | b'|'
| b'<' | b'>' | b'=' | b'!' | b'~' | b'`' | b';' | b':' | b'?' | b'.',
| b'<' | b'>' | b'=' | b'!' | b'~' | b'`' | b';' | b':' | b'?' | b'.' | b'@' | b'-',
) => {
state = state.advance(width);
Ok((MadeProgress, (), state))
@ -1669,6 +1668,21 @@ where
}
}
/// Creates a parser that fails if the next byte is the given byte.
pub fn error_on_byte<'a, T, E, F>(byte_to_match: u8, to_error: F) -> impl Parser<'a, T, E>
where
T: 'a,
E: 'a,
F: Fn(Position) -> E,
{
debug_assert_ne!(byte_to_match, b'\n');
move |_arena: &'a Bump, state: State<'a>, _min_indent: u32| match state.bytes().first() {
Some(x) if *x == byte_to_match => Err((MadeProgress, to_error(state.pos()))),
_ => Err((NoProgress, to_error(state.pos()))),
}
}
/// Runs two parsers in succession. If both parsers succeed, the output is a tuple of both outputs.
/// Both parsers must have the same error type.
///

View file

@ -181,6 +181,7 @@ pub fn parse_str_like_literal<'a>() -> impl Parser<'a, StrLikeLiteral<'a>, EStri
match one_byte {
b'"' if !is_single_quote => {
preceded_by_dollar = false;
if segment_parsed_bytes == 1 && segments.is_empty() {
// special case of the empty string
if is_multiline {
@ -318,6 +319,7 @@ pub fn parse_str_like_literal<'a>() -> impl Parser<'a, StrLikeLiteral<'a>, EStri
));
}
b'\n' => {
preceded_by_dollar = false;
if is_multiline {
let without_newline = &state.bytes()[0..(segment_parsed_bytes - 1)];
let with_newline = &state.bytes()[0..segment_parsed_bytes];
@ -456,7 +458,8 @@ pub fn parse_str_like_literal<'a>() -> impl Parser<'a, StrLikeLiteral<'a>, EStri
let (_progress, loc_expr, new_state) = skip_second(
specialize_err_ref(
EString::Format,
loc(allocated(reset_min_indent(expr::expr_help()))),
loc(allocated(reset_min_indent(expr::expr_help())))
.trace("str_interpolation"),
),
byte(b')', EString::FormatEnd),
)

View file

@ -10,9 +10,9 @@ use crate::expr::record_field;
use crate::ident::{lowercase_ident, lowercase_ident_keyword_e};
use crate::keyword;
use crate::parser::{
absolute_column_min_indent, and, collection_trailing_sep_e, either, increment_min_indent,
indented_seq, loc, map, map_with_arena, skip_first, skip_second, succeed, then, zero_or_more,
ERecord, ETypeAbilityImpl,
absolute_column_min_indent, and, collection_trailing_sep_e, either, error_on_byte,
increment_min_indent, indented_seq, loc, map, map_with_arena, skip_first, skip_second, succeed,
then, zero_or_more, ERecord, ETypeAbilityImpl,
};
use crate::parser::{
allocated, backtrackable, byte, fail, optional, specialize_err, specialize_err_ref, two_bytes,
@ -97,7 +97,7 @@ fn check_type_alias<'a>(
fn parse_type_alias_after_as<'a>() -> impl Parser<'a, TypeHeader<'a>, EType<'a>> {
then(
space0_before_e(term(false), EType::TAsIndentStart),
space0_before_e(term_or_apply_with_as(false), EType::TAsIndentStart),
// TODO: introduce a better combinator for this.
// `check_type_alias` doesn't need to modify the state or progress, but it needs to access `state.pos()`
|arena, state, progress, output| {
@ -111,23 +111,42 @@ fn parse_type_alias_after_as<'a>() -> impl Parser<'a, TypeHeader<'a>, EType<'a>>
)
}
fn term_fragment<'a>(
stop_at_surface_has: bool,
) -> impl Parser<'a, Loc<TypeAnnotation<'a>>, EType<'a>> {
one_of!(
loc_wildcard(),
loc_inferred(),
specialize_err(EType::TInParens, loc_type_in_parens(stop_at_surface_has)),
loc(specialize_err(
EType::TRecord,
record_type(stop_at_surface_has)
)),
loc(specialize_err(
EType::TTagUnion,
tag_union_type(stop_at_surface_has)
)),
loc(parse_type_variable(stop_at_surface_has)),
)
}
fn term<'a>(stop_at_surface_has: bool) -> impl Parser<'a, Loc<TypeAnnotation<'a>>, EType<'a>> {
one_of!(
term_fragment(stop_at_surface_has),
loc(specialize_err(EType::TApply, concrete_type())),
fail(EType::TStart),
)
.trace("type_annotation:term")
}
fn term_or_apply_with_as<'a>(
stop_at_surface_has: bool,
) -> impl Parser<'a, Loc<TypeAnnotation<'a>>, EType<'a>> {
map_with_arena(
and(
one_of!(
loc_wildcard(),
loc_inferred(),
specialize_err(EType::TInParens, loc_type_in_parens(stop_at_surface_has)),
loc(specialize_err(
EType::TRecord,
record_type(stop_at_surface_has)
)),
loc(specialize_err(
EType::TTagUnion,
tag_union_type(stop_at_surface_has)
)),
term_fragment(stop_at_surface_has),
loc(applied_type(stop_at_surface_has)),
loc(parse_type_variable(stop_at_surface_has)),
fail(EType::TStart),
),
// Inline alias notation, e.g. [Nil, Cons a (List a)] as List a
@ -161,7 +180,7 @@ fn term<'a>(stop_at_surface_has: bool) -> impl Parser<'a, Loc<TypeAnnotation<'a>
}
},
)
.trace("type_annotation:term")
.trace("type_annotation:term_or_apply_with_as")
}
/// The `*` type variable, e.g. in (List *) Wildcard,
@ -579,42 +598,52 @@ fn expression<'a>(
stop_at_surface_has: bool,
) -> impl Parser<'a, Loc<TypeAnnotation<'a>>, EType<'a>> {
(move |arena, state: State<'a>, min_indent: u32| {
let (p1, first, state) = space0_before_e(term(stop_at_surface_has), EType::TIndentStart)
.parse(arena, state, min_indent)?;
let (p1, first, state) = space0_before_e(
term_or_apply_with_as(stop_at_surface_has),
EType::TIndentStart,
)
.parse(arena, state, min_indent)?;
let (p2, rest, rest_state) = zero_or_more(skip_first(
backtrackable(byte(b',', EType::TFunctionArgument)),
one_of![
map_with_arena(
and(
backtrackable(space0_e(EType::TIndentStart)),
and(
term_or_apply_with_as(stop_at_surface_has),
space0_e(EType::TIndentEnd)
),
),
comma_args_help,
),
error_on_byte(b',', EType::TFunctionArgument)
],
))
.trace("type_annotation:expression:rest_args")
.parse(arena, state.clone(), min_indent)?;
let result = and(
zero_or_more(skip_first(
byte(b',', EType::TFunctionArgument),
one_of![
space0_around_ee(
term(stop_at_surface_has),
EType::TIndentStart,
EType::TIndentEnd
),
fail(EType::TFunctionArgument)
],
))
.trace("type_annotation:expression:rest_args"),
and(
space0_e(EType::TIndentStart),
one_of![
map(two_bytes(b'-', b'>', EType::TStart), |_| {
FunctionArrow::Pure
}),
map(two_bytes(b'=', b'>', EType::TStart), |_| {
FunctionArrow::Effectful
}),
],
)
.trace("type_annotation:expression:arrow"),
space0_e(EType::TIndentStart),
one_of![
map(two_bytes(b'-', b'>', EType::TStart), |_| {
FunctionArrow::Pure
}),
map(two_bytes(b'=', b'>', EType::TStart), |_| {
FunctionArrow::Effectful
}),
],
)
.parse(arena, state.clone(), min_indent);
.trace("type_annotation:expression:arrow")
.parse(arena, rest_state, min_indent);
let (progress, annot, state) = match result {
Ok((p2, (rest, (space_before_arrow, arrow)), state)) => {
let (p3, return_type, state) =
space0_before_e(term(stop_at_surface_has), EType::TIndentStart)
.parse(arena, state, min_indent)?;
Ok((p3, (space_before_arrow, arrow), state)) => {
let (p4, return_type, state) = space0_before_e(
term_or_apply_with_as(stop_at_surface_has),
EType::TIndentStart,
)
.parse(arena, state, min_indent)?;
let region = Region::span_across(&first.region, &return_type.region);
@ -636,7 +665,7 @@ fn expression<'a>(
region,
value: TypeAnnotation::Function(output, arrow, arena.alloc(return_type)),
};
let progress = p1.or(p2).or(p3);
let progress = p1.or(p2).or(p3).or(p4);
(progress, result, state)
}
Err(err) => {
@ -694,6 +723,36 @@ fn expression<'a>(
.trace("type_annotation:expression")
}
fn comma_args_help<'a>(
arena: &'a Bump,
(spaces_before, (loc_val, spaces_after)): (
&'a [CommentOrNewline<'a>],
(Loc<TypeAnnotation<'a>>, &'a [CommentOrNewline<'a>]),
),
) -> Loc<TypeAnnotation<'a>> {
if spaces_before.is_empty() {
if spaces_after.is_empty() {
loc_val
} else {
arena
.alloc(loc_val.value)
.with_spaces_after(spaces_after, loc_val.region)
}
} else if spaces_after.is_empty() {
arena
.alloc(loc_val.value)
.with_spaces_before(spaces_before, loc_val.region)
} else {
let wrapped_expr = arena
.alloc(loc_val.value)
.with_spaces_after(spaces_after, loc_val.region);
arena
.alloc(wrapped_expr.value)
.with_spaces_before(spaces_before, wrapped_expr.region)
}
}
/// Parse a basic type annotation that's a combination of variables
/// (which are lowercase and unqualified, e.g. `a` in `List a`),
/// type applications (which are uppercase and optionally qualified, e.g.

View file

@ -372,10 +372,10 @@ mod test_parse {
let parsed = parse_module_defs(arena, state, ast::Defs::default());
match parsed {
Ok(_) => {
// dbg!(_state);
// eprintln!("{:?}", _state);
}
Err(_) => {
// dbg!(_fail, _state);
// eprintln!("{:?}, {:?}", _fail, _state);
panic!("Failed to parse!");
}
}

View file

@ -8,7 +8,7 @@ use roc_module::symbol::{ModuleId, Symbol};
use roc_parse::ast::Base;
use roc_parse::pattern::PatternType;
use roc_region::all::{Loc, Region};
use roc_types::types::AliasKind;
use roc_types::types::{AliasKind, EarlyReturnKind};
use crate::Severity;
@ -244,6 +244,7 @@ pub enum Problem {
},
ReturnOutsideOfFunction {
region: Region,
return_kind: EarlyReturnKind,
},
StatementsAfterReturn {
region: Region,
@ -504,7 +505,7 @@ impl Problem {
| Problem::OverAppliedDbg { region }
| Problem::UnappliedDbg { region }
| Problem::DefsOnlyUsedInRecursion(_, region)
| Problem::ReturnOutsideOfFunction { region }
| Problem::ReturnOutsideOfFunction { region, .. }
| Problem::StatementsAfterReturn { region }
| Problem::ReturnAtEndOfFunction { region }
| Problem::UnsuffixedEffectfulRecordField(region)
@ -763,8 +764,8 @@ impl RuntimeError {
record: _,
field: region,
}
| RuntimeError::ReadIngestedFileError { region, .. } => *region,
RuntimeError::InvalidUnicodeCodePt(region) => *region,
| RuntimeError::ReadIngestedFileError { region, .. }
| RuntimeError::InvalidUnicodeCodePt(region) => *region,
RuntimeError::UnresolvedTypeVar | RuntimeError::ErroneousType => Region::zero(),
RuntimeError::LookupNotInScope { loc_name, .. } => loc_name.region,
RuntimeError::OpaqueNotDefined { usage, .. } => usage.region,

View file

@ -16,6 +16,7 @@ use roc_can::abilities::{AbilitiesStore, MemberSpecializationInfo};
use roc_can::constraint::Constraint::{self, *};
use roc_can::constraint::{
Cycle, FxCallConstraint, FxSuffixConstraint, FxSuffixKind, LetConstraint, OpportunisticResolve,
TryTargetConstraint,
};
use roc_can::expected::{Expected, PExpected};
use roc_can::module::ModuleParams;
@ -908,6 +909,96 @@ fn solve(
}
}
}
TryTarget(index) => {
let try_target_constraint = &env.constraints.try_target_constraints[index.index()];
let TryTargetConstraint {
target_type_index,
ok_payload_var,
err_payload_var,
region,
kind,
} = try_target_constraint;
let target_actual = either_type_index_to_var(
env,
rank,
problems,
abilities_store,
obligation_cache,
&mut can_types,
aliases,
*target_type_index,
);
let wanted_result_ty = can_types.from_old_type(&Type::TagUnion(
vec![
("Ok".into(), vec![Type::Variable(*ok_payload_var)]),
("Err".into(), vec![Type::Variable(*err_payload_var)]),
],
TypeExtension::Closed,
));
let wanted_result_var = type_to_var(
env,
rank,
problems,
abilities_store,
obligation_cache,
&mut can_types,
aliases,
wanted_result_ty,
);
match unify(
&mut env.uenv(),
target_actual,
wanted_result_var,
UnificationMode::EQ,
Polarity::OF_VALUE,
) {
Success {
vars,
must_implement_ability,
lambda_sets_to_specialize,
extra_metadata: _,
} => {
env.introduce(rank, &vars);
if !must_implement_ability.is_empty() {
let new_problems = obligation_cache.check_obligations(
env.subs,
abilities_store,
must_implement_ability,
AbilityImplError::BadExpr(
*region,
Category::TryTarget,
target_actual,
),
);
problems.extend(new_problems);
}
compact_lambdas_and_check_obligations(
env,
problems,
abilities_store,
obligation_cache,
awaiting_specializations,
lambda_sets_to_specialize,
);
state
}
Failure(vars, actual_type, _expected_type, _bad_impls) => {
env.introduce(rank, &vars);
let problem = TypeError::InvalidTryTarget(*region, actual_type, *kind);
problems.push(problem);
state
}
}
}
Let(index, pool_slice) => {
let let_con = &env.constraints.let_constraints[index.index()];

View file

@ -2,6 +2,7 @@
use std::{path::PathBuf, str::Utf8Error};
use roc_can::constraint::{ExpectEffectfulReason, FxSuffixKind};
use roc_can::expr::TryKind;
use roc_can::{
constraint::FxCallKind,
expected::{Expected, PExpected},
@ -48,6 +49,7 @@ pub enum TypeError {
ExpectedEffectful(Region, ExpectEffectfulReason),
UnsuffixedEffectfulFunction(Region, FxSuffixKind),
SuffixedPureFunction(Region, FxSuffixKind),
InvalidTryTarget(Region, ErrorType, TryKind),
}
impl TypeError {
@ -77,6 +79,7 @@ impl TypeError {
TypeError::FxInTopLevel(_, _) => Warning,
TypeError::UnsuffixedEffectfulFunction(_, _) => Warning,
TypeError::SuffixedPureFunction(_, _) => Warning,
TypeError::InvalidTryTarget(_, _, _) => RuntimeError,
}
}
@ -97,7 +100,8 @@ impl TypeError {
| TypeError::FxInTopLevel(region, _)
| TypeError::ExpectedEffectful(region, _)
| TypeError::UnsuffixedEffectfulFunction(region, _)
| TypeError::SuffixedPureFunction(region, _) => Some(*region),
| TypeError::SuffixedPureFunction(region, _)
| TypeError::InvalidTryTarget(region, _, _) => Some(*region),
TypeError::UnfulfilledAbility(ab, ..) => ab.region(),
TypeError::Exhaustive(e) => Some(e.region()),
TypeError::CircularDef(c) => c.first().map(|ce| ce.symbol_region),

View file

@ -155,7 +155,7 @@ impl<'a, 'c, 'd, 'e, 'f, 'm, 'p, P: Push<Problem>> Env<'a, 'c, 'd, 'e, 'f, 'm, '
}
// Convert the Content to a MonoType, often by passing an iterator. None of these iterators introduce allocations.
let mono_id = match dbg!(*subs.get_content_without_compacting(root_var)) {
let mono_id = match *subs.get_content_without_compacting(root_var) {
Content::Structure(flat_type) => match flat_type {
FlatType::Apply(symbol, args) => {
if symbol.is_builtin() {
@ -452,12 +452,11 @@ fn number_args_to_mono_id(
// Unroll aliases in this loop, as many aliases as we encounter.
loop {
match content {
Content::Structure(flat_type) => {
if let FlatType::Apply(outer_symbol, args) = flat_type {
return num_num_args_to_mono_id(*outer_symbol, *args, subs, problems);
} else {
break;
}
Content::Structure(FlatType::Apply(outer_symbol, args)) => {
return num_num_args_to_mono_id(*outer_symbol, *args, subs, problems);
}
Content::Structure(_) => {
break;
}
Content::FlexVar(_) => {
// Num *

View file

@ -4105,3 +4105,12 @@ fn infinity_f32() {
fn infinity_f64() {
assert_evals_to!(r"Num.infinityF64", f64::INFINITY, f64);
}
#[allow(clippy::non_minimal_cfg)]
#[test]
#[cfg(any(feature = "gen-llvm"))]
fn cast_signed_unsigned() {
assert_evals_to!(r"Num.toI16 255u8", 255, i16);
assert_evals_to!(r"Num.toU16 127i8", 127, u16);
assert_evals_to!(r"Num.toU8 127i8", 127, u8);
assert_evals_to!(r"Num.toI8 127u8", 127, i8);
}

View file

@ -340,8 +340,6 @@ pub(crate) fn asm_evals_to<T, U, F>(
let result = crate::helpers::dev::run_test_main::<T>(&lib);
if !errors.is_empty() {
dbg!(&errors);
assert_eq!(
errors,
std::vec::Vec::new(),

View file

@ -24,25 +24,25 @@ procedure Str.66 (Str.191):
let Str.247 : [C {}, C U64] = TagId(0) Str.248;
ret Str.247;
procedure Test.3 (Test.4):
joinpoint Test.14 Test.5:
let Test.12 : [C {}, C U64] = TagId(1) Test.5;
ret Test.12;
procedure Test.1 (Test.2):
joinpoint Test.11 Test.3:
let Test.7 : [C {}, C U64] = TagId(1) Test.3;
ret Test.7;
in
let Test.13 : [C {}, C U64] = CallByName Str.26 Test.4;
let Test.18 : U8 = 1i64;
let Test.19 : U8 = GetTagId Test.13;
let Test.20 : Int1 = lowlevel Eq Test.18 Test.19;
if Test.20 then
let Test.6 : U64 = UnionAtIndex (Id 1) (Index 0) Test.13;
jump Test.14 Test.6;
let Test.10 : [C {}, C U64] = CallByName Str.26 Test.2;
let Test.15 : U8 = 1i64;
let Test.16 : U8 = GetTagId Test.10;
let Test.17 : Int1 = lowlevel Eq Test.15 Test.16;
if Test.17 then
let Test.8 : U64 = UnionAtIndex (Id 1) (Index 0) Test.10;
jump Test.11 Test.8;
else
let Test.7 : {} = UnionAtIndex (Id 0) (Index 0) Test.13;
let Test.17 : [C {}, C U64] = TagId(0) Test.7;
ret Test.17;
let Test.9 : {} = UnionAtIndex (Id 0) (Index 0) Test.10;
let Test.14 : [C {}, C U64] = TagId(0) Test.9;
ret Test.14;
procedure Test.0 ():
let Test.11 : Str = "123";
let Test.10 : [C {}, C U64] = CallByName Test.3 Test.11;
dec Test.11;
ret Test.10;
let Test.6 : Str = "123";
let Test.5 : [C {}, C U64] = CallByName Test.1 Test.6;
dec Test.6;
ret Test.5;

View file

@ -12,13 +12,16 @@ version.workspace = true
[dependencies]
bumpalo.workspace = true
roc_can.workspace = true
roc_collections.workspace = true
roc_error_macros.workspace = true
roc_fmt.workspace = true
roc_module.workspace = true
roc_parse.workspace = true
roc_region.workspace = true
roc_test_utils.workspace = true
roc_test_utils_dir.workspace = true
roc_types.workspace = true
[dev-dependencies]
indoc.workspace = true

View file

@ -10,9 +10,12 @@ edition = "2021"
cargo-fuzz = true
[dependencies]
test_syntax.workspace = true
roc_parse.workspace = true
bumpalo = { workspace = true, features = ["collections"] }
# WARNING! Do not update these dependencies to use the dot workspace syntax.
# this crate is intentionally not a part of the overall workspace, so making changes here
# is neither necessary nor desired.
test_syntax = { path = "../../test_syntax" }
roc_parse = { path = "../../parse" }
bumpalo = { version = "3.12.0", features = ["collections"] }
libfuzzer-sys = "0.4"
# Prevent this from interfering with workspaces

View file

@ -11,7 +11,7 @@ fuzz_target!(|data: &[u8]| {
let ast = input.parse_in(&arena);
if let Ok(ast) = ast {
if !ast.is_malformed() {
input.check_invariants(|_| (), true);
input.check_invariants(|_| (), true, None);
}
}
}

View file

@ -1,6 +1,6 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
use bumpalo::Bump;
use libfuzzer_sys::fuzz_target;
use test_syntax::test_helpers::Input;
fuzz_target!(|data: &[u8]| {

View file

@ -102,6 +102,12 @@ fn round_trip_once(input: Input<'_>) -> Option<String> {
return Some("Different ast".to_string());
}
let reformatted = reparsed_ast.format();
if output != reformatted {
return Some("Formatting not stable".to_string());
}
None
}

View file

@ -1,14 +1,30 @@
use std::path::Path;
use bumpalo::Bump;
use roc_fmt::{annotation::Formattable, header::fmt_header};
use roc_can::desugar;
use roc_can::env::Env;
use roc_can::expr::canonicalize_expr;
use roc_can::scope::Scope;
use roc_error_macros::set_panic_not_exit;
use roc_fmt::{annotation::Formattable, header::fmt_header, MigrationFlags};
use roc_module::symbol::{IdentIds, Interns, ModuleIds, PackageModuleIds, Symbol};
use roc_parse::header::parse_module_defs;
use roc_parse::parser::Parser;
use roc_parse::parser::SyntaxError;
use roc_parse::state::State;
use roc_parse::test_helpers::parse_loc_with;
use roc_parse::{ast::Malformed, normalize::Normalize};
use roc_parse::{
ast::{Defs, Expr, FullAst, Header, Malformed, SpacesBefore},
header::parse_module_defs,
normalize::Normalize,
parser::{Parser, SyntaxError},
state::State,
test_helpers::{parse_defs_with, parse_expr_with, parse_header_with},
ast::{Defs, Expr, FullAst, Header, SpacesBefore},
test_helpers::{parse_defs_with, parse_header_with},
};
use roc_region::all::Loc;
use roc_region::all::Region;
use roc_test_utils::assert_multiline_str_eq;
use roc_types::{
subs::{VarStore, Variable},
types::{AliasVar, Type},
};
use roc_fmt::Buf;
@ -74,7 +90,7 @@ pub enum Output<'a> {
ModuleDefs(Defs<'a>),
Expr(Expr<'a>),
Expr(Loc<Expr<'a>>),
Full(FullAst<'a>),
}
@ -82,7 +98,8 @@ pub enum Output<'a> {
impl<'a> Output<'a> {
pub fn format(&self) -> InputOwned {
let arena = Bump::new();
let mut buf = Buf::new_in(&arena);
let flags = MigrationFlags::new(false);
let mut buf = Buf::new_in(&arena, flags);
match self {
Output::Header(header) => {
fmt_header(&mut buf, header);
@ -115,6 +132,81 @@ impl<'a> Output<'a> {
Output::Full { .. } => format!("{self:#?}\n"),
}
}
pub fn canonicalize(&self, arena: &Bump, src: &str) {
set_panic_not_exit(true); // can has a bunch of internal_error! calls
match self {
Output::Header(_) => {}
Output::ModuleDefs(_) => {
// TODO: canonicalize module defs
}
Output::Full(_) => {
// TODO: canonicalize full ast
}
Output::Expr(loc_expr) => {
let mut var_store = VarStore::default();
let qualified_module_ids = PackageModuleIds::default();
let home = ModuleIds::default().get_or_insert(&"Test".into());
let mut scope = Scope::new(
home,
"TestPath".into(),
IdentIds::default(),
Default::default(),
);
let dep_idents = IdentIds::exposed_builtins(0);
let mut env = Env::new(
arena,
src,
home,
Path::new("Test.roc"),
&dep_idents,
&qualified_module_ids,
None,
roc_can::env::FxMode::PurityInference,
);
// Desugar operators (convert them to Apply calls, taking into account
// operator precedence and associativity rules), before doing other canonicalization.
//
// If we did this *during* canonicalization, then each time we
// visited a BinOp node we'd recursively try to apply this to each of its nested
// operators, and then again on *their* nested operators, ultimately applying the
// rules multiple times unnecessarily.
let loc_expr = desugar::desugar_expr(&mut env, &mut scope, loc_expr);
scope.add_alias(
Symbol::NUM_INT,
Region::zero(),
vec![Loc::at_zero(AliasVar::unbound(
"a".into(),
Variable::EMPTY_RECORD,
))],
vec![],
Type::EmptyRec,
roc_types::types::AliasKind::Structural,
);
let (_loc_expr, _output) = canonicalize_expr(
&mut env,
&mut var_store,
&mut scope,
Region::zero(),
&loc_expr.value,
);
let mut all_ident_ids = IdentIds::exposed_builtins(1);
all_ident_ids.insert(home, scope.locals.ident_ids);
let _interns = Interns {
module_ids: env.qualified_module_ids.clone().into_module_ids(),
all_ident_ids,
};
}
}
}
}
impl<'a> Malformed for Output<'a> {
@ -162,7 +254,7 @@ impl<'a> Input<'a> {
}
Input::Expr(input) => {
let expr = parse_expr_with(arena, input)?;
let expr = parse_loc_with(arena, input).map_err(|e| e.problem)?;
Ok(Output::Expr(expr))
}
@ -196,6 +288,7 @@ impl<'a> Input<'a> {
&self,
handle_formatted_output: impl Fn(Input),
check_idempotency: bool,
canonicalize_mode: Option<bool>,
) {
let arena = Bump::new();
@ -239,7 +332,7 @@ impl<'a> Input<'a> {
self.as_str(),
output.as_ref().as_str(),
actual,
reparsed_ast_normalized
reparsed_ast
);
}
@ -258,5 +351,27 @@ impl<'a> Input<'a> {
assert_multiline_str_eq!(output.as_ref().as_str(), reformatted.as_ref().as_str());
}
}
if let Some(expect_panic) = canonicalize_mode {
if expect_panic {
let text = self.as_str();
let res = std::panic::catch_unwind(|| {
let new_arena = Bump::new();
actual.canonicalize(&new_arena, text);
});
assert!(
res.is_err(),
"Canonicalize was expected to panic, but it did not. \
If you're running test_snapshots, you may need to remove this test from \
the list of tests that are expected to panic (great!), \
in `fn expect_canonicalize_panics`"
);
} else {
// TODO grab the output here and assert things about it.
// For now we just make sure that it doesn't crash on this input
actual.canonicalize(&arena, self.as_str());
}
}
}
}

View file

@ -1 +1 @@
Expr(When(IfGuard(Start(@28), @27), @0), @0)
Expr(When(IfGuard(Start(@27), @27), @0), @0)

View file

@ -0,0 +1 @@
Expr(If(Condition(Start(@2), @2), @0), @0)

View file

@ -0,0 +1 @@
if!==9

View file

@ -0,0 +1 @@
Expr(Pattern(Start(@4), @4), @0)

View file

@ -0,0 +1 @@
M@S -S implements

View file

@ -0,0 +1 @@
Expr(Type(TInParens(Type(TInParens(Type(TInParens(Type(TInParens(Type(TInParens(Type(TInParens(Type(TInParens(Type(TInParens(Type(TInParens(Type(TInParens(Type(TInParens(Type(TInParens(Type(TInParens(Type(TInParens(Type(TInParens(Type(TInParens(Type(TInParens(Type(TInParens(Type(TInParens(Type(TInParens(Type(TInParens(End(@78), @74), @72), @71), @69), @68), @66), @65), @61), @60), @58), @57), @53), @52), @50), @49), @47), @46), @44), @43), @41), @40), @39), @38), @36), @35), @31), @30), @28), @27), @23), @22), @20), @19), @17), @16), @11), @10), @8), @7), @3), @2), @2), @0)

View file

@ -0,0 +1 @@
.:(i,i,(i,(i,ii,(i,(i,(i,i,(i,(i,i,(i,(J(i,(i,(i,(i,(i,i,(i,(i,i,(i,(i,(i,(J[]

View file

@ -0,0 +1 @@
Expr(Str(EndlessMultiLine(@3), @0), @0)

View file

@ -1,4 +1,4 @@
Apply(
@0-2 Apply(
@0-1 Tag(
"I",
),

View file

@ -1,13 +0,0 @@
Closure(
[
@1-11 MalformedIdent(
"the_answer",
UnderscoreInMiddle(
@5,
),
),
],
@15-17 Num(
"42",
),
)

View file

@ -1,4 +1,4 @@
SpaceAfter(
@0-37 SpaceAfter(
When(
@5-6 Var {
module_name: "",

View file

@ -1,4 +1,4 @@
SpaceAfter(
@0-37 SpaceAfter(
When(
@5-6 Var {
module_name: "",

View file

@ -1,4 +1,4 @@
MalformedIdent(
@0-3 MalformedIdent(
"I.5",
QualifiedTupleAccessor(
@1,

View file

@ -1,4 +1,4 @@
MalformedIdent(
@0-12 MalformedIdent(
"One.Two.Whee",
QualifiedTag(
@12,

View file

@ -1,4 +1,4 @@
SpaceAfter(
@0-46 SpaceAfter(
Defs(
Defs {
tags: [

View file

@ -1,4 +1,4 @@
SpaceAfter(
@0-55 SpaceAfter(
Defs(
Defs {
tags: [

View file

@ -1,4 +1,4 @@
SpaceAfter(
@0-58 SpaceAfter(
Defs(
Defs {
tags: [

View file

@ -1,4 +1,4 @@
SpaceAfter(
@0-107 SpaceAfter(
Defs(
Defs {
tags: [

View file

@ -0,0 +1,7 @@
if
h
then
A
else
&e
s #

View file

@ -0,0 +1,75 @@
@0-22 SpaceAfter(
Defs(
Defs {
tags: [
EitherIndex(2147483648),
],
regions: [
@0-20,
],
space_before: [
Slice<roc_parse::ast::CommentOrNewline> { start: 0, length: 0 },
],
space_after: [
Slice<roc_parse::ast::CommentOrNewline> { start: 0, length: 0 },
],
spaces: [],
type_defs: [],
value_defs: [
Stmt(
@0-20 If {
if_thens: [
(
@3-4 SpaceBefore(
SpaceAfter(
Var {
module_name: "",
ident: "h",
},
[
Newline,
],
),
[
Newline,
],
),
@10-11 SpaceBefore(
SpaceAfter(
Tag(
"A",
),
[
Newline,
],
),
[
Newline,
],
),
),
],
final_else: @18-20 RecordUpdater(
"e",
),
indented_else: false,
},
),
],
},
@21-22 SpaceBefore(
Var {
module_name: "",
ident: "s",
},
[
Newline,
],
),
),
[
LineComment(
"",
),
],
)

View file

@ -0,0 +1,6 @@
if
h
then
A
else &e
s#

View file

@ -1,4 +1,4 @@
BinOps(
@0-5 BinOps(
[
(
@0-1 Var {

View file

@ -1,4 +1,4 @@
BinOps(
@0-8 BinOps(
[
(
@0-1 Num(

View file

@ -1,10 +1,10 @@
Defs(
@0-8 Defs(
Defs {
tags: [
EitherIndex(0),
],
regions: [
@0-3,
@0-6,
],
space_before: [
Slice<roc_parse::ast::CommentOrNewline> { start: 0, length: 0 },
@ -16,25 +16,26 @@ Defs(
type_defs: [
Alias {
header: TypeHeader {
name: @0-1 "J",
name: @0-1 "M",
vars: [],
},
ann: @2-3 Apply(
"",
"R",
[],
ann: @4-5 SpaceBefore(
BoundVariable(
"r",
),
[
Newline,
],
),
},
],
value_defs: [],
},
@5-8 SpaceBefore(
MalformedIdent(
"n_p",
UnderscoreInMiddle(
@7,
),
),
@7-8 SpaceBefore(
Var {
module_name: "",
ident: "h",
},
[
Newline,
],

View file

@ -0,0 +1,48 @@
@0-9 Defs(
Defs {
tags: [
EitherIndex(0),
],
regions: [
@0-7,
],
space_before: [
Slice<roc_parse::ast::CommentOrNewline> { start: 0, length: 0 },
],
space_after: [
Slice<roc_parse::ast::CommentOrNewline> { start: 0, length: 0 },
],
spaces: [],
type_defs: [
Alias {
header: TypeHeader {
name: @0-1 "A",
vars: [
@4-5 SpaceBefore(
Identifier {
ident: "p",
},
[
LineComment(
"",
),
],
),
],
},
ann: @6-7 BoundVariable(
"e",
),
},
],
value_defs: [],
},
@8-9 SpaceBefore(
Tag(
"A",
),
[
Newline,
],
),
)

View file

@ -0,0 +1,44 @@
@0-9 Defs(
Defs {
tags: [
EitherIndex(0),
],
regions: [
@0-7,
],
space_before: [
Slice<roc_parse::ast::CommentOrNewline> { start: 0, length: 0 },
],
space_after: [
Slice<roc_parse::ast::CommentOrNewline> { start: 0, length: 0 },
],
spaces: [],
type_defs: [
Alias {
header: TypeHeader {
name: @0-1 "K",
vars: [],
},
ann: @5-6 SpaceBefore(
BoundVariable(
"s",
),
[
LineComment(
"",
),
],
),
},
],
value_defs: [],
},
@8-9 SpaceBefore(
Tag(
"K",
),
[
Newline,
],
),
)

View file

@ -0,0 +1,3 @@
K:(#
s)
K

View file

@ -0,0 +1,59 @@
@0-11 SpaceAfter(
Defs(
Defs {
tags: [
EitherIndex(0),
],
regions: [
@0-9,
],
space_before: [
Slice<roc_parse::ast::CommentOrNewline> { start: 0, length: 0 },
],
space_after: [
Slice<roc_parse::ast::CommentOrNewline> { start: 0, length: 0 },
],
spaces: [],
type_defs: [
Alias {
header: TypeHeader {
name: @0-1 "O",
vars: [],
},
ann: @2-9 Apply(
"",
"O",
[
@4-5 SpaceAfter(
BoundVariable(
"z",
),
[
Newline,
LineComment(
"",
),
],
),
],
),
},
],
value_defs: [],
},
@10-11 SpaceBefore(
Var {
module_name: "",
ident: "b",
},
[
Newline,
],
),
),
[
LineComment(
"",
),
],
)

View file

@ -0,0 +1,47 @@
@0-8 Defs(
Defs {
tags: [
EitherIndex(2147483648),
],
regions: [
@0-1,
],
space_before: [
Slice<roc_parse::ast::CommentOrNewline> { start: 0, length: 0 },
],
space_after: [
Slice<roc_parse::ast::CommentOrNewline> { start: 0, length: 0 },
],
spaces: [],
type_defs: [],
value_defs: [
Stmt(
@0-1 Var {
module_name: "",
ident: "p",
},
),
],
},
@2-8 SpaceBefore(
UnaryOp(
@4-7 SpaceBefore(
TrySuffix {
target: Task,
expr: AccessorFunction(
RecordField(
"p!",
),
),
},
[
Newline,
],
),
@2-3 Not,
),
[
Newline,
],
),
)

View file

@ -0,0 +1,3 @@
p
!
.p!!

View file

@ -1,4 +1,4 @@
Defs(
@0-42 Defs(
Defs {
tags: [
EitherIndex(2147483648),

View file

@ -1,4 +1,4 @@
SpaceAfter(
@0-105 SpaceAfter(
Defs(
Defs {
tags: [

View file

@ -1,4 +1,4 @@
Defs(
@0-43 Defs(
Defs {
tags: [
EitherIndex(2147483648),

View file

@ -0,0 +1,58 @@
@0-13 SpaceAfter(
Defs(
Defs {
tags: [
EitherIndex(2147483648),
],
regions: [
@0-11,
],
space_before: [
Slice<roc_parse::ast::CommentOrNewline> { start: 0, length: 0 },
],
space_after: [
Slice<roc_parse::ast::CommentOrNewline> { start: 0, length: 0 },
],
spaces: [],
type_defs: [],
value_defs: [
Annotation(
@0-1 Identifier {
ident: "r",
},
@4-5 SpaceBefore(
SpaceAfter(
BoundVariable(
"r",
),
[
Newline,
LineComment(
"",
),
LineComment(
"",
),
],
),
[
Newline,
],
),
),
],
},
@12-13 SpaceBefore(
Var {
module_name: "",
ident: "h",
},
[
Newline,
],
),
),
[
Newline,
],
)

View file

@ -0,0 +1,6 @@
r:(
r
#
#
)
h

View file

@ -0,0 +1,51 @@
@0-11 Defs(
Defs {
tags: [
EitherIndex(2147483648),
],
regions: [
@0-9,
],
space_before: [
Slice<roc_parse::ast::CommentOrNewline> { start: 0, length: 0 },
],
space_after: [
Slice<roc_parse::ast::CommentOrNewline> { start: 0, length: 0 },
],
spaces: [],
type_defs: [],
value_defs: [
Annotation(
@0-7 RecordDestructure(
[
@1-6 SpaceAfter(
RequiredField(
"l",
@5-6 Identifier {
ident: "s",
},
),
[
LineComment(
"",
),
],
),
],
),
@8-9 BoundVariable(
"s",
),
),
],
},
@10-11 SpaceBefore(
Var {
module_name: "",
ident: "o",
},
[
Newline,
],
),
)

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