Merge branch 'main' of github.com:roc-lang/roc into simplify_examples

This commit is contained in:
Anton-4 2022-10-07 16:00:32 +02:00
commit c6ec3d5d30
No known key found for this signature in database
GPG key ID: A13F4A6E21141925
65 changed files with 3191 additions and 679 deletions

View file

@ -5,6 +5,7 @@ use build::BuiltFile;
use bumpalo::Bump;
use clap::{Arg, ArgMatches, Command, ValueSource};
use roc_build::link::{LinkType, LinkingStrategy};
use roc_build::program::Problems;
use roc_collections::VecMap;
use roc_error_macros::{internal_error, user_error};
use roc_load::{Expectations, LoadingProblem, Threading};
@ -566,89 +567,36 @@ pub fn build(
// If possible, report the generated executable name relative to the current dir.
let generated_filename = binary_path
.strip_prefix(env::current_dir().unwrap())
.unwrap_or(&binary_path);
.unwrap_or(&binary_path)
.to_str()
.unwrap();
// No need to waste time freeing this memory,
// since the process is about to exit anyway.
std::mem::forget(arena);
println!(
"\x1B[{}m{}\x1B[39m {} and \x1B[{}m{}\x1B[39m {} found in {} ms while successfully building:\n\n {}",
if problems.errors == 0 {
32 // green
} else {
33 // yellow
},
problems.errors,
if problems.errors == 1 {
"error"
} else {
"errors"
},
if problems.warnings == 0 {
32 // green
} else {
33 // yellow
},
problems.warnings,
if problems.warnings == 1 {
"warning"
} else {
"warnings"
},
total_time.as_millis(),
generated_filename.to_str().unwrap()
);
print_problems(problems, total_time);
println!(" while successfully building:\n\n {generated_filename}");
// Return a nonzero exit code if there were problems
Ok(problems.exit_code())
}
BuildAndRun => {
if problems.errors > 0 || problems.warnings > 0 {
print_problems(problems, total_time);
println!(
"\x1B[{}m{}\x1B[39m {} and \x1B[{}m{}\x1B[39m {} found in {} ms.\n\nRunning program anyway…\n\n\x1B[36m{}\x1B[39m",
if problems.errors == 0 {
32 // green
} else {
33 // yellow
},
problems.errors,
if problems.errors == 1 {
"error"
} else {
"errors"
},
if problems.warnings == 0 {
32 // green
} else {
33 // yellow
},
problems.warnings,
if problems.warnings == 1 {
"warning"
} else {
"warnings"
},
total_time.as_millis(),
".\n\nRunning program anyway…\n\n\x1B[36m{}\x1B[39m",
"".repeat(80)
);
}
let args = matches.values_of_os(ARGS_FOR_APP).unwrap_or_default();
let bytes = std::fs::read(&binary_path).unwrap();
// don't waste time deallocating; the process ends anyway
// ManuallyDrop will leak the bytes because we don't drop manually
let bytes = &ManuallyDrop::new(std::fs::read(&binary_path).unwrap());
let x = roc_run(
arena,
opt_level,
triple,
args,
&bytes,
expectations,
interns,
);
std::mem::forget(bytes);
x
roc_run(arena, opt_level, triple, args, bytes, expectations, interns)
}
BuildAndRunIfNoErrors => {
debug_assert!(
@ -656,21 +604,16 @@ pub fn build(
"if there are errors, they should have been returned as an error variant"
);
if problems.warnings > 0 {
print_problems(problems, total_time);
println!(
"\x1B[32m0\x1B[39m errors and \x1B[33m{}\x1B[39m {} found in {} ms.\n\nRunning program…\n\n\x1B[36m{}\x1B[39m",
problems.warnings,
if problems.warnings == 1 {
"warning"
} else {
"warnings"
},
total_time.as_millis(),
".\n\nRunning program…\n\n\x1B[36m{}\x1B[39m",
"".repeat(80)
);
}
let args = matches.values_of_os(ARGS_FOR_APP).unwrap_or_default();
// don't waste time deallocating; the process ends anyway
// ManuallyDrop will leak the bytes because we don't drop manually
let bytes = &ManuallyDrop::new(std::fs::read(&binary_path).unwrap());
@ -686,40 +629,17 @@ pub fn build(
let problems = roc_build::program::report_problems_typechecked(&mut module);
let mut output = format!(
"\x1B[{}m{}\x1B[39m {} and \x1B[{}m{}\x1B[39m {} found in {} ms.\n\nYou can run the program anyway with \x1B[32mroc run",
if problems.errors == 0 {
32 // green
} else {
33 // yellow
},
problems.errors,
if problems.errors == 1 {
"error"
} else {
"errors"
},
if problems.warnings == 0 {
32 // green
} else {
33 // yellow
},
problems.warnings,
if problems.warnings == 1 {
"warning"
} else {
"warnings"
},
total_time.as_millis(),
);
print_problems(problems, total_time);
print!(".\n\nYou can run the program anyway with \x1B[32mroc run");
// If you're running "main.roc" then you can just do `roc run`
// to re-run the program.
if filename != DEFAULT_ROC_FILENAME {
output.push(' ');
output.push_str(&filename.to_string_lossy());
print!(" {}", &filename.to_string_lossy());
}
println!("{}\x1B[39m", output);
println!("\x1B[39m");
Ok(problems.exit_code())
}
@ -734,6 +654,34 @@ pub fn build(
}
}
fn print_problems(problems: Problems, total_time: std::time::Duration) {
const GREEN: usize = 32;
const YELLOW: usize = 33;
print!(
"\x1B[{}m{}\x1B[39m {} and \x1B[{}m{}\x1B[39m {} found in {} ms",
match problems.errors {
0 => GREEN,
_ => YELLOW,
},
problems.errors,
match problems.errors {
1 => "error",
_ => "errors",
},
match problems.warnings {
0 => GREEN,
_ => YELLOW,
},
problems.warnings,
match problems.warnings {
1 => "warning",
_ => "warnings",
},
total_time.as_millis(),
);
}
fn roc_run<'a, I: IntoIterator<Item = &'a OsStr>>(
arena: Bump, // This should be passed an owned value, not a reference, so we can usefully mem::forget it!
opt_level: OptLevel,
@ -814,12 +762,17 @@ fn make_argv_envp<'a, I: IntoIterator<Item = S>, S: AsRef<OsStr>>(
// envp is an array of pointers to strings, conventionally of the
// form key=value, which are passed as the environment of the new
// program. The envp array must be terminated by a NULL pointer.
let mut buffer = Vec::with_capacity(100);
let envp_cstrings: bumpalo::collections::Vec<CString> = std::env::vars_os()
.flat_map(|(k, v)| {
[
CString::new(k.as_bytes()).unwrap(),
CString::new(v.as_bytes()).unwrap(),
]
.map(|(k, v)| {
buffer.clear();
use std::io::Write;
buffer.write_all(k.as_bytes()).unwrap();
buffer.write_all(b"=").unwrap();
buffer.write_all(v.as_bytes()).unwrap();
CString::new(buffer.as_slice()).unwrap()
})
.collect_in(arena);

View file

@ -70,7 +70,11 @@ mod cli_run {
}
fn check_compile_error(file: &Path, flags: &[&str], expected: &str) {
let compile_out = run_roc([CMD_CHECK, file.to_str().unwrap()].iter().chain(flags), &[]);
let compile_out = run_roc(
[CMD_CHECK, file.to_str().unwrap()].iter().chain(flags),
&[],
&[],
);
let err = compile_out.stdout.trim();
let err = strip_colors(err);
@ -82,7 +86,7 @@ mod cli_run {
}
fn check_format_check_as_expected(file: &Path, expects_success_exit_code: bool) {
let out = run_roc([CMD_FORMAT, file.to_str().unwrap(), CHECK_FLAG], &[]);
let out = run_roc([CMD_FORMAT, file.to_str().unwrap(), CHECK_FLAG], &[], &[]);
assert_eq!(out.status.success(), expects_success_exit_code);
}
@ -92,6 +96,7 @@ mod cli_run {
args: I,
stdin: &[&str],
roc_app_args: &[String],
env: &[(&str, &str)],
) -> Out {
let compile_out = run_roc(
// converting these all to String avoids lifetime issues
@ -100,6 +105,7 @@ mod cli_run {
.chain([file.to_str().unwrap().to_string(), "--".to_string()])
.chain(roc_app_args.iter().cloned()),
stdin,
env,
);
let ignorable = "🔨 Rebuilding platform...\n";
@ -150,7 +156,13 @@ mod cli_run {
let out = match cli_mode {
CliMode::RocBuild => {
run_roc_on(file, iter::once(CMD_BUILD).chain(flags.clone()), &[], &[]);
run_roc_on(
file,
iter::once(CMD_BUILD).chain(flags.clone()),
&[],
&[],
&[],
);
if use_valgrind && ALLOW_VALGRIND {
let mut valgrind_args = vec![file
@ -207,25 +219,18 @@ mod cli_run {
}
CliMode::Roc => {
if !extra_env.is_empty() {
// TODO: environment is not currently forwarded by Roc to the target
// binary, so this would fail!
// TODO: `roc` and `roc dev` are currently buggy for `env.roc`
continue;
}
run_roc_on(file, flags.clone(), stdin, roc_app_args)
run_roc_on(file, flags.clone(), stdin, roc_app_args, extra_env)
}
CliMode::RocRun => {
if !extra_env.is_empty() {
// TODO: environment is not currently forwarded by Roc to the target
// binary, so this would fail!
continue;
}
run_roc_on(
CliMode::RocRun => run_roc_on(
file,
iter::once(CMD_RUN).chain(flags.clone()),
stdin,
roc_app_args,
)
}
extra_env,
),
};
if !&out.stdout.ends_with(expected_ending) {
@ -305,7 +310,7 @@ mod cli_run {
// Since these require things the build system often doesn't have
// (e.g. GUIs open a window, Ruby needs ruby installed, WASM needs a browser)
// we do `roc build` on them but don't run them.
run_roc_on(&file_name, [CMD_BUILD, OPTIMIZE_FLAG], &[], &[]);
run_roc_on(&file_name, [CMD_BUILD, OPTIMIZE_FLAG], &[], &[], &[]);
return;
}
"swiftui" | "rocLovesSwift" => {
@ -316,7 +321,7 @@ mod cli_run {
);
return;
} else {
run_roc_on(&file_name, [CMD_BUILD, OPTIMIZE_FLAG], &[], &[]);
run_roc_on(&file_name, [CMD_BUILD, OPTIMIZE_FLAG], &[], &[], &[]);
return;
}
}

View file

@ -1,3 +1,3 @@
interface Foo
interface ExposedNotDefined
exposes [bar]
imports []

View file

@ -16,6 +16,7 @@ fn exec_bench_w_input<T: Measurement>(
let compile_out = run_roc(
["build", OPTIMIZE_FLAG, file.to_str().unwrap()],
&[stdin_str],
&[],
);
if !compile_out.stderr.is_empty() && compile_out.stderr != "🔨 Rebuilding platform...\n" {

View file

@ -22,7 +22,7 @@ pub struct Out {
pub status: ExitStatus,
}
pub fn run_roc<I, S>(args: I, stdin_vals: &[&str]) -> Out
pub fn run_roc<I, S>(args: I, stdin_vals: &[&str], extra_env: &[(&str, &str)]) -> Out
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
@ -62,7 +62,7 @@ where
}
}
run_with_stdin(&roc_binary_path, args, stdin_vals)
run_with_stdin_and_env(&roc_binary_path, args, stdin_vals, extra_env)
}
pub fn run_glue<I, S>(args: I) -> Out
@ -118,6 +118,19 @@ pub fn strip_colors(str: &str) -> String {
}
pub fn run_with_stdin<I, S>(path: &Path, args: I, stdin_vals: &[&str]) -> Out
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
run_with_stdin_and_env(path, args, stdin_vals, &[])
}
pub fn run_with_stdin_and_env<I, S>(
path: &Path,
args: I,
stdin_vals: &[&str],
extra_env: &[(&str, &str)],
) -> Out
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
@ -128,6 +141,10 @@ where
cmd.arg(arg);
}
for (k, v) in extra_env {
cmd.env(k, v);
}
let mut child = cmd
.stdin(Stdio::piped())
.stdout(Stdio::piped())

View file

@ -517,17 +517,25 @@ pub fn listSublist(
len: usize,
dec: Dec,
) callconv(.C) RocList {
if (len == 0) {
const size = list.len();
if (len == 0 or start >= size) {
if (list.isUnique()) {
// Decrement the reference counts of all elements.
if (list.bytes) |source_ptr| {
var i: usize = 0;
while (i < size) : (i += 1) {
const element = source_ptr + i * element_width;
dec(element);
}
var output = list;
output.length = 0;
return output;
}
}
return RocList.empty();
}
if (list.bytes) |source_ptr| {
const size = list.len();
if (start >= size) {
return RocList.empty();
}
const keep_len = std.math.min(len, size - start);
const drop_start_len = start;
const drop_end_len = size - (start + keep_len);
@ -546,10 +554,17 @@ pub fn listSublist(
dec(element);
}
if (start == 0 and list.isUnique()) {
if (list.isUnique()) {
var output = list;
output.length = keep_len;
if (start == 0) {
return output;
} else {
// We want memmove due to aliasing. Zig does not expose it directly.
// Instead use copy which can write to aliases as long as the dest is before the source.
mem.copy(u8, source_ptr[0 .. keep_len * element_width], source_ptr[start * element_width .. (start + keep_len) * element_width]);
return output;
}
} else {
const output = RocList.allocate(alignment, keep_len, element_width);
const target_ptr = output.bytes orelse unreachable;

View file

@ -30,6 +30,23 @@ interface Decode
]
imports [
List,
Result.{ Result },
Num.{
U8,
U16,
U32,
U64,
U128,
I8,
I16,
I32,
I64,
I128,
F32,
F64,
Dec,
},
Bool.{ Bool },
]
DecodeError : [TooShort]

View file

@ -20,6 +20,7 @@ interface Dict
Bool.{ Bool },
Result.{ Result },
List,
Num.{ Nat },
]
## A [dictionary](https://en.wikipedia.org/wiki/Associative_array) that lets you can associate keys with values.

View file

@ -27,7 +27,24 @@ interface Encode
append,
toBytes,
]
imports []
imports [
Num.{
U8,
U16,
U32,
U64,
U128,
I8,
I16,
I32,
I64,
I128,
F32,
F64,
Dec,
},
Bool.{ Bool },
]
Encoder fmt := List U8, fmt -> List U8 | fmt has EncoderFormatting

View file

@ -0,0 +1,84 @@
interface Hash
exposes [
Hash,
Hasher,
hash,
addBytes,
addU8,
addU16,
addU32,
addU64,
addU128,
addI8,
addI16,
addI32,
addI64,
addI128,
complete,
hashStrBytes,
hashList,
] imports [
List,
Str,
Num.{ U8, U16, U32, U64, U128, I8, I16, I32, I64, I128 },
]
## A value that can hashed.
Hash has
## Hashes a value into a [Hasher].
## Note that [hash] does not produce a hash value itself; the hasher must be
## [complete]d in order to extract the hash value.
hash : hasher, a -> hasher | a has Hash, hasher has Hasher
## Describes a hashing algorithm that is fed bytes and produces an integer hash.
##
## The [Hasher] ability describes general-purpose hashers. It only allows
## emission of 64-bit unsigned integer hashes. It is not suitable for
## cryptographically-secure hashing.
Hasher has
## Adds a list of bytes to the hasher.
addBytes : a, List U8 -> a | a has Hasher
## Adds a single U8 to the hasher.
addU8 : a, U8 -> a | a has Hasher
## Adds a single U16 to the hasher.
addU16 : a, U16 -> a | a has Hasher
## Adds a single U32 to the hasher.
addU32 : a, U32 -> a | a has Hasher
## Adds a single U64 to the hasher.
addU64 : a, U64 -> a | a has Hasher
## Adds a single U128 to the hasher.
addU128 : a, U128 -> a | a has Hasher
## Adds a single I8 to the hasher.
addI8 : a, I8 -> a | a has Hasher
## Adds a single I16 to the hasher.
addI16 : a, I16 -> a | a has Hasher
## Adds a single I32 to the hasher.
addI32 : a, I32 -> a | a has Hasher
## Adds a single I64 to the hasher.
addI64 : a, I64 -> a | a has Hasher
## Adds a single I128 to the hasher.
addI128 : a, I128 -> a | a has Hasher
## Completes the hasher, extracting a hash value from its
## accumulated hash state.
complete : a -> U64 | a has Hasher
## Adds a string into a [Hasher] by hashing its UTF-8 bytes.
hashStrBytes = \hasher, s ->
Str.walkUtf8WithIndex s hasher \accumHasher, byte, _ ->
addU8 accumHasher byte
## Adds a list of [Hash]able elements to a [Hasher] by hashing each element.
hashList = \hasher, lst ->
List.walk lst hasher \accumHasher, elem ->
hash accumHasher elem

View file

@ -18,6 +18,23 @@ interface Json
DecoderFormatting,
DecodeResult,
},
Num.{
U8,
U16,
U32,
U64,
U128,
I8,
I16,
I32,
I64,
I128,
F32,
F64,
Dec,
},
Bool.{ Bool },
Result,
]
Json := {} has [

View file

@ -63,6 +63,8 @@ interface List
]
imports [
Bool.{ Bool },
Result.{ Result },
Num.{ Nat, Num, Int },
]
## Types

View file

@ -145,6 +145,7 @@ interface Num
]
imports [
Bool.{ Bool },
Result.{ Result },
]
## Represents a number that could be either an [Int] or a [Frac].
@ -868,7 +869,7 @@ bitwiseOr : Int a, Int a -> Int a
## >>> 0b0000_0101 |> shiftLeftBy 2 == 0b0000_1100
##
## In some languages `shiftLeftBy` is implemented as a binary operator `<<`.
shiftLeftBy : Int a, Int a -> Int a
shiftLeftBy : Int a, U8 -> Int a
## Bitwise arithmetic shift of a number by another
##
@ -881,7 +882,7 @@ shiftLeftBy : Int a, Int a -> Int a
## >>> 0b1001_0000 |> shiftRightBy 2 == 0b1110_0100
##
## In some languages `shiftRightBy` is implemented as a binary operator `>>>`.
shiftRightBy : Int a, Int a -> Int a
shiftRightBy : Int a, U8 -> Int a
## Bitwise logical right shift of a number by another
##
@ -895,7 +896,7 @@ shiftRightBy : Int a, Int a -> Int a
## >>> 0b1001_0000 |> shiftRightBy 2 == 0b0010_0100
##
## In some languages `shiftRightBy` is implemented as a binary operator `>>`.
shiftRightZfBy : Int a, Int a -> Int a
shiftRightZfBy : Int a, U8 -> Int a
## Round off the given fraction to the nearest integer.
round : Frac * -> Int *

View file

@ -14,7 +14,7 @@ interface Set
intersection,
difference,
]
imports [List, Bool.{ Bool }, Dict.{ Dict }]
imports [List, Bool.{ Bool }, Dict.{ Dict }, Num.{ Nat }]
Set k := Dict.Dict k {}

View file

@ -44,7 +44,12 @@ interface Str
walkScalars,
walkScalarsUntil,
]
imports [Bool.{ Bool }, Result.{ Result }, List]
imports [
Bool.{ Bool },
Result.{ Result },
List,
Num.{ Nat, Num, U8, U16, U32, U64, U128, I8, I16, I32, I64, I128, F32, F64, Dec },
]
## # Types
##

View file

@ -13,6 +13,7 @@ pub fn module_source(module_id: ModuleId) -> &'static str {
ModuleId::BOOL => BOOL,
ModuleId::ENCODE => ENCODE,
ModuleId::DECODE => DECODE,
ModuleId::HASH => HASH,
ModuleId::JSON => JSON,
_ => panic!(
"ModuleId {:?} is not part of the standard library",
@ -31,4 +32,5 @@ const BOX: &str = include_str!("../roc/Box.roc");
const BOOL: &str = include_str!("../roc/Bool.roc");
const ENCODE: &str = include_str!("../roc/Encode.roc");
const DECODE: &str = include_str!("../roc/Decode.roc");
const HASH: &str = include_str!("../roc/Hash.roc");
const JSON: &str = include_str!("../roc/Json.roc");

View file

@ -1516,8 +1516,10 @@ pub(crate) fn sort_can_defs_new(
let def = defs.pop().unwrap();
let index = group.first_one().unwrap();
if def_ordering.direct_references.get_row_col(index, index) {
// a definition like `x = x + 1`, which is invalid in roc
let bad_recursion_body = if def_ordering.direct_references.get_row_col(index, index)
{
// a definition like `x = x + 1`, which is invalid in roc.
// We need to convert the body of the def to a runtime error.
let symbol = def_ordering.get_symbol(index).unwrap();
let entries = vec![make_cycle_entry(symbol, &def)];
@ -1525,9 +1527,19 @@ pub(crate) fn sort_can_defs_new(
let problem = Problem::RuntimeError(RuntimeError::CircularDef(entries.clone()));
env.problem(problem);
// Declaration::InvalidCycle(entries)
todo!("InvalidCycle: {:?}", entries)
} else if def_ordering.references.get_row_col(index, index) {
Some(Expr::RuntimeError(RuntimeError::CircularDef(entries)))
} else {
None
};
let is_illegally_self_recursive = bad_recursion_body.is_some();
let set_opt_invalid_recursion_body = |e: &mut Expr| match bad_recursion_body {
Some(err) => *e = err,
None => {}
};
if def_ordering.references.get_row_col(index, index) && !is_illegally_self_recursive
{
// this function calls itself, and must be typechecked as a recursive def
match def.loc_pattern.value {
Pattern::Identifier(symbol) => match def.loc_expr.value {
@ -1540,7 +1552,7 @@ pub(crate) fn sort_can_defs_new(
None,
);
}
_ => todo!(),
e => todo!("{:?}", e),
},
Pattern::AbilityMemberSpecialization {
ident: symbol,
@ -1562,7 +1574,9 @@ pub(crate) fn sort_can_defs_new(
} else {
match def.loc_pattern.value {
Pattern::Identifier(symbol) => match def.loc_expr.value {
Closure(closure_data) => {
Closure(mut closure_data) => {
set_opt_invalid_recursion_body(&mut closure_data.loc_body.value);
declarations.push_function_def(
Loc::at(def.loc_pattern.region, symbol),
Loc::at(def.loc_expr.region, closure_data),
@ -1572,6 +1586,9 @@ pub(crate) fn sort_can_defs_new(
);
}
_ => {
let mut def = def;
set_opt_invalid_recursion_body(&mut def.loc_expr.value);
declarations.push_value_def(
Loc::at(def.loc_pattern.region, symbol),
def.loc_expr,
@ -1585,7 +1602,9 @@ pub(crate) fn sort_can_defs_new(
ident: symbol,
specializes,
} => match def.loc_expr.value {
Closure(closure_data) => {
Closure(mut closure_data) => {
set_opt_invalid_recursion_body(&mut closure_data.loc_body.value);
declarations.push_function_def(
Loc::at(def.loc_pattern.region, symbol),
Loc::at(def.loc_expr.region, closure_data),
@ -1595,6 +1614,9 @@ pub(crate) fn sort_can_defs_new(
);
}
_ => {
let mut def = def;
set_opt_invalid_recursion_body(&mut def.loc_expr.value);
declarations.push_value_def(
Loc::at(def.loc_pattern.region, symbol),
def.loc_expr,
@ -1605,6 +1627,9 @@ pub(crate) fn sort_can_defs_new(
}
},
_ => {
let mut def = def;
set_opt_invalid_recursion_body(&mut def.loc_expr.value);
declarations.push_destructure_def(
def.loc_pattern,
def.loc_expr,

View file

@ -0,0 +1,191 @@
//! Derivers for the `Hash` ability.
use std::iter::once;
use roc_can::{
expr::{AnnotatedMark, ClosureData, Expr, Recursive},
pattern::Pattern,
};
use roc_derive_key::hash::FlatHashKey;
use roc_module::{called_via::CalledVia, ident::Lowercase, symbol::Symbol};
use roc_region::all::Loc;
use roc_types::{
subs::{
Content, FlatType, LambdaSet, OptVariable, RecordFields, SubsSlice, UnionLambdas, Variable,
VariableSubsSlice,
},
types::RecordField,
};
use crate::{synth_var, util::Env, DerivedBody};
pub(crate) fn derive_hash(env: &mut Env<'_>, key: FlatHashKey, def_symbol: Symbol) -> DerivedBody {
let (body, body_type) = match key {
FlatHashKey::Record(fields) => hash_record(env, def_symbol, fields),
};
let specialization_lambda_sets =
env.get_specialization_lambda_sets(body_type, Symbol::HASH_HASH);
DerivedBody {
body,
body_type,
specialization_lambda_sets,
}
}
fn hash_record(env: &mut Env<'_>, fn_name: Symbol, fields: Vec<Lowercase>) -> (Expr, Variable) {
// Suppose rcd = { f1, ..., fn }.
// Build a generalized type t_rcd = { f1: t1, ..., fn: tn }, with fresh t1, ..., tn,
// so that we can re-use the derived impl for many records of the same fields.
let (record_var, record_fields) = {
let flex_fields = fields
.into_iter()
.map(|name| {
(
name,
RecordField::Required(env.subs.fresh_unnamed_flex_var()),
)
})
.collect::<Vec<(Lowercase, _)>>();
let fields = RecordFields::insert_into_subs(env.subs, flex_fields);
let record_var = synth_var(
env.subs,
Content::Structure(FlatType::Record(fields, Variable::EMPTY_RECORD)),
);
(record_var, fields)
};
// Now, a hasher for this record is
//
// hash_rcd : hasher, { f1: t1, ..., fn: tn } -> hasher | hasher has Hasher
// hash_rcd = \hasher, rcd ->
// Hash.hash (
// Hash.hash
// ...
// (Hash.hash hasher rcd.f1)
// ...
// rcd.f_n1)
// rcd.fn
//
// So, just a build a fold travelling up the fields.
let rcd_sym = env.new_symbol("rcd");
let hasher_sym = env.new_symbol("hasher");
let hasher_var = synth_var(env.subs, Content::FlexAbleVar(None, Symbol::HASH_HASHER));
let (body, body_var) = record_fields.iter_all().fold(
(Expr::Var(hasher_sym), hasher_var),
|(body, body_var), (field_name, field_var, _)| {
let field_name = env.subs[field_name].clone();
let field_var = env.subs[field_var];
let field_access = Expr::Access {
record_var,
field_var,
ext_var: env.subs.fresh_unnamed_flex_var(),
loc_expr: Box::new(Loc::at_zero(Expr::Var(rcd_sym))),
field: field_name,
};
let (hash_fn_data, returned_hasher_var) = {
// build `Hash.hash ...` function type
//
// hasher, val -[uls]-> hasher | hasher has Hasher, val has Hash
let exposed_hash_fn_var = env.import_builtin_symbol_var(Symbol::HASH_HASH);
// (typeof body), (typeof field) -[clos]-> hasher_result
let this_arguments_slice =
VariableSubsSlice::insert_into_subs(env.subs, [body_var, field_var]);
let this_hash_clos_var = env.subs.fresh_unnamed_flex_var();
let this_hasher_result_var = env.subs.fresh_unnamed_flex_var();
let this_hash_fn_var = synth_var(
env.subs,
Content::Structure(FlatType::Func(
this_arguments_slice,
this_hash_clos_var,
this_hasher_result_var,
)),
);
// hasher, val -[uls]-> hasher | hasher has Hasher, val has Hash
// ~ (typeof body), (typeof field) -[clos]-> hasher_result
env.unify(exposed_hash_fn_var, this_hash_fn_var);
// Hash.hash : hasher, (typeof field) -[clos]-> hasher | hasher has Hasher, (typeof field) has Hash
let hash_fn_head = Expr::AbilityMember(Symbol::HASH_HASH, None, this_hash_fn_var);
let hash_fn_data = Box::new((
this_hash_fn_var,
Loc::at_zero(hash_fn_head),
this_hash_clos_var,
this_hasher_result_var,
));
(hash_fn_data, this_hasher_result_var)
};
let hash_arguments = vec![
(body_var, Loc::at_zero(body)),
(field_var, Loc::at_zero(field_access)),
];
let call_hash = Expr::Call(hash_fn_data, hash_arguments, CalledVia::Space);
(call_hash, returned_hasher_var)
},
);
// Finally, build the closure
// \hasher, rcd -> body
let (fn_var, fn_clos_var) = {
// Create fn_var for ambient capture; we fix it up below.
let fn_var = synth_var(env.subs, Content::Error);
// -[fn_name]->
let fn_captures = vec![];
let fn_name_labels = UnionLambdas::insert_into_subs(env.subs, once((fn_name, fn_captures)));
let fn_clos_var = synth_var(
env.subs,
Content::LambdaSet(LambdaSet {
solved: fn_name_labels,
recursion_var: OptVariable::NONE,
unspecialized: SubsSlice::default(),
ambient_function: fn_var,
}),
);
// hasher, rcd_var -[fn_name]-> (hasher = body_var)
let args_slice = SubsSlice::insert_into_subs(env.subs, [hasher_var, record_var]);
env.subs.set_content(
fn_var,
Content::Structure(FlatType::Func(args_slice, fn_clos_var, body_var)),
);
(fn_var, fn_clos_var)
};
let clos_expr = Expr::Closure(ClosureData {
function_type: fn_var,
closure_type: fn_clos_var,
return_type: body_var,
name: fn_name,
captured_symbols: vec![],
recursive: Recursive::NotRecursive,
arguments: vec![
(
hasher_var,
AnnotatedMark::known_exhaustive(),
Loc::at_zero(Pattern::Identifier(hasher_sym)),
),
(
record_var,
AnnotatedMark::known_exhaustive(),
Loc::at_zero(Pattern::Identifier(rcd_sym)),
),
],
loc_body: Box::new(Loc::at_zero(body)),
});
(clos_expr, fn_var)
}

View file

@ -18,6 +18,7 @@ use util::Env;
mod decoding;
mod encoding;
mod hash;
mod util;
@ -77,6 +78,7 @@ fn build_derived_body(
DeriveKey::Decoder(decoder_key) => {
decoding::derive_decoder(&mut env, decoder_key, derived_symbol)
}
DeriveKey::Hash(hash_key) => hash::derive_hash(&mut env, hash_key, derived_symbol),
};
let def = Def {

View file

@ -0,0 +1,123 @@
use roc_module::{ident::Lowercase, symbol::Symbol};
use roc_types::subs::{Content, FlatType, Subs, Variable};
use crate::{
util::{check_derivable_ext_var, debug_name_record},
DeriveError,
};
#[derive(Hash)]
pub enum FlatHash {
// `hash` is always of form `hasher, a -> hasher` where `hasher` and `a` are opaque, so all
// immediates must have exactly one lambda set!
SingleLambdaSetImmediate(Symbol),
Key(FlatHashKey),
}
#[derive(Hash, PartialEq, Eq, Debug, Clone)]
pub enum FlatHashKey {
// Unfortunate that we must allocate here, c'est la vie
Record(Vec<Lowercase>),
}
impl FlatHashKey {
pub(crate) fn debug_name(&self) -> String {
match self {
FlatHashKey::Record(fields) => debug_name_record(fields),
}
}
}
impl FlatHash {
pub(crate) fn from_var(subs: &Subs, var: Variable) -> Result<FlatHash, DeriveError> {
use DeriveError::*;
use FlatHash::*;
match *subs.get_content_without_compacting(var) {
Content::Structure(flat_type) => match flat_type {
FlatType::Apply(sym, _) => match sym {
Symbol::LIST_LIST => Ok(SingleLambdaSetImmediate(Symbol::HASH_HASH_LIST)),
Symbol::STR_STR => Ok(SingleLambdaSetImmediate(Symbol::HASH_HASH_STR_BYTES)),
_ => Err(Underivable),
},
FlatType::Record(fields, ext) => {
let (fields_iter, ext) = fields.unsorted_iterator_and_ext(subs, ext);
check_derivable_ext_var(subs, ext, |ext| {
matches!(ext, Content::Structure(FlatType::EmptyRecord))
})?;
let mut field_names = Vec::with_capacity(fields.len());
for (field_name, record_field) in fields_iter {
if record_field.is_optional() {
// Can't derive a concrete decoder for optional fields, since those are
// compile-time-polymorphic
return Err(Underivable);
}
field_names.push(field_name.clone());
}
field_names.sort();
Ok(Key(FlatHashKey::Record(field_names)))
}
FlatType::TagUnion(_tags, _ext) | FlatType::RecursiveTagUnion(_, _tags, _ext) => {
Err(Underivable) // yet
}
FlatType::FunctionOrTagUnion(_name_index, _, _) => {
Err(Underivable) // yet
}
FlatType::EmptyRecord => Ok(Key(FlatHashKey::Record(vec![]))),
FlatType::EmptyTagUnion => {
Err(Underivable) // yet
}
//
FlatType::Erroneous(_) => Err(Underivable),
FlatType::Func(..) => Err(Underivable),
},
Content::Alias(sym, _, real_var, _) => match sym {
Symbol::NUM_U8 | Symbol::NUM_UNSIGNED8 => {
Ok(SingleLambdaSetImmediate(Symbol::HASH_ADD_U8))
}
Symbol::NUM_U16 | Symbol::NUM_UNSIGNED16 => {
Ok(SingleLambdaSetImmediate(Symbol::HASH_ADD_U16))
}
Symbol::NUM_U32 | Symbol::NUM_UNSIGNED32 => {
Ok(SingleLambdaSetImmediate(Symbol::HASH_ADD_U32))
}
Symbol::NUM_U64 | Symbol::NUM_UNSIGNED64 => {
Ok(SingleLambdaSetImmediate(Symbol::HASH_ADD_U64))
}
Symbol::NUM_U128 | Symbol::NUM_UNSIGNED128 => {
Ok(SingleLambdaSetImmediate(Symbol::HASH_ADD_U128))
}
Symbol::NUM_I8 | Symbol::NUM_SIGNED8 => {
Ok(SingleLambdaSetImmediate(Symbol::HASH_ADD_I8))
}
Symbol::NUM_I16 | Symbol::NUM_SIGNED16 => {
Ok(SingleLambdaSetImmediate(Symbol::HASH_ADD_I16))
}
Symbol::NUM_I32 | Symbol::NUM_SIGNED32 => {
Ok(SingleLambdaSetImmediate(Symbol::HASH_ADD_I32))
}
Symbol::NUM_I64 | Symbol::NUM_SIGNED64 => {
Ok(SingleLambdaSetImmediate(Symbol::HASH_ADD_I64))
}
Symbol::NUM_I128 | Symbol::NUM_SIGNED128 => {
Ok(SingleLambdaSetImmediate(Symbol::HASH_ADD_I128))
}
// NB: I believe it is okay to unwrap opaques here because derivers are only used
// by the backend, and the backend treats opaques like structural aliases.
_ => Self::from_var(subs, real_var),
},
Content::RangedNumber(_) => Err(Underivable),
//
Content::RecursionVar { .. } => Err(Underivable),
Content::Error => Err(Underivable),
Content::FlexVar(_)
| Content::RigidVar(_)
| Content::FlexAbleVar(_, _)
| Content::RigidAbleVar(_, _) => Err(UnboundVar),
Content::LambdaSet(_) => Err(Underivable),
}
}
}

View file

@ -15,10 +15,12 @@
pub mod decoding;
pub mod encoding;
pub mod hash;
mod util;
use decoding::{FlatDecodable, FlatDecodableKey};
use encoding::{FlatEncodable, FlatEncodableKey};
use hash::{FlatHash, FlatHashKey};
use roc_module::symbol::Symbol;
use roc_types::subs::{Subs, Variable};
@ -37,6 +39,7 @@ pub enum DeriveError {
pub enum DeriveKey {
ToEncoder(FlatEncodableKey),
Decoder(FlatDecodableKey),
Hash(FlatHashKey),
}
impl DeriveKey {
@ -44,6 +47,7 @@ impl DeriveKey {
match self {
DeriveKey::ToEncoder(key) => format!("toEncoder_{}", key.debug_name()),
DeriveKey::Decoder(key) => format!("decoder_{}", key.debug_name()),
DeriveKey::Hash(key) => format!("hash_{}", key.debug_name()),
}
}
}
@ -52,7 +56,14 @@ impl DeriveKey {
pub enum Derived {
/// If a derived implementation name is well-known ahead-of-time, we can inline the symbol
/// directly rather than associating a key for an implementation to be made later on.
///
/// Immediates refer to ability members that are "inlined" at the derivation call site.
Immediate(Symbol),
/// Like an [Derived::Immediate], but with the additional constraint that the immediate
/// symbol is statically known to have exactly one lamdba set.
/// This unlocks some optimization opportunities, as regioned lambda sets do not need to be
/// chased.
SingleLambdaSetImmediate(Symbol),
/// Key of the derived implementation to use. This allows association of derived implementation
/// names to a key, when the key is known ahead-of-time but the implementation (and it's name)
/// is yet-to-be-made.
@ -64,6 +75,7 @@ pub enum Derived {
pub enum DeriveBuiltin {
ToEncoder,
Decoder,
Hash,
}
impl TryFrom<Symbol> for DeriveBuiltin {
@ -73,6 +85,7 @@ impl TryFrom<Symbol> for DeriveBuiltin {
match value {
Symbol::ENCODE_TO_ENCODER => Ok(DeriveBuiltin::ToEncoder),
Symbol::DECODE_DECODER => Ok(DeriveBuiltin::Decoder),
Symbol::HASH_HASH => Ok(DeriveBuiltin::Hash),
_ => Err(value),
}
}
@ -93,6 +106,12 @@ impl Derived {
FlatDecodable::Immediate(imm) => Ok(Derived::Immediate(imm)),
FlatDecodable::Key(repr) => Ok(Derived::Key(DeriveKey::Decoder(repr))),
},
DeriveBuiltin::Hash => match hash::FlatHash::from_var(subs, var)? {
FlatHash::SingleLambdaSetImmediate(imm) => {
Ok(Derived::SingleLambdaSetImmediate(imm))
}
FlatHash::Key(repr) => Ok(Derived::Key(DeriveKey::Hash(repr))),
},
}
}
}

View file

@ -3,7 +3,7 @@ use crate::pattern::fmt_pattern;
use crate::spaces::{fmt_spaces, INDENT};
use crate::Buf;
use roc_parse::ast::{
AbilityMember, Defs, Expr, ExtractSpaces, Pattern, TypeAnnotation, TypeDef, TypeHeader,
AbilityMember, Defs, Expr, ExtractSpaces, Pattern, Spaces, TypeAnnotation, TypeDef, TypeHeader,
ValueDef,
};
use roc_region::all::Loc;
@ -135,12 +135,20 @@ impl<'a> Formattable for TypeDef<'a> {
if !self.is_multiline() {
debug_assert_eq!(members.len(), 1);
buf.push_str(" ");
members[0].format(buf, indent + INDENT);
members[0].format_with_options(
buf,
Parens::NotNeeded,
Newlines::No,
indent + INDENT,
);
} else {
for demand in members.iter() {
buf.newline();
buf.indent(indent + INDENT);
demand.format(buf, indent + INDENT);
for member in members.iter() {
member.format_with_options(
buf,
Parens::NotNeeded,
Newlines::Yes,
indent + INDENT,
);
}
}
}
@ -408,8 +416,18 @@ impl<'a> Formattable for AbilityMember<'a> {
self.name.value.is_multiline() || self.typ.is_multiline()
}
fn format<'buf>(&self, buf: &mut Buf<'buf>, indent: u16) {
buf.push_str(self.name.value.extract_spaces().item);
fn format_with_options<'buf>(
&self,
buf: &mut Buf<'buf>,
_parens: Parens,
_newlines: Newlines,
indent: u16,
) {
let Spaces { before, item, .. } = self.name.value.extract_spaces();
fmt_spaces(buf, before.iter(), indent);
buf.indent(indent);
buf.push_str(item);
buf.spaces(1);
buf.push(':');
buf.spaces(1);

View file

@ -1382,9 +1382,9 @@ fn sub_expr_requests_parens(expr: &Expr<'_>) -> bool {
| BinOp::LessThanOrEq
| BinOp::GreaterThanOrEq
| BinOp::And
| BinOp::Or => true,
BinOp::Pizza
| BinOp::Assignment
| BinOp::Or
| BinOp::Pizza => true,
BinOp::Assignment
| BinOp::IsAliasType
| BinOp::IsOpaqueType
| BinOp::Backpassing => false,

View file

@ -1,7 +1,7 @@
use crate::annotation::{Formattable, Newlines};
use crate::collection::{fmt_collection, Braces};
use crate::expr::fmt_str_literal;
use crate::spaces::{fmt_default_spaces, fmt_spaces, INDENT};
use crate::spaces::{fmt_comments_only, fmt_default_spaces, fmt_spaces, NewlineAt, INDENT};
use crate::Buf;
use roc_parse::ast::{Collection, Module, Spaced};
use roc_parse::header::{
@ -31,6 +31,8 @@ pub fn fmt_module<'a>(buf: &mut Buf<'_>, module: &'a Module<'a>) {
pub fn fmt_interface_header<'a, 'buf>(buf: &mut Buf<'buf>, header: &'a InterfaceHeader<'a>) {
let indent = INDENT;
fmt_comments_only(buf, header.before_header.iter(), NewlineAt::Bottom, indent);
buf.indent(0);
buf.push_str("interface");
@ -56,6 +58,8 @@ pub fn fmt_interface_header<'a, 'buf>(buf: &mut Buf<'buf>, header: &'a Interface
pub fn fmt_hosted_header<'a, 'buf>(buf: &mut Buf<'buf>, header: &'a HostedHeader<'a>) {
let indent = INDENT;
fmt_comments_only(buf, header.before_header.iter(), NewlineAt::Bottom, indent);
buf.indent(0);
buf.push_str("hosted");
@ -94,6 +98,9 @@ pub fn fmt_hosted_header<'a, 'buf>(buf: &mut Buf<'buf>, header: &'a HostedHeader
pub fn fmt_app_header<'a, 'buf>(buf: &mut Buf<'buf>, header: &'a AppHeader<'a>) {
let indent = INDENT;
fmt_comments_only(buf, header.before_header.iter(), NewlineAt::Bottom, indent);
buf.indent(0);
buf.push_str("app");
@ -130,6 +137,8 @@ pub fn fmt_app_header<'a, 'buf>(buf: &mut Buf<'buf>, header: &'a AppHeader<'a>)
pub fn fmt_platform_header<'a, 'buf>(buf: &mut Buf<'buf>, header: &'a PlatformHeader<'a>) {
let indent = INDENT;
fmt_comments_only(buf, header.before_header.iter(), NewlineAt::Bottom, indent);
buf.indent(0);
buf.push_str("platform");

View file

@ -5582,6 +5582,70 @@ mod test_fmt {
));
}
#[test]
fn format_nested_pipeline() {
expr_formats_same(indoc!(
r#"
(a |> b) |> c
"#
));
expr_formats_same(indoc!(
r#"
a |> b |> c
"#
));
}
#[test]
fn ability_member_doc_comments() {
module_formats_same(indoc!(
r#"
interface Foo exposes [] imports []
A has
## This is member ab
ab : a -> a | a has A
## This is member de
de : a -> a | a has A
f = g
"#
));
}
#[test]
fn leading_comments_preserved() {
module_formats_same(indoc!(
r#"
# hello world
interface Foo
exposes []
imports []
"#
));
module_formats_same(indoc!(
r#"
# hello world
app "test" packages {} imports [] provides [] to "./platform"
"#
));
module_formats_same(indoc!(
r#"
# hello world
platform "hello-world"
requires {} { main : Str }
exposes []
packages {}
imports []
provides [mainForHost]
"#
));
}
// this is a parse error atm
// #[test]
// fn multiline_apply() {

View file

@ -714,7 +714,7 @@ pub fn construct_optimization_passes<'a>(
OptLevel::Optimize => {
pmb.set_optimization_level(OptimizationLevel::Aggressive);
// this threshold seems to do what we want
pmb.set_inliner_with_threshold(275);
pmb.set_inliner_with_threshold(750);
}
}
@ -6448,9 +6448,23 @@ fn run_low_level<'a, 'ctx, 'env>(
let (lhs_arg, lhs_layout) = load_symbol_and_layout(scope, &args[0]);
let (rhs_arg, rhs_layout) = load_symbol_and_layout(scope, &args[1]);
debug_assert_eq!(lhs_layout, rhs_layout);
let int_width = intwidth_from_layout(*lhs_layout);
debug_assert_eq!(rhs_layout, &Layout::Builtin(Builtin::Int(IntWidth::U8)));
let rhs_arg = if rhs_layout != lhs_layout {
// LLVM shift intrinsics expect the left and right sides to have the same type, so
// here we cast up `rhs` to the lhs type. Since the rhs was checked to be a U8,
// this cast isn't lossy.
let rhs_arg = env.builder.build_int_cast(
rhs_arg.into_int_value(),
lhs_arg.get_type().into_int_type(),
"cast_for_shift",
);
rhs_arg.into()
} else {
rhs_arg
};
build_int_binop(
env,
parent,
@ -7921,11 +7935,10 @@ fn int_abs_with_overflow<'a, 'ctx, 'env>(
// (xor arg shifted) - shifted
let bd = env.builder;
let ctx = env.context;
let shifted_name = "abs_shift_right";
let shifted_alloca = {
let bits_to_shift = int_type.get_bit_width() as u64 - 1;
let shift_val = ctx.i64_type().const_int(bits_to_shift, false);
let shift_val = int_type.const_int(bits_to_shift, false);
let shifted = bd.build_right_shift(arg, shift_val, true, shifted_name);
let alloca = bd.build_alloca(int_type, "#int_abs_help");

View file

@ -1686,7 +1686,7 @@ impl<'a> LowLevelCall<'a> {
let bit_width = 8 * self
.ret_layout
.stack_size(backend.env.layout_interner, TARGET_INFO);
if bit_width < 32 && symbol_is_signed_int(backend, bits) {
if bit_width < 32 && symbol_is_signed_int(backend, num) {
let mask = (1 << bit_width) - 1;
backend

View file

@ -22,6 +22,7 @@ const MODULES: &[(ModuleId, &str)] = &[
(ModuleId::BOX, "Box.roc"),
(ModuleId::ENCODE, "Encode.roc"),
(ModuleId::DECODE, "Decode.roc"),
(ModuleId::HASH, "Hash.roc"),
(ModuleId::JSON, "Json.roc"),
];

View file

@ -42,7 +42,7 @@ use roc_parse::header::{ExposedName, ImportsEntry, PackageEntry, PlatformHeader,
use roc_parse::header::{HeaderFor, ModuleNameEnum, PackageName};
use roc_parse::ident::UppercaseIdent;
use roc_parse::module::module_defs;
use roc_parse::parser::{FileError, Parser, SyntaxError};
use roc_parse::parser::{FileError, Parser, SourceError, SyntaxError};
use roc_region::all::{LineInfo, Loc, Region};
use roc_reporting::report::{Annotation, RenderTarget};
use roc_solve::module::{extract_module_owned_implementations, Solved, SolvedModule};
@ -61,8 +61,8 @@ use std::str::from_utf8_unchecked;
use std::sync::Arc;
use std::{env, fs};
use crate::work::Dependencies;
pub use crate::work::Phase;
use crate::work::{DepCycle, Dependencies};
#[cfg(target_family = "wasm")]
use crate::wasm_instant::{Duration, Instant};
@ -177,6 +177,7 @@ impl Default for ModuleCache<'_> {
BOX,
ENCODE,
DECODE,
HASH,
JSON,
}
@ -827,6 +828,8 @@ enum Msg<'a> {
filename: PathBuf,
error: io::ErrorKind,
},
IncorrectModuleName(FileError<'a, IncorrectModuleName<'a>>),
}
#[derive(Debug)]
@ -1124,6 +1127,13 @@ enum WorkerMsg {
TaskAdded,
}
#[derive(Debug)]
pub struct IncorrectModuleName<'a> {
pub module_id: ModuleId,
pub found: Loc<PQModuleName<'a>>,
pub expected: PQModuleName<'a>,
}
#[derive(Debug)]
pub enum LoadingProblem<'a> {
FileProblem {
@ -1139,6 +1149,9 @@ pub enum LoadingProblem<'a> {
/// a formatted report
FormattedReport(String),
ImportCycle(PathBuf, Vec<ModuleId>),
IncorrectModuleName(FileError<'a, IncorrectModuleName<'a>>),
}
pub enum Phases {
@ -1233,6 +1246,7 @@ impl<'a> LoadStart<'a> {
filename,
true,
None,
None,
Arc::clone(&arc_modules),
Arc::clone(&ident_ids_by_module),
root_start_time,
@ -1288,6 +1302,46 @@ impl<'a> LoadStart<'a> {
let buf = to_file_problem_report(&filename, error);
return Err(LoadingProblem::FormattedReport(buf));
}
Err(LoadingProblem::ImportCycle(filename, cycle)) => {
let module_ids = Arc::try_unwrap(arc_modules)
.unwrap_or_else(|_| {
panic!("There were still outstanding Arc references to module_ids")
})
.into_inner()
.into_module_ids();
let root_exposed_ident_ids = IdentIds::exposed_builtins(0);
let buf = to_import_cycle_report(
module_ids,
root_exposed_ident_ids,
cycle,
filename,
render,
);
return Err(LoadingProblem::FormattedReport(buf));
}
Err(LoadingProblem::IncorrectModuleName(FileError {
problem: SourceError { problem, bytes },
filename,
})) => {
let module_ids = Arc::try_unwrap(arc_modules)
.unwrap_or_else(|_| {
panic!("There were still outstanding Arc references to module_ids")
})
.into_inner()
.into_module_ids();
let root_exposed_ident_ids = IdentIds::exposed_builtins(0);
let buf = to_incorrect_module_name_report(
module_ids,
root_exposed_ident_ids,
problem,
filename,
bytes,
render,
);
return Err(LoadingProblem::FormattedReport(buf));
}
Err(e) => return Err(e),
}
};
@ -1605,6 +1659,21 @@ fn state_thread_step<'a>(
);
Err(LoadingProblem::FormattedReport(buf))
}
Msg::IncorrectModuleName(FileError {
problem: SourceError { problem, bytes },
filename,
}) => {
let module_ids = (*state.arc_modules).lock().clone().into_module_ids();
let buf = to_incorrect_module_name_report(
module_ids,
state.constrained_ident_ids,
problem,
filename,
bytes,
state.render,
);
Err(LoadingProblem::FormattedReport(buf))
}
msg => {
// This is where most of the main thread's work gets done.
// Everything up to this point has been setting up the threading
@ -1644,6 +1713,36 @@ fn state_thread_step<'a>(
);
Err(LoadingProblem::FormattedReport(buf))
}
Err(LoadingProblem::ImportCycle(filename, cycle)) => {
let module_ids = arc_modules.lock().clone().into_module_ids();
let root_exposed_ident_ids = IdentIds::exposed_builtins(0);
let buf = to_import_cycle_report(
module_ids,
root_exposed_ident_ids,
cycle,
filename,
render,
);
return Err(LoadingProblem::FormattedReport(buf));
}
Err(LoadingProblem::IncorrectModuleName(FileError {
problem: SourceError { problem, bytes },
filename,
})) => {
let module_ids = arc_modules.lock().clone().into_module_ids();
let root_exposed_ident_ids = IdentIds::exposed_builtins(0);
let buf = to_incorrect_module_name_report(
module_ids,
root_exposed_ident_ids,
problem,
filename,
bytes,
render,
);
return Err(LoadingProblem::FormattedReport(buf));
}
Err(e) => Err(e),
}
}
@ -1869,6 +1968,9 @@ fn worker_task_step<'a>(
.send(Msg::FailedToReadFile { filename, error })
.unwrap();
}
Err(LoadingProblem::IncorrectModuleName(err)) => {
msg_tx.send(Msg::IncorrectModuleName(err)).unwrap();
}
Err(other) => {
return Err(other);
}
@ -1933,6 +2035,9 @@ fn worker_task<'a>(
.send(Msg::FailedToReadFile { filename, error })
.unwrap();
}
Err(LoadingProblem::IncorrectModuleName(err)) => {
msg_tx.send(Msg::IncorrectModuleName(err)).unwrap();
}
Err(other) => {
return Err(other);
}
@ -2127,26 +2232,18 @@ fn update<'a>(
// add the prelude
let mut header = header;
if ![ModuleId::RESULT, ModuleId::BOOL].contains(&header.module_id) {
extend_header_with_builtin(&mut header, ModuleId::RESULT);
}
if ![ModuleId::NUM, ModuleId::BOOL, ModuleId::RESULT].contains(&header.module_id) {
extend_header_with_builtin(&mut header, ModuleId::NUM);
}
if ![ModuleId::BOOL].contains(&header.module_id) {
extend_header_with_builtin(&mut header, ModuleId::BOOL);
}
if !header.module_id.is_builtin() {
extend_header_with_builtin(&mut header, ModuleId::BOX);
extend_header_with_builtin(&mut header, ModuleId::NUM);
extend_header_with_builtin(&mut header, ModuleId::BOOL);
extend_header_with_builtin(&mut header, ModuleId::STR);
extend_header_with_builtin(&mut header, ModuleId::LIST);
extend_header_with_builtin(&mut header, ModuleId::RESULT);
extend_header_with_builtin(&mut header, ModuleId::DICT);
extend_header_with_builtin(&mut header, ModuleId::SET);
extend_header_with_builtin(&mut header, ModuleId::LIST);
extend_header_with_builtin(&mut header, ModuleId::BOX);
extend_header_with_builtin(&mut header, ModuleId::ENCODE);
extend_header_with_builtin(&mut header, ModuleId::DECODE);
extend_header_with_builtin(&mut header, ModuleId::HASH);
}
state
@ -2161,11 +2258,23 @@ fn update<'a>(
.map(|x| *x.as_inner()),
);
work.extend(state.dependencies.add_module(
let added_deps_result = state.dependencies.add_module(
header.module_id,
&header.package_qualified_imported_modules,
state.exec_mode.goal_phase(),
);
let new_work = match added_deps_result {
Ok(work) => work,
Err(DepCycle { cycle }) => {
return Err(LoadingProblem::ImportCycle(
header.module_path.clone(),
cycle,
));
}
};
work.extend(new_work);
state.module_cache.headers.insert(header.module_id, header);
@ -2749,6 +2858,9 @@ fn update<'a>(
Msg::FailedToReadFile { .. } => {
unreachable!();
}
Msg::IncorrectModuleName(..) => {
internal_error!();
}
}
}
@ -3105,13 +3217,14 @@ fn load_builtin_module<'a>(
let (info, parse_state) = load_builtin_module_help(arena, module_name, src_bytes);
send_header(
let (module_id, _, header) = build_header(
info,
parse_state,
module_ids,
ident_ids_by_module,
module_timing,
)
);
(module_id, Msg::Header(header))
}
/// Load a module by its module name, rather than by its filename
@ -3164,16 +3277,18 @@ fn load_module<'a>(
"Box", ModuleId::BOX
"Encode", ModuleId::ENCODE
"Decode", ModuleId::DECODE
"Hash", ModuleId::HASH
"Json", ModuleId::JSON
}
let (filename, opt_shorthand) = module_name_to_path(src_dir, module_name, arc_shorthands);
let (filename, opt_shorthand) = module_name_to_path(src_dir, &module_name, arc_shorthands);
load_filename(
arena,
filename,
false,
opt_shorthand,
Some(module_name),
module_ids,
ident_ids_by_module,
module_start_time,
@ -3182,7 +3297,7 @@ fn load_module<'a>(
fn module_name_to_path<'a>(
src_dir: &Path,
module_name: PQModuleName<'a>,
module_name: &PQModuleName<'a>,
arc_shorthands: Arc<Mutex<MutMap<&'a str, PackageName<'a>>>>,
) -> (PathBuf, Option<&'a str>) {
let mut filename;
@ -3199,7 +3314,7 @@ fn module_name_to_path<'a>(
}
}
PQModuleName::Qualified(shorthand, name) => {
opt_shorthand = Some(shorthand);
opt_shorthand = Some(*shorthand);
let shorthands = arc_shorthands.lock();
match shorthands.get(shorthand) {
@ -3255,6 +3370,45 @@ fn find_task<T>(local: &Worker<T>, global: &Injector<T>, stealers: &[Stealer<T>]
})
}
fn verify_interface_matches_file_path<'a>(
interface_name: Loc<roc_parse::header::ModuleName<'a>>,
path: &Path,
state: &roc_parse::state::State<'a>,
) -> Result<(), LoadingProblem<'a>> {
let module_parts = interface_name.value.as_str().split(MODULE_SEPARATOR).rev();
let mut is_mismatched = false;
let mut opt_path = Some(path);
for part in module_parts {
match opt_path.and_then(|path| path.file_stem().map(|fi| (path, fi))) {
None => {
is_mismatched = true;
break;
}
Some((path, fi)) => {
if fi != part {
is_mismatched = true;
break;
}
opt_path = path.parent();
}
}
}
if !is_mismatched {
return Ok(());
}
use roc_parse::parser::EHeader;
let syntax_problem =
SyntaxError::Header(EHeader::InconsistentModuleName(interface_name.region));
let problem = LoadingProblem::ParsingFailed(FileError {
problem: SourceError::new(syntax_problem, state),
filename: path.to_path_buf(),
});
Err(problem)
}
#[allow(clippy::too_many_arguments)]
fn parse_header<'a>(
arena: &'a Bump,
@ -3262,6 +3416,7 @@ fn parse_header<'a>(
filename: PathBuf,
is_root_module: bool,
opt_shorthand: Option<&'a str>,
opt_expected_module_name: Option<PackageQualified<'a, ModuleName>>,
module_ids: Arc<Mutex<PackageModuleIds<'a>>>,
ident_ids_by_module: SharedIdentIdsByModule,
src_bytes: &'a [u8],
@ -3280,9 +3435,13 @@ fn parse_header<'a>(
match parsed {
Ok((ast::Module::Interface { header }, parse_state)) => {
verify_interface_matches_file_path(header.name, &filename, &parse_state)?;
let header_name_region = header.name.region;
let info = HeaderInfo {
loc_name: Loc {
region: header.name.region,
region: header_name_region,
value: ModuleNameEnum::Interface(header.name.value),
},
filename,
@ -3294,13 +3453,33 @@ fn parse_header<'a>(
extra: HeaderFor::Interface,
};
Ok(send_header(
let (module_id, module_name, header) = build_header(
info,
parse_state,
parse_state.clone(),
module_ids,
ident_ids_by_module,
module_timing,
))
);
if let Some(expected_module_name) = opt_expected_module_name {
if expected_module_name != module_name {
let problem = SourceError::new(
IncorrectModuleName {
module_id,
found: Loc::at(header_name_region, module_name),
expected: expected_module_name,
},
&parse_state,
);
let problem = LoadingProblem::IncorrectModuleName(FileError {
problem,
filename: header.module_path,
});
return Err(problem);
}
}
Ok((module_id, Msg::Header(header)))
}
Ok((ast::Module::Hosted { header }, parse_state)) => {
let info = HeaderInfo {
@ -3320,13 +3499,15 @@ fn parse_header<'a>(
},
};
Ok(send_header(
let (module_id, _, header) = build_header(
info,
parse_state,
module_ids,
ident_ids_by_module,
module_timing,
))
);
Ok((module_id, Msg::Header(header)))
}
Ok((ast::Module::App { header }, parse_state)) => {
let mut app_file_dir = filename.clone();
@ -3364,13 +3545,14 @@ fn parse_header<'a>(
},
};
let (module_id, app_module_header_msg) = send_header(
let (module_id, _, resolved_header) = build_header(
info,
parse_state,
module_ids.clone(),
ident_ids_by_module.clone(),
module_timing,
);
let app_module_header_msg = Msg::Header(resolved_header);
match header.to.value {
To::ExistingPackage(existing_package) => {
@ -3450,6 +3632,7 @@ fn load_filename<'a>(
filename: PathBuf,
is_root_module: bool,
opt_shorthand: Option<&'a str>,
opt_expected_module_name: Option<PackageQualified<'a, ModuleName>>,
module_ids: Arc<Mutex<PackageModuleIds<'a>>>,
ident_ids_by_module: SharedIdentIdsByModule,
module_start_time: Instant,
@ -3465,6 +3648,7 @@ fn load_filename<'a>(
filename,
is_root_module,
opt_shorthand,
opt_expected_module_name,
module_ids,
ident_ids_by_module,
arena.alloc(bytes),
@ -3497,6 +3681,7 @@ fn load_from_str<'a>(
filename,
false,
None,
None,
module_ids,
ident_ids_by_module,
src.as_bytes(),
@ -3517,13 +3702,13 @@ struct HeaderInfo<'a> {
}
#[allow(clippy::too_many_arguments)]
fn send_header<'a>(
fn build_header<'a>(
info: HeaderInfo<'a>,
parse_state: roc_parse::state::State<'a>,
module_ids: Arc<Mutex<PackageModuleIds<'a>>>,
ident_ids_by_module: SharedIdentIdsByModule,
module_timing: ModuleTiming,
) -> (ModuleId, Msg<'a>) {
) -> (ModuleId, PQModuleName<'a>, ModuleHeader<'a>) {
use ModuleNameEnum::*;
let HeaderInfo {
@ -3571,13 +3756,14 @@ fn send_header<'a>(
let mut scope: MutMap<Ident, (Symbol, Region)> =
HashMap::with_capacity_and_hasher(scope_size, default_hasher());
let home: ModuleId;
let name: PQModuleName;
let ident_ids = {
// Lock just long enough to perform the minimal operations necessary.
let mut module_ids = (*module_ids).lock();
let mut ident_ids_by_module = (*ident_ids_by_module).lock();
let name = match opt_shorthand {
name = match opt_shorthand {
Some(shorthand) => PQModuleName::Qualified(shorthand, declared_name),
None => PQModuleName::Unqualified(declared_name),
};
@ -3706,7 +3892,8 @@ fn send_header<'a>(
(
home,
Msg::Header(ModuleHeader {
name,
ModuleHeader {
module_id: home,
module_path: filename,
is_root_module,
@ -3722,7 +3909,7 @@ fn send_header<'a>(
symbols_from_requires: Vec::new(),
header_for: extra,
module_timing,
}),
},
)
}
@ -4575,7 +4762,11 @@ fn canonicalize_and_constrain<'a>(
Vacant(vacant) => {
let should_include_builtin = matches!(
name.module_id(),
ModuleId::ENCODE | ModuleId::DECODE | ModuleId::DICT | ModuleId::SET
ModuleId::ENCODE
| ModuleId::DECODE
| ModuleId::DICT
| ModuleId::SET
| ModuleId::HASH
);
if !name.is_builtin() || should_include_builtin {
@ -5544,6 +5735,114 @@ fn to_file_problem_report(filename: &Path, error: io::ErrorKind) -> String {
buf
}
fn to_import_cycle_report(
module_ids: ModuleIds,
all_ident_ids: IdentIdsByModule,
import_cycle: Vec<ModuleId>,
filename: PathBuf,
render: RenderTarget,
) -> String {
use roc_reporting::report::{Report, RocDocAllocator, Severity, DEFAULT_PALETTE};
use ven_pretty::DocAllocator;
// import_cycle looks like CycleModule, Import1, ..., ImportN, CycleModule
// In a self-referential case, it just looks like CycleModule, CycleModule.
debug_assert!(import_cycle.len() >= 2);
let source_of_cycle = import_cycle.first().unwrap();
// We won't be printing any lines for this report, so this is okay.
// TODO: it would be nice to show how each module imports another in the cycle.
let src_lines = &[];
let interns = Interns {
module_ids,
all_ident_ids,
};
let alloc = RocDocAllocator::new(src_lines, *source_of_cycle, &interns);
let doc = alloc.stack([
alloc.concat([
alloc.reflow("I can't compile "),
alloc.module(*source_of_cycle),
alloc.reflow(
" because it depends on itself through the following chain of module imports:",
),
]),
roc_reporting::report::cycle(
&alloc,
4,
alloc.module(*source_of_cycle),
import_cycle
.into_iter()
.skip(1)
.map(|module| alloc.module(module))
.collect(),
),
alloc.reflow("Cyclic dependencies are not allowed in Roc! Can you restructure a module in this import chain so that it doesn't have to depend on itself?")
]);
let report = Report {
filename,
doc,
title: "IMPORT CYCLE".to_string(),
severity: Severity::RuntimeError,
};
let mut buf = String::new();
let palette = DEFAULT_PALETTE;
report.render(render, &mut buf, &alloc, &palette);
buf
}
fn to_incorrect_module_name_report<'a>(
module_ids: ModuleIds,
all_ident_ids: IdentIdsByModule,
problem: IncorrectModuleName<'a>,
filename: PathBuf,
src: &'a [u8],
render: RenderTarget,
) -> String {
use roc_reporting::report::{Report, RocDocAllocator, Severity, DEFAULT_PALETTE};
use ven_pretty::DocAllocator;
let IncorrectModuleName {
module_id,
found,
expected,
} = problem;
// SAFETY: if the module was not UTF-8, that would be reported as a parsing problem, rather
// than an incorrect module name problem (the latter can happen only after parsing).
let src = unsafe { from_utf8_unchecked(src) };
let src_lines = src.lines().collect::<Vec<_>>();
let lines = LineInfo::new(src);
let interns = Interns {
module_ids,
all_ident_ids,
};
let alloc = RocDocAllocator::new(&src_lines, module_id, &interns);
let doc = alloc.stack([
alloc.reflow("This module has a different name than I expected:"),
alloc.region(lines.convert_region(found.region)),
alloc.reflow("Based on the nesting and use of this module, I expect it to have name"),
alloc.pq_module_name(expected).indent(4),
]);
let report = Report {
filename,
doc,
title: "INCORRECT MODULE NAME".to_string(),
severity: Severity::RuntimeError,
};
let mut buf = String::new();
let palette = DEFAULT_PALETTE;
report.render(render, &mut buf, &alloc, &palette);
buf
}
fn to_parse_problem_report<'a>(
problem: FileError<'a, SyntaxError<'a>>,
mut module_ids: ModuleIds,

View file

@ -1,4 +1,7 @@
use roc_collections::all::{MutMap, MutSet};
use roc_collections::{
all::{MutMap, MutSet},
VecMap,
};
use roc_module::symbol::{ModuleId, PackageQualified};
use std::collections::hash_map::Entry;
@ -103,6 +106,10 @@ pub struct Dependencies<'a> {
make_specializations_dependents: MakeSpecializationsDependents,
}
pub struct DepCycle {
pub cycle: Vec<ModuleId>,
}
impl<'a> Dependencies<'a> {
pub fn new(goal_phase: Phase) -> Self {
let mut deps = Self {
@ -127,12 +134,26 @@ impl<'a> Dependencies<'a> {
module_id: ModuleId,
dependencies: &MutSet<PackageQualified<'a, ModuleId>>,
goal_phase: Phase,
) -> MutSet<(ModuleId, Phase)> {
) -> Result<MutSet<(ModuleId, Phase)>, DepCycle> {
use Phase::*;
let mut output = MutSet::default();
for dep in dependencies.iter() {
// Do a BFS to check if we have an import cycle; if we do, calculate the cycle and
// report the error. Although the worst case here is that we do a quadratic amount of
// work for all modules added in a batch compilation, in practice, most dependencies
// inserted here have not been seen by [Dependencies] yet, so their import chain is
// size 0.
if self.has_import_dependency(*dep.as_inner(), module_id) {
let mut rev_cycle = self.calculate_reverse_import_path(*dep.as_inner(), module_id);
rev_cycle.push(module_id);
rev_cycle.reverse();
let cycle = rev_cycle;
return Err(DepCycle { cycle });
}
let has_package_dependency = self.add_package_dependency(dep, Phase::LoadHeader);
let dep = *dep.as_inner();
@ -183,7 +204,59 @@ impl<'a> Dependencies<'a> {
self.add_to_status_for_all_phases(module_id, goal_phase);
output
Ok(output)
}
fn has_import_dependency(&self, module_id: ModuleId, target: ModuleId) -> bool {
if module_id.is_builtin() {
return false;
}
let mut stack = vec![module_id];
while let Some(module) = stack.pop() {
if module.is_builtin() {
continue;
}
if module == target {
return true;
}
if let Some(dependencies) = self.make_specializations_dependents.0.get(&module) {
stack.extend(dependencies.succ.iter());
}
}
false
}
fn calculate_reverse_import_path(
&self,
module_id: ModuleId,
target: ModuleId,
) -> Vec<ModuleId> {
let mut stack = vec![module_id];
let mut backlinks = VecMap::with_capacity(16);
let mut found_import = false;
while let Some(module) = stack.pop() {
if module == target {
found_import = true;
break;
}
if let Some(dependencies) = self.make_specializations_dependents.0.get(&module) {
for import in dependencies.succ.iter() {
backlinks.insert(*import, module);
stack.push(*import);
}
}
}
if !found_import {
roc_error_macros::internal_error!("calculate_import_path should only be called when an import path is known to exist!");
}
let mut source = target;
let mut rev_path = vec![source];
while let Some(&parent) = backlinks.get(&source) {
rev_path.push(parent);
source = parent;
}
rev_path
}
/// Adds a status for the given module for exactly one phase.

View file

@ -318,7 +318,7 @@ fn import_transitive_alias() {
),
),
(
"Main",
"Other",
indoc!(
r#"
interface Other exposes [empty] imports [RBTree]
@ -909,3 +909,164 @@ fn import_builtin_in_platform_and_check_app() {
let result = multiple_modules("import_builtin_in_platform_and_check_app", modules);
assert!(result.is_ok(), "should check");
}
#[test]
fn module_doesnt_match_file_path() {
let modules = vec![(
"Age",
indoc!(
r#"
interface NotAge exposes [Age] imports []
Age := U32
"#
),
)];
let err = multiple_modules("module_doesnt_match_file_path", modules).unwrap_err();
assert_eq!(
err,
indoc!(
r#"
WEIRD MODULE NAME tmp/module_doesnt_match_file_path/Age
This module name does not correspond with the file path it is defined
in:
1 interface NotAge exposes [Age] imports []
^^^^^^
Module names must correspond with the file paths they are defined in.
For example, I expect to see BigNum defined in BigNum.roc, or Math.Sin
defined in Math/Sin.roc."#
),
"\n{}",
err
);
}
#[test]
fn module_cyclic_import_itself() {
let modules = vec![(
"Age",
indoc!(
r#"
interface Age exposes [] imports [Age]
"#
),
)];
let err = multiple_modules("module_cyclic_import_itself", modules).unwrap_err();
assert_eq!(
err,
indoc!(
r#"
IMPORT CYCLE tmp/module_cyclic_import_itself/Age
I can't compile Age because it depends on itself through the following
chain of module imports:
Age
Age
Cyclic dependencies are not allowed in Roc! Can you restructure a
module in this import chain so that it doesn't have to depend on
itself?"#
),
"\n{}",
err
);
}
#[test]
fn module_cyclic_import_transitive() {
let modules = vec![
(
"Age",
indoc!(
r#"
interface Age exposes [] imports [Person]
"#
),
),
(
"Person",
indoc!(
r#"
interface Person exposes [] imports [Age]
"#
),
),
];
let err = multiple_modules("module_cyclic_import_transitive", modules).unwrap_err();
assert_eq!(
err,
indoc!(
r#"
IMPORT CYCLE tmp/module_cyclic_import_transitive/Age.roc
I can't compile Age because it depends on itself through the following
chain of module imports:
Age
Person
Age
Cyclic dependencies are not allowed in Roc! Can you restructure a
module in this import chain so that it doesn't have to depend on
itself?"#
),
"\n{}",
err
);
}
#[test]
fn nested_module_has_incorrect_name() {
let modules = vec![
(
"Dep/Foo.roc",
indoc!(
r#"
interface Foo exposes [] imports []
"#
),
),
(
"I.roc",
indoc!(
r#"
interface I exposes [] imports [Dep.Foo]
"#
),
),
];
let err = multiple_modules("nested_module_has_incorrect_name", modules).unwrap_err();
assert_eq!(
err,
indoc!(
r#"
INCORRECT MODULE NAME tmp/nested_module_has_incorrect_name/Dep/Foo.roc
This module has a different name than I expected:
1 interface Foo exposes [] imports []
^^^
Based on the nesting and use of this module, I expect it to have name
Dep.Foo"#
),
"\n{}",
err
);
}

View file

@ -85,6 +85,7 @@ impl ModuleName {
pub const BOX: &'static str = "Box";
pub const ENCODE: &'static str = "Encode";
pub const DECODE: &'static str = "Decode";
pub const HASH: &'static str = "Hash";
pub const JSON: &'static str = "Json";
pub fn as_str(&self) -> &str {

View file

@ -50,6 +50,7 @@ const PRETTY_PRINT_DEBUG_SYMBOLS: bool = true;
pub const DERIVABLE_ABILITIES: &[(Symbol, &[Symbol])] = &[
(Symbol::ENCODE_ENCODING, &[Symbol::ENCODE_TO_ENCODER]),
(Symbol::DECODE_DECODING, &[Symbol::DECODE_DECODER]),
(Symbol::HASH_HASH_ABILITY, &[Symbol::HASH_HASH]),
];
/// In Debug builds only, Symbol has a name() method that lets
@ -425,7 +426,7 @@ impl fmt::Debug for ModuleId {
/// 4. throw away short names. stash the module id in the can env under the resolved module name
/// 5. test:
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PackageQualified<'a, T> {
Unqualified(T),
Qualified(&'a str, T),
@ -1194,39 +1195,41 @@ define_builtins! {
110 NUM_MAX_U64: "maxU64"
111 NUM_MIN_I128: "minI128"
112 NUM_MAX_I128: "maxI128"
113 NUM_TO_I8: "toI8"
114 NUM_TO_I8_CHECKED: "toI8Checked"
115 NUM_TO_I16: "toI16"
116 NUM_TO_I16_CHECKED: "toI16Checked"
117 NUM_TO_I32: "toI32"
118 NUM_TO_I32_CHECKED: "toI32Checked"
119 NUM_TO_I64: "toI64"
120 NUM_TO_I64_CHECKED: "toI64Checked"
121 NUM_TO_I128: "toI128"
122 NUM_TO_I128_CHECKED: "toI128Checked"
123 NUM_TO_U8: "toU8"
124 NUM_TO_U8_CHECKED: "toU8Checked"
125 NUM_TO_U16: "toU16"
126 NUM_TO_U16_CHECKED: "toU16Checked"
127 NUM_TO_U32: "toU32"
128 NUM_TO_U32_CHECKED: "toU32Checked"
129 NUM_TO_U64: "toU64"
130 NUM_TO_U64_CHECKED: "toU64Checked"
131 NUM_TO_U128: "toU128"
132 NUM_TO_U128_CHECKED: "toU128Checked"
133 NUM_TO_NAT: "toNat"
134 NUM_TO_NAT_CHECKED: "toNatChecked"
135 NUM_TO_F32: "toF32"
136 NUM_TO_F32_CHECKED: "toF32Checked"
137 NUM_TO_F64: "toF64"
138 NUM_TO_F64_CHECKED: "toF64Checked"
139 NUM_MAX_F64: "maxF64"
140 NUM_MIN_F64: "minF64"
141 NUM_ADD_CHECKED_LOWLEVEL: "addCheckedLowlevel"
142 NUM_SUB_CHECKED_LOWLEVEL: "subCheckedLowlevel"
143 NUM_MUL_CHECKED_LOWLEVEL: "mulCheckedLowlevel"
144 NUM_BYTES_TO_U16_LOWLEVEL: "bytesToU16Lowlevel"
145 NUM_BYTES_TO_U32_LOWLEVEL: "bytesToU32Lowlevel"
113 NUM_MIN_U128: "minU128"
114 NUM_MAX_U128: "maxU128"
115 NUM_TO_I8: "toI8"
116 NUM_TO_I8_CHECKED: "toI8Checked"
117 NUM_TO_I16: "toI16"
118 NUM_TO_I16_CHECKED: "toI16Checked"
119 NUM_TO_I32: "toI32"
120 NUM_TO_I32_CHECKED: "toI32Checked"
121 NUM_TO_I64: "toI64"
122 NUM_TO_I64_CHECKED: "toI64Checked"
123 NUM_TO_I128: "toI128"
124 NUM_TO_I128_CHECKED: "toI128Checked"
125 NUM_TO_U8: "toU8"
126 NUM_TO_U8_CHECKED: "toU8Checked"
127 NUM_TO_U16: "toU16"
128 NUM_TO_U16_CHECKED: "toU16Checked"
129 NUM_TO_U32: "toU32"
130 NUM_TO_U32_CHECKED: "toU32Checked"
131 NUM_TO_U64: "toU64"
132 NUM_TO_U64_CHECKED: "toU64Checked"
133 NUM_TO_U128: "toU128"
134 NUM_TO_U128_CHECKED: "toU128Checked"
135 NUM_TO_NAT: "toNat"
136 NUM_TO_NAT_CHECKED: "toNatChecked"
137 NUM_TO_F32: "toF32"
138 NUM_TO_F32_CHECKED: "toF32Checked"
139 NUM_TO_F64: "toF64"
140 NUM_TO_F64_CHECKED: "toF64Checked"
141 NUM_MAX_F64: "maxF64"
142 NUM_MIN_F64: "minF64"
143 NUM_ADD_CHECKED_LOWLEVEL: "addCheckedLowlevel"
144 NUM_SUB_CHECKED_LOWLEVEL: "subCheckedLowlevel"
145 NUM_MUL_CHECKED_LOWLEVEL: "mulCheckedLowlevel"
146 NUM_BYTES_TO_U16_LOWLEVEL: "bytesToU16Lowlevel"
147 NUM_BYTES_TO_U32_LOWLEVEL: "bytesToU32Lowlevel"
}
4 BOOL: "Bool" => {
0 BOOL_BOOL: "Bool" exposed_type=true // the Bool.Bool type alias
@ -1485,9 +1488,28 @@ define_builtins! {
25 DECODE_FROM_BYTES_PARTIAL: "fromBytesPartial"
26 DECODE_FROM_BYTES: "fromBytes"
}
13 JSON: "Json" => {
13 HASH: "Hash" => {
0 HASH_HASH_ABILITY: "Hash" exposed_type=true
1 HASH_HASH: "hash"
2 HASH_HASHER: "Hasher" exposed_type=true
3 HASH_ADD_BYTES: "addBytes"
4 HASH_ADD_U8: "addU8"
5 HASH_ADD_U16: "addU16"
6 HASH_ADD_U32: "addU32"
7 HASH_ADD_U64: "addU64"
8 HASH_ADD_U128: "addU128"
9 HASH_ADD_I8: "addI8"
10 HASH_ADD_I16: "addI16"
11 HASH_ADD_I32: "addI32"
12 HASH_ADD_I64: "addI64"
13 HASH_ADD_I128: "addI128"
14 HASH_COMPLETE: "complete"
15 HASH_HASH_STR_BYTES: "hashStrBytes"
16 HASH_HASH_LIST: "hashList"
}
14 JSON: "Json" => {
0 JSON_JSON: "Json"
}
num_modules: 14 // Keep this count up to date by hand! (TODO: see the mut_map! macro for how we could determine this count correctly in the macro)
num_modules: 15 // Keep this count up to date by hand! (TODO: see the mut_map! macro for how we could determine this count correctly in the macro)
}

View file

@ -5524,8 +5524,9 @@ fn late_resolve_ability_specialization<'a>(
.expect("specialization var not derivable!");
match derive_key {
roc_derive_key::Derived::Immediate(imm) => {
// The immediate is an ability member itself, so it must be resolved!
roc_derive_key::Derived::Immediate(imm)
| roc_derive_key::Derived::SingleLambdaSetImmediate(imm) => {
// The immediate may be an ability member itself, so it must be resolved!
late_resolve_ability_specialization(env, imm, None, specialization_var)
}
roc_derive_key::Derived::Key(derive_key) => {

View file

@ -1257,8 +1257,17 @@ mod ability {
IndentLevel::Exact(wanted) if state.column() > wanted => {
// This demand is not indented correctly
let indent_difference = state.column() as i32 - wanted as i32;
// We might be trying to parse at EOF, at which case the indent level
// will be off, but there is actually nothing left.
let progress = if state.has_reached_end() {
NoProgress
} else {
MadeProgress
};
Err((
MadeProgress,
progress,
EAbility::DemandAlignment(indent_difference, state.pos()),
initial,
))

View file

@ -126,6 +126,8 @@ pub enum EHeader<'a> {
AppName(EString<'a>, Position),
PlatformName(EPackageName<'a>, Position),
IndentStart(Position),
InconsistentModuleName(Region),
}
#[derive(Debug, Clone, PartialEq, Eq)]

View file

@ -2,6 +2,8 @@ use roc_region::all::{Position, Region};
use std::fmt;
/// A position in a source file.
// NB: [Copy] is explicitly NOT derived to reduce the chance of bugs due to accidentally re-using
// parser state.
#[derive(Clone)]
pub struct State<'a> {
/// The raw input bytes from the file.

View file

@ -272,6 +272,10 @@ impl ObligationCache {
var,
)),
Symbol::HASH_HASH_ABILITY => {
Some(DeriveHash::is_derivable(self, abilities_store, subs, var))
}
_ => None,
};
@ -893,6 +897,95 @@ impl DerivableVisitor for DeriveDecoding {
}
}
struct DeriveHash;
impl DerivableVisitor for DeriveHash {
const ABILITY: Symbol = Symbol::HASH_HASH_ABILITY;
#[inline(always)]
fn is_derivable_builtin_opaque(symbol: Symbol) -> bool {
is_builtin_number_alias(symbol)
}
#[inline(always)]
fn visit_recursion(_var: Variable) -> Result<Descend, NotDerivable> {
Ok(Descend(true))
}
#[inline(always)]
fn visit_apply(var: Variable, symbol: Symbol) -> Result<Descend, NotDerivable> {
if matches!(
symbol,
Symbol::LIST_LIST | Symbol::SET_SET | Symbol::DICT_DICT | Symbol::STR_STR,
) {
Ok(Descend(true))
} else {
Err(NotDerivable {
var,
context: NotDerivableContext::NoContext,
})
}
}
#[inline(always)]
fn visit_record(
subs: &Subs,
var: Variable,
fields: RecordFields,
) -> Result<Descend, NotDerivable> {
for (field_name, _, field) in fields.iter_all() {
if subs[field].is_optional() {
return Err(NotDerivable {
var,
context: NotDerivableContext::Decode(NotDerivableDecode::OptionalRecordField(
subs[field_name].clone(),
)),
});
}
}
Ok(Descend(true))
}
#[inline(always)]
fn visit_tag_union(_var: Variable) -> Result<Descend, NotDerivable> {
Ok(Descend(true))
}
#[inline(always)]
fn visit_recursive_tag_union(_var: Variable) -> Result<Descend, NotDerivable> {
Ok(Descend(true))
}
#[inline(always)]
fn visit_function_or_tag_union(_var: Variable) -> Result<Descend, NotDerivable> {
Ok(Descend(true))
}
#[inline(always)]
fn visit_empty_record(_var: Variable) -> Result<(), NotDerivable> {
Ok(())
}
#[inline(always)]
fn visit_empty_tag_union(_var: Variable) -> Result<(), NotDerivable> {
Ok(())
}
#[inline(always)]
fn visit_alias(_var: Variable, symbol: Symbol) -> Result<Descend, NotDerivable> {
if is_builtin_number_alias(symbol) {
Ok(Descend(false))
} else {
Ok(Descend(true))
}
}
#[inline(always)]
fn visit_ranged_number(_var: Variable, _range: NumericRange) -> Result<(), NotDerivable> {
Ok(())
}
}
/// Determines what type implements an ability member of a specialized signature, given the
/// [MustImplementAbility] constraints of the signature.
pub fn type_implementing_specialization(

View file

@ -601,6 +601,7 @@ enum SpecializationTypeKey {
Opaque(Symbol),
Derived(DeriveKey),
Immediate(Symbol),
SingleLambdaSetImmediate(Symbol),
}
enum SpecializeDecision {
@ -666,6 +667,9 @@ fn make_specialization_decision<P: Phase>(
roc_derive_key::Derived::Immediate(imm) => {
SpecializeDecision::Specialize(Immediate(imm))
}
roc_derive_key::Derived::SingleLambdaSetImmediate(imm) => {
SpecializeDecision::Specialize(SingleLambdaSetImmediate(imm))
}
roc_derive_key::Derived::Key(derive_key) => {
SpecializeDecision::Specialize(Derived(derive_key))
}
@ -780,10 +784,37 @@ fn get_specialization_lambda_set_ambient_function<P: Phase>(
//
// THEORY: if something can become an immediate, it will always be available in the
// local ability store, because the transformation is local (?)
//
// TODO: I actually think we can get what we need here by examining `derived_env.exposed_types`,
// since immediates can only refer to builtins - and in userspace, all builtin types
// are available in `exposed_types`.
let immediate_lambda_set_at_region =
phase.get_and_copy_ability_member_ambient_function(imm, lset_region, subs);
Ok(immediate_lambda_set_at_region)
}
SpecializationTypeKey::SingleLambdaSetImmediate(imm) => {
let module_id = imm.module_id();
debug_assert!(module_id.is_builtin());
let module_types = &derived_env
.exposed_types
.get(&module_id)
.unwrap()
.exposed_types_storage_subs;
// Since this immediate has only one lambda set, the region must be pointing to 1, and
// moreover the imported function type is the ambient function of the single lset.
debug_assert_eq!(lset_region, 1);
let storage_var = module_types.stored_vars_by_symbol.get(&imm).unwrap();
let imported = module_types
.storage_subs
.export_variable_to(subs, *storage_var);
roc_types::subs::instantiate_rigids(subs, imported.variable);
Ok(imported.variable)
}
}
}

View file

@ -5956,10 +5956,10 @@ mod solve_expr {
r#"
app "test" provides [hash] to "./platform"
Hash has hash : a -> U64 | a has Hash
MHash has hash : a -> U64 | a has MHash
"#
),
"a -> U64 | a has Hash",
"a -> U64 | a has MHash",
)
}
@ -5970,14 +5970,14 @@ mod solve_expr {
r#"
app "test" provides [hash] to "./platform"
Hash has hash : a -> U64 | a has Hash
MHash has hash : a -> U64 | a has MHash
Id := U64 has [Hash {hash}]
Id := U64 has [MHash {hash}]
hash = \@Id n -> n
"#
),
[("Hash:hash", "Id")],
[("MHash:hash", "Id")],
)
}
@ -5988,17 +5988,17 @@ mod solve_expr {
r#"
app "test" provides [hash, hash32] to "./platform"
Hash has
hash : a -> U64 | a has Hash
hash32 : a -> U32 | a has Hash
MHash has
hash : a -> U64 | a has MHash
hash32 : a -> U32 | a has MHash
Id := U64 has [Hash {hash, hash32}]
Id := U64 has [MHash {hash, hash32}]
hash = \@Id n -> n
hash32 = \@Id n -> Num.toU32 n
"#
),
[("Hash:hash", "Id"), ("Hash:hash32", "Id")],
[("MHash:hash", "Id"), ("MHash:hash32", "Id")],
)
}
@ -6009,15 +6009,15 @@ mod solve_expr {
r#"
app "test" provides [hash, hash32, eq, le] to "./platform"
Hash has
hash : a -> U64 | a has Hash
hash32 : a -> U32 | a has Hash
MHash has
hash : a -> U64 | a has MHash
hash32 : a -> U32 | a has MHash
Ord has
eq : a, a -> Bool | a has Ord
le : a, a -> Bool | a has Ord
Id := U64 has [Hash {hash, hash32}, Ord {eq, le}]
Id := U64 has [MHash {hash, hash32}, Ord {eq, le}]
hash = \@Id n -> n
hash32 = \@Id n -> Num.toU32 n
@ -6027,8 +6027,8 @@ mod solve_expr {
"#
),
[
("Hash:hash", "Id"),
("Hash:hash32", "Id"),
("MHash:hash", "Id"),
("MHash:hash32", "Id"),
("Ord:eq", "Id"),
("Ord:le", "Id"),
],
@ -6042,16 +6042,16 @@ mod solve_expr {
r#"
app "test" provides [hash] to "./platform"
Hash has
hash : a -> U64 | a has Hash
MHash has
hash : a -> U64 | a has MHash
Id := U64 has [Hash {hash}]
Id := U64 has [MHash {hash}]
hash : Id -> U64
hash = \@Id n -> n
"#
),
[("Hash:hash", "Id")],
[("MHash:hash", "Id")],
)
}
@ -6062,15 +6062,15 @@ mod solve_expr {
r#"
app "test" provides [hash] to "./platform"
Hash has
hash : a -> U64 | a has Hash
MHash has
hash : a -> U64 | a has MHash
Id := U64 has [Hash {hash}]
Id := U64 has [MHash {hash}]
hash : Id -> U64
"#
),
[("Hash:hash", "Id")],
[("MHash:hash", "Id")],
)
}
@ -6081,10 +6081,10 @@ mod solve_expr {
r#"
app "test" provides [zero] to "./platform"
Hash has
hash : a -> U64 | a has Hash
MHash has
hash : a -> U64 | a has MHash
Id := U64 has [Hash {hash}]
Id := U64 has [MHash {hash}]
hash = \@Id n -> n
@ -6102,15 +6102,15 @@ mod solve_expr {
r#"
app "test" provides [thething] to "./platform"
Hash has
hash : a -> U64 | a has Hash
MHash has
hash : a -> U64 | a has MHash
thething =
itis = hash
itis
"#
),
"a -> U64 | a has Hash",
"a -> U64 | a has MHash",
)
}
@ -6138,14 +6138,14 @@ mod solve_expr {
r#"
app "test" provides [hashEq] to "./platform"
Hash has
hash : a -> U64 | a has Hash
MHash has
hash : a -> U64 | a has MHash
hashEq : a, a -> Bool | a has Hash
hashEq : a, a -> Bool | a has MHash
hashEq = \x, y -> hash x == hash y
"#
),
"a, a -> Bool | a has Hash",
"a, a -> Bool | a has MHash",
)
}
@ -6156,13 +6156,13 @@ mod solve_expr {
r#"
app "test" provides [hashEq] to "./platform"
Hash has
hash : a -> U64 | a has Hash
MHash has
hash : a -> U64 | a has MHash
hashEq = \x, y -> hash x == hash y
"#
),
"a, a1 -> Bool | a has Hash, a1 has Hash",
"a, a1 -> Bool | a has MHash, a1 has MHash",
)
}
@ -6173,12 +6173,12 @@ mod solve_expr {
r#"
app "test" provides [result] to "./platform"
Hash has
hash : a -> U64 | a has Hash
MHash has
hash : a -> U64 | a has MHash
hashEq = \x, y -> hash x == hash y
Id := U64 has [Hash {hash}]
Id := U64 has [MHash {hash}]
hash = \@Id n -> n
result = hashEq (@Id 100) (@Id 101)
@ -6195,18 +6195,18 @@ mod solve_expr {
r#"
app "test" provides [result] to "./platform"
Hash has
hash : a -> U64 | a has Hash
MHash has
hash : a -> U64 | a has MHash
mulHashes = \x, y -> hash x * hash y
mulMHashes = \x, y -> hash x * hash y
Id := U64 has [Hash { hash: hashId }]
Id := U64 has [MHash { hash: hashId }]
hashId = \@Id n -> n
Three := {} has [Hash { hash: hashThree }]
Three := {} has [MHash { hash: hashThree }]
hashThree = \@Three _ -> 3
result = mulHashes (@Id 100) (@Three {})
result = mulMHashes (@Id 100) (@Three {})
"#
),
"U64",
@ -7823,4 +7823,22 @@ mod solve_expr {
"Result Str [] -> Str",
);
}
#[test]
fn custom_implement_hash() {
infer_eq_without_problem(
indoc!(
r#"
app "test" provides [main] to "./platform"
Noop := {} has [Hash {hash}]
hash = \hasher, @Noop {} -> hasher
main = \hasher -> hash hasher (@Noop {})
"#
),
"hasher -> hasher | hasher has Hasher",
);
}
}

View file

@ -0,0 +1,151 @@
#![cfg(test)]
// Even with #[allow(non_snake_case)] on individual idents, rust-analyzer issues diagnostics.
// See https://github.com/rust-lang/rust-analyzer/issues/6541.
// For the `v!` macro we use uppercase variables when constructing tag unions.
#![allow(non_snake_case)]
use crate::{
test_key_eq, test_key_neq,
util::{check_derivable, check_single_lset_immediate, check_underivable, derive_test},
v,
};
use insta::assert_snapshot;
use roc_module::symbol::Symbol;
use roc_types::subs::Variable;
use roc_derive_key::{hash::FlatHashKey, DeriveBuiltin::Hash, DeriveError, DeriveKey};
test_key_eq! {
Hash,
same_record:
v!({ a: v!(U8), }), v!({ a: v!(U8), })
same_record_fields_diff_types:
v!({ a: v!(U8), }), v!({ a: v!(STR), })
same_record_fields_any_order:
v!({ a: v!(U8), b: v!(U8), c: v!(U8), }),
v!({ c: v!(U8), a: v!(U8), b: v!(U8), })
explicit_empty_record_and_implicit_empty_record:
v!(EMPTY_RECORD), v!({})
}
test_key_neq! {
Hash,
different_record_fields:
v!({ a: v!(U8), }), v!({ b: v!(U8), })
record_empty_vs_nonempty:
v!(EMPTY_RECORD), v!({ a: v!(U8), })
}
#[test]
fn immediates() {
check_single_lset_immediate(Hash, v!(U8), Symbol::HASH_ADD_U8);
check_single_lset_immediate(Hash, v!(U16), Symbol::HASH_ADD_U16);
check_single_lset_immediate(Hash, v!(U32), Symbol::HASH_ADD_U32);
check_single_lset_immediate(Hash, v!(U64), Symbol::HASH_ADD_U64);
check_single_lset_immediate(Hash, v!(U128), Symbol::HASH_ADD_U128);
check_single_lset_immediate(Hash, v!(I8), Symbol::HASH_ADD_I8);
check_single_lset_immediate(Hash, v!(I16), Symbol::HASH_ADD_I16);
check_single_lset_immediate(Hash, v!(I32), Symbol::HASH_ADD_I32);
check_single_lset_immediate(Hash, v!(I64), Symbol::HASH_ADD_I64);
check_single_lset_immediate(Hash, v!(I128), Symbol::HASH_ADD_I128);
check_single_lset_immediate(Hash, v!(STR), Symbol::HASH_HASH_STR_BYTES);
check_single_lset_immediate(Hash, v!(Symbol::LIST_LIST v!(U8)), Symbol::HASH_HASH_LIST);
check_single_lset_immediate(Hash, v!(Symbol::LIST_LIST v!(STR)), Symbol::HASH_HASH_LIST);
}
#[test]
fn optional_record_field_derive_error() {
check_underivable(Hash, v!({ ?a: v!(U8), }), DeriveError::Underivable);
}
#[test]
fn derivable_record_ext_flex_var() {
check_derivable(
Hash,
v!({ a: v!(STR), }* ),
DeriveKey::Hash(FlatHashKey::Record(vec!["a".into()])),
);
}
#[test]
fn derivable_record_ext_flex_able_var() {
check_derivable(
Hash,
v!({ a: v!(STR), }a has Symbol::DECODE_DECODER ),
DeriveKey::Hash(FlatHashKey::Record(vec!["a".into()])),
);
}
#[test]
fn derivable_record_with_record_ext() {
check_derivable(
Hash,
v!({ b: v!(STR), }{ a: v!(STR), } ),
DeriveKey::Hash(FlatHashKey::Record(vec!["a".into(), "b".into()])),
);
}
#[test]
fn empty_record() {
derive_test(Hash, v!(EMPTY_RECORD), |golden| {
assert_snapshot!(golden, @r###"
# derived for {}
# hasher, {} -[[hash_{}(0)]]-> hasher | hasher has Hasher
# hasher, {} -[[hash_{}(0)]]-> hasher | hasher has Hasher
# Specialization lambda sets:
# @<1>: [[hash_{}(0)]]
#Derived.hash_{} = \#Derived.hasher, #Derived.rcd -> #Derived.hasher
"###
)
})
}
#[test]
fn zero_field_record() {
derive_test(Hash, v!({}), |golden| {
assert_snapshot!(golden, @r###"
# derived for {}
# hasher, {} -[[hash_{}(0)]]-> hasher | hasher has Hasher
# hasher, {} -[[hash_{}(0)]]-> hasher | hasher has Hasher
# Specialization lambda sets:
# @<1>: [[hash_{}(0)]]
#Derived.hash_{} = \#Derived.hasher, #Derived.rcd -> #Derived.hasher
"###
)
})
}
#[test]
fn one_field_record() {
derive_test(Hash, v!({ a: v!(U8), }), |golden| {
assert_snapshot!(golden, @r###"
# derived for { a : U8 }
# hasher, { a : a } -[[hash_{a}(0)]]-> hasher | a has Hash, hasher has Hasher
# hasher, { a : a } -[[hash_{a}(0)]]-> hasher | a has Hash, hasher has Hasher
# Specialization lambda sets:
# @<1>: [[hash_{a}(0)]]
#Derived.hash_{a} =
\#Derived.hasher, #Derived.rcd -> Hash.hash #Derived.hasher #Derived.rcd.a
"###
)
})
}
#[test]
fn two_field_record() {
derive_test(Hash, v!({ a: v!(U8), b: v!(STR), }), |golden| {
assert_snapshot!(golden, @r###"
# derived for { a : U8, b : Str }
# hasher, { a : a, b : a1 } -[[hash_{a,b}(0)]]-> hasher | a has Hash, a1 has Hash, hasher has Hasher
# hasher, { a : a, b : a1 } -[[hash_{a,b}(0)]]-> hasher | a has Hash, a1 has Hash, hasher has Hasher
# Specialization lambda sets:
# @<1>: [[hash_{a,b}(0)]]
#Derived.hash_{a,b} =
\#Derived.hasher, #Derived.rcd ->
Hash.hash (Hash.hash #Derived.hasher #Derived.rcd.a) #Derived.rcd.b
"###
)
})
}

View file

@ -2,6 +2,7 @@
mod decoding;
mod encoding;
mod hash;
mod pretty_print;
mod util;

View file

@ -50,6 +50,11 @@ fn module_source_and_path(builtin: DeriveBuiltin) -> (ModuleId, &'static str, Pa
module_source(ModuleId::DECODE),
builtins_path.join("Decode.roc"),
),
DeriveBuiltin::Hash => (
ModuleId::HASH,
module_source(ModuleId::HASH),
builtins_path.join("Hash.roc"),
),
}
}
@ -247,6 +252,18 @@ where
assert_eq!(key, Ok(Derived::Immediate(immediate)));
}
pub(crate) fn check_single_lset_immediate<S>(builtin: DeriveBuiltin, synth: S, immediate: Symbol)
where
S: FnOnce(&mut Subs) -> Variable,
{
let mut subs = Subs::new();
let var = synth(&mut subs);
let key = Derived::builtin(builtin, &subs, var);
assert_eq!(key, Ok(Derived::SingleLambdaSetImmediate(immediate)));
}
#[allow(clippy::too_many_arguments)]
fn assemble_derived_golden(
subs: &mut Subs,

View file

@ -20,10 +20,10 @@ fn hash_specialization() {
r#"
app "test" provides [main] to "./platform"
Hash has
hash : a -> U64 | a has Hash
MHash has
hash : a -> U64 | a has MHash
Id := U64 has [Hash {hash}]
Id := U64 has [MHash {hash}]
hash = \@Id n -> n
@ -43,14 +43,14 @@ fn hash_specialization_multiple_add() {
r#"
app "test" provides [main] to "./platform"
Hash has
hash : a -> U64 | a has Hash
MHash has
hash : a -> U64 | a has MHash
Id := U64 has [ Hash {hash: hashId} ]
Id := U64 has [ MHash {hash: hashId} ]
hashId = \@Id n -> n
One := {} has [ Hash {hash: hashOne} ]
One := {} has [ MHash {hash: hashOne} ]
hashOne = \@One _ -> 1
@ -70,16 +70,16 @@ fn alias_member_specialization() {
r#"
app "test" provides [main] to "./platform"
Hash has
hash : a -> U64 | a has Hash
MHash has
hash : a -> U64 | a has MHash
Id := U64 has [Hash {hash}]
Id := U64 has [MHash {hash}]
hash = \@Id n -> n
main =
aliasedHash = hash
aliasedHash (@Id 1234)
aliasedMHash = hash
aliasedMHash (@Id 1234)
"#
),
1234,
@ -95,16 +95,16 @@ fn ability_constrained_in_non_member_usage() {
r#"
app "test" provides [result] to "./platform"
Hash has
hash : a -> U64 | a has Hash
MHash has
hash : a -> U64 | a has MHash
mulHashes : a, a -> U64 | a has Hash
mulHashes = \x, y -> hash x * hash y
mulMHashes : a, a -> U64 | a has MHash
mulMHashes = \x, y -> hash x * hash y
Id := U64 has [Hash {hash}]
Id := U64 has [MHash {hash}]
hash = \@Id n -> n
result = mulHashes (@Id 5) (@Id 7)
result = mulMHashes (@Id 5) (@Id 7)
"#
),
35,
@ -120,15 +120,15 @@ fn ability_constrained_in_non_member_usage_inferred() {
r#"
app "test" provides [result] to "./platform"
Hash has
hash : a -> U64 | a has Hash
MHash has
hash : a -> U64 | a has MHash
mulHashes = \x, y -> hash x * hash y
mulMHashes = \x, y -> hash x * hash y
Id := U64 has [Hash {hash}]
Id := U64 has [MHash {hash}]
hash = \@Id n -> n
result = mulHashes (@Id 5) (@Id 7)
result = mulMHashes (@Id 5) (@Id 7)
"#
),
35,
@ -144,19 +144,19 @@ fn ability_constrained_in_non_member_multiple_specializations() {
r#"
app "test" provides [result] to "./platform"
Hash has
hash : a -> U64 | a has Hash
MHash has
hash : a -> U64 | a has MHash
mulHashes : a, b -> U64 | a has Hash, b has Hash
mulHashes = \x, y -> hash x * hash y
mulMHashes : a, b -> U64 | a has MHash, b has MHash
mulMHashes = \x, y -> hash x * hash y
Id := U64 has [Hash { hash: hashId }]
Id := U64 has [MHash { hash: hashId }]
hashId = \@Id n -> n
Three := {} has [Hash { hash: hashThree }]
Three := {} has [MHash { hash: hashThree }]
hashThree = \@Three _ -> 3
result = mulHashes (@Id 100) (@Three {})
result = mulMHashes (@Id 100) (@Three {})
"#
),
300,
@ -172,18 +172,18 @@ fn ability_constrained_in_non_member_multiple_specializations_inferred() {
r#"
app "test" provides [result] to "./platform"
Hash has
hash : a -> U64 | a has Hash
MHash has
hash : a -> U64 | a has MHash
mulHashes = \x, y -> hash x * hash y
mulMHashes = \x, y -> hash x * hash y
Id := U64 has [Hash { hash: hashId }]
Id := U64 has [MHash { hash: hashId }]
hashId = \@Id n -> n
Three := {} has [Hash { hash: hashThree }]
Three := {} has [MHash { hash: hashThree }]
hashThree = \@Three _ -> 3
result = mulHashes (@Id 100) (@Three {})
result = mulMHashes (@Id 100) (@Three {})
"#
),
300,
@ -199,19 +199,19 @@ fn ability_used_as_type_still_compiles() {
r#"
app "test" provides [result] to "./platform"
Hash has
hash : a -> U64 | a has Hash
MHash has
hash : a -> U64 | a has MHash
mulHashes : Hash, Hash -> U64
mulHashes = \x, y -> hash x * hash y
mulMHashes : MHash, MHash -> U64
mulMHashes = \x, y -> hash x * hash y
Id := U64 has [Hash { hash: hashId }]
Id := U64 has [MHash { hash: hashId }]
hashId = \@Id n -> n
Three := {} has [Hash { hash: hashThree }]
Three := {} has [MHash { hash: hashThree }]
hashThree = \@Three _ -> 3
result = mulHashes (@Id 100) (@Three {})
result = mulMHashes (@Id 100) (@Three {})
"#
),
300,
@ -1102,3 +1102,275 @@ fn decode_record_of_record() {
RocStr
)
}
#[cfg(all(test, any(feature = "gen-llvm", feature = "gen-wasm")))]
mod hash {
#[cfg(feature = "gen-llvm")]
use crate::helpers::llvm::assert_evals_to;
#[cfg(feature = "gen-wasm")]
use crate::helpers::wasm::assert_evals_to;
use indoc::indoc;
const TEST_HASHER: &str = indoc!(
r#"
THasher := List U8 has [Hasher {
addBytes: tAddBytes,
addU8: tAddU8,
addU16: tAddU16,
addU32: tAddU32,
addU64: tAddU64,
addU128: tAddU128,
addI8: tAddI8,
addI16: tAddI16,
addI32: tAddI32,
addI64: tAddI64,
addI128: tAddI128,
complete: tComplete,
}]
# ignores endian-ness
byteAt = \n, shift ->
Num.bitwiseAnd (Num.shiftRightBy n (shift * 8)) 0xFF
|> Num.toU8
do8 = \total, n ->
total
|> List.append (byteAt n 0)
do16 = \total, n ->
total
|> do8 (n |> Num.toU8)
|> do8 (Num.shiftRightBy n 8 |> Num.toU8)
do32 = \total, n ->
total
|> do16 (n |> Num.toU16)
|> do16 (Num.shiftRightBy n 16 |> Num.toU16)
do64 = \total, n ->
total
|> do32 (n |> Num.toU32)
|> do32 (Num.shiftRightBy n 32 |> Num.toU32)
do128 = \total, n ->
total
|> do64 (n |> Num.toU64)
|> do64 (Num.shiftRightBy n 64 |> Num.toU64)
tAddBytes = \@THasher total, bytes -> @THasher (List.concat total bytes)
tAddU8 = \@THasher total, n -> @THasher (do8 total n)
tAddU16 = \@THasher total, n -> @THasher (do16 total n)
tAddU32 = \@THasher total, n -> @THasher (do32 total n)
tAddU64 = \@THasher total, n -> @THasher (do64 total n)
tAddU128 = \@THasher total, n -> @THasher (do128 total n)
tAddI8 = \@THasher total, n -> @THasher (do8 total (Num.toU8 n))
tAddI16 = \@THasher total, n -> @THasher (do16 total (Num.toU16 n))
tAddI32 = \@THasher total, n -> @THasher (do32 total (Num.toU32 n))
tAddI64 = \@THasher total, n -> @THasher (do64 total (Num.toU64 n))
tAddI128 = \@THasher total, n -> @THasher (do128 total (Num.toU128 n))
tComplete = \@THasher _ -> Num.maxU64
tRead = \@THasher bytes -> bytes
"#
);
fn build_test(input: &str) -> String {
format!(
indoc!(
r#"
app "test" provides [main] to "./platform"
{}
main =
@THasher []
|> Hash.hash ({})
|> tRead
"#
),
TEST_HASHER, input,
)
}
mod immediate {
use super::{assert_evals_to, build_test};
use roc_std::RocList;
#[test]
fn i8() {
assert_evals_to!(
&build_test("-2i8"),
RocList::from_slice(&[254]),
RocList<u8>
)
}
#[test]
fn u8() {
assert_evals_to!(
&build_test("254u8"),
RocList::from_slice(&[254]),
RocList<u8>
)
}
#[test]
fn i16() {
assert_evals_to!(
&build_test("-2i16"),
RocList::from_slice(&[254, 255]),
RocList<u8>
)
}
#[test]
fn u16() {
assert_evals_to!(
&build_test("Num.maxU16 - 1"),
RocList::from_slice(&[254, 255]),
RocList<u8>
)
}
#[test]
fn i32() {
assert_evals_to!(
&build_test("-2i32"),
RocList::from_slice(&[254, 255, 255, 255]),
RocList<u8>
)
}
#[test]
fn u32() {
assert_evals_to!(
&build_test("Num.maxU32 - 1"),
RocList::from_slice(&[254, 255, 255, 255]),
RocList<u8>
)
}
#[test]
fn i64() {
assert_evals_to!(
&build_test("-2i64"),
RocList::from_slice(&[254, 255, 255, 255, 255, 255, 255, 255]),
RocList<u8>
)
}
#[test]
fn u64() {
assert_evals_to!(
&build_test("Num.maxU64 - 1"),
RocList::from_slice(&[254, 255, 255, 255, 255, 255, 255, 255]),
RocList<u8>
)
}
#[test]
#[cfg(not(feature = "gen-wasm"))] // shr not implemented for U128
fn i128() {
assert_evals_to!(
&build_test("-2i128"),
RocList::from_slice(&[
254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
]),
RocList<u8>
)
}
#[test]
#[cfg(not(feature = "gen-wasm"))] // shr not implemented for U128
fn u128() {
assert_evals_to!(
&build_test("Num.maxU128 - 1"),
RocList::from_slice(&[
254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255
]),
RocList<u8>
)
}
#[test]
fn string() {
assert_evals_to!(
&build_test(r#""ab☃AB""#),
RocList::from_slice(&[97, 98, 226, 152, 131, 65, 66]),
RocList<u8>
)
}
#[test]
fn list_u8() {
assert_evals_to!(
&build_test(r#"[15u8, 23u8, 37u8]"#),
RocList::from_slice(&[15, 23, 37]),
RocList<u8>
)
}
#[test]
fn list_string() {
assert_evals_to!(
&build_test(r#"["ab", "cd", "ef"]"#),
RocList::from_slice(&[97, 98, 99, 100, 101, 102]),
RocList<u8>
)
}
#[test]
fn list_list_string() {
assert_evals_to!(
&build_test(r#"[[ "ab", "cd" ], [ "ef" ]]"#),
RocList::from_slice(&[97, 98, 99, 100, 101, 102]),
RocList<u8>
)
}
}
mod derived {
use super::{assert_evals_to, build_test};
use roc_std::RocList;
#[test]
fn empty_record() {
assert_evals_to!(
&build_test(r#"{}"#),
RocList::from_slice(&[] as &[u8]),
RocList<u8>
)
}
#[test]
fn record_of_u8_and_str() {
assert_evals_to!(
&build_test(r#"{ a: 15u8, b: "bc" }"#),
RocList::from_slice(&[15, 98, 99]),
RocList<u8>
)
}
#[test]
fn record_of_records() {
assert_evals_to!(
&build_test(r#"{ a: { b: 15u8, c: "bc" }, d: { b: 23u8, e: "ef" } }"#),
RocList::from_slice(&[15, 98, 99, 23, 101, 102]),
RocList<u8>
)
}
#[test]
fn record_of_list_of_records() {
assert_evals_to!(
&build_test(
r#"{ a: [ { b: 15u8 }, { b: 23u8 } ], b: [ { c: 45u8 }, { c: 73u8 } ] }"#
),
RocList::from_slice(&[15, 23, 45, 73]),
RocList<u8>
)
}
}
}

View file

@ -614,6 +614,25 @@ fn i64_abs() {
assert_evals_to!("Num.abs (Num.minI64 + 1)", -(i64::MIN + 1), i64);
}
#[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))]
fn various_sized_abs() {
assert_evals_to!("Num.abs -6i8", 6, i8);
assert_evals_to!("Num.abs -6i16", 6, i16);
assert_evals_to!("Num.abs -6i32", 6, i32);
assert_evals_to!("Num.abs -6i64", 6, i64);
if !cfg!(feature = "gen-wasm") {
assert_evals_to!("Num.abs -6i128", 6, i128);
}
assert_evals_to!("Num.abs 6u8", 6, u8);
assert_evals_to!("Num.abs 6u16", 6, u16);
assert_evals_to!("Num.abs 6u32", 6, u32);
assert_evals_to!("Num.abs 6u64", 6, u64);
if !cfg!(feature = "gen-wasm") {
assert_evals_to!("Num.abs 6u128", 6, u128);
}
}
#[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))]
#[should_panic(
@ -2075,6 +2094,7 @@ fn shift_left_by() {
assert_evals_to!("Num.shiftLeftBy 0b0000_0001 0", 0b0000_0001, i64);
assert_evals_to!("Num.shiftLeftBy 0b0000_0001 1", 0b0000_0010, i64);
assert_evals_to!("Num.shiftLeftBy 0b0000_0011 2", 0b0000_1100, i64);
assert_evals_to!("Num.shiftLeftBy 2u16 2", 8, u16);
}
#[test]
@ -2104,7 +2124,6 @@ fn shift_right_by() {
assert_evals_to!("Num.shiftRightBy -12 1", -6, i64);
assert_evals_to!("Num.shiftRightBy 12 8", 0, i64);
assert_evals_to!("Num.shiftRightBy -12 8", -1, i64);
assert_evals_to!("Num.shiftRightBy 12 -1", 0, i64);
assert_evals_to!("Num.shiftRightBy 0 0", 0, i64);
assert_evals_to!("Num.shiftRightBy 0 1", 0, i64);
@ -2120,8 +2139,6 @@ fn shift_right_by() {
assert_evals_to!("Num.shiftRightBy 12i8 8", 0, i8);
if !is_llvm_release_mode {
assert_evals_to!("Num.shiftRightBy 0 -1", 0, i64);
assert_evals_to!("Num.shiftRightBy -12 -1", -1, i64);
assert_evals_to!("Num.shiftRightBy -12i8 8", -1, i8);
}
}

View file

@ -1325,10 +1325,10 @@ fn specialize_ability_call() {
r#"
app "test" provides [main] to "./platform"
Hash has
hash : a -> U64 | a has Hash
MHash has
hash : a -> U64 | a has MHash
Id := U64 has [Hash {hash}]
Id := U64 has [MHash {hash}]
hash : Id -> U64
hash = \@Id n -> n

View file

@ -3868,11 +3868,11 @@ fn flat_type_to_err_type(
ErrorType::TagUnion(sub_tags.union(err_tags), sub_ext)
}
ErrorType::FlexVar(var) => {
ErrorType::FlexVar(var) | ErrorType::FlexAbleVar(var, _) => {
ErrorType::TagUnion(err_tags, TypeExt::FlexOpen(var))
}
ErrorType::RigidVar(var) => {
ErrorType::RigidVar(var) | ErrorType::RigidAbleVar(var, _)=> {
ErrorType::TagUnion(err_tags, TypeExt::RigidOpen(var))
}
@ -3896,11 +3896,11 @@ fn flat_type_to_err_type(
ErrorType::TagUnion(sub_tags.union(err_tags), sub_ext)
}
ErrorType::FlexVar(var) => {
ErrorType::FlexVar(var) | ErrorType::FlexAbleVar(var, _) => {
ErrorType::TagUnion(err_tags, TypeExt::FlexOpen(var))
}
ErrorType::RigidVar(var) => {
ErrorType::RigidVar(var) | ErrorType::RigidAbleVar(var, _)=> {
ErrorType::TagUnion(err_tags, TypeExt::RigidOpen(var))
}

View file

@ -2820,7 +2820,7 @@ fn unify_rigid_able<M: MetaCollector>(
match other {
FlexVar(_) => {
// If the other is flex, rigid wins!
merge(env, ctx, RigidVar(*name))
merge(env, ctx, RigidAbleVar(*name, ability))
}
FlexAbleVar(_, other_ability) => {
if ability == *other_ability {

View file

@ -215,6 +215,7 @@ mod glue_cli_run {
.map(|arg| arg.to_string())
.chain([app_file.to_str().unwrap().to_string()]),
&[],
&[],
);
let ignorable = "🔨 Rebuilding platform...\n";

View file

@ -357,7 +357,6 @@ fn gen_and_eval_llvm<'a>(
&loaded.interns,
DebugPrint::NOTHING,
);
let content = *loaded.subs.get_content_without_compacting(main_fn_var);
let (_, main_fn_layout) = match loaded.procedures.keys().find(|(s, _)| *s == main_fn_symbol) {
Some(layout) => *layout,
@ -381,7 +380,7 @@ fn gen_and_eval_llvm<'a>(
&mut app,
main_fn_name,
main_fn_layout,
&content,
main_fn_var,
&subs,
&interns,
layout_interner.into_global().fork(),

View file

@ -48,7 +48,7 @@ pub fn jit_to_ast<'a, A: ReplApp<'a>>(
app: &mut A,
main_fn_name: &str,
layout: ProcLayout<'a>,
content: &Content,
var: Variable,
subs: &Subs,
interns: &'a Interns,
layout_interner: LayoutInterner<'a>,
@ -69,7 +69,7 @@ pub fn jit_to_ast<'a, A: ReplApp<'a>>(
captures_niche: _,
} => {
// this is a thunk
jit_to_ast_help(&mut env, app, main_fn_name, &result, content)
jit_to_ast_help(&mut env, app, main_fn_name, &result, var)
}
_ => Err(ToAstProblem::FunctionLayout),
}
@ -82,6 +82,45 @@ enum NewtypeKind {
Opaque(Symbol),
}
fn get_newtype_tag_and_var(
env: &mut Env,
var: Variable,
tags: UnionTags,
) -> Option<(TagName, Variable)> {
let union_variant = {
let mut layout_env = roc_mono::layout::Env::from_components(
&mut env.layout_cache,
env.subs,
env.arena,
env.target_info,
);
roc_mono::layout::union_sorted_tags(&mut layout_env, var).unwrap()
};
let tag_name = match union_variant {
UnionVariant::Newtype { tag_name, .. }
| UnionVariant::NewtypeByVoid {
data_tag_name: tag_name,
..
} => tag_name.expect_tag(),
_ => return None,
};
let vars = tags
.unsorted_iterator(env.subs, Variable::EMPTY_TAG_UNION)
.find(|(tag, _)| **tag == tag_name)
.unwrap()
.1;
match vars {
[var] => Some((tag_name, *var)),
_ => {
// Multiple variables; we should not display this as a newtype.
None
}
}
}
/// Unrolls types that are newtypes. These include
/// - Singleton tags with one type argument (e.g. `Container Str`)
/// - Records with exactly one field (e.g. `{ number: Nat }`)
@ -97,24 +136,23 @@ enum NewtypeKind {
///
/// Returns (new type containers, optional alias content, real content).
fn unroll_newtypes_and_aliases<'a, 'env>(
env: &Env<'a, 'env>,
mut content: &'env Content,
) -> (Vec<'a, NewtypeKind>, Option<&'env Content>, &'env Content) {
env: &mut Env<'a, 'env>,
var: Variable,
) -> (Vec<'a, NewtypeKind>, Option<&'env Content>, Variable) {
let mut var = var;
let mut newtype_containers = Vec::with_capacity_in(1, env.arena);
let mut alias_content = None;
loop {
let content = env.subs.get_content_without_compacting(var);
match content {
Content::Structure(FlatType::TagUnion(tags, _))
if tags.is_newtype_wrapper(env.subs) =>
{
let (tag_name, vars): (&TagName, &[Variable]) = tags
.unsorted_iterator(env.subs, Variable::EMPTY_TAG_UNION)
.next()
.unwrap();
newtype_containers.push(NewtypeKind::Tag(tag_name.clone()));
let var = vars[0];
content = env.subs.get_content_without_compacting(var);
Content::Structure(FlatType::TagUnion(tags, _)) => {
match get_newtype_tag_and_var(env, var, *tags) {
Some((tag_name, inner_var)) => {
newtype_containers.push(NewtypeKind::Tag(tag_name));
var = inner_var;
}
None => return (newtype_containers, alias_content, var),
}
}
Content::Structure(FlatType::Record(fields, _)) if fields.len() == 1 => {
let (label, field) = fields
@ -122,11 +160,11 @@ fn unroll_newtypes_and_aliases<'a, 'env>(
.next()
.unwrap();
newtype_containers.push(NewtypeKind::RecordField(label.to_string()));
content = env.subs.get_content_without_compacting(field.into_inner());
var = field.into_inner();
}
Content::Alias(name, _, real_var, kind) => {
if *name == Symbol::BOOL_BOOL {
return (newtype_containers, alias_content, content);
return (newtype_containers, alias_content, var);
}
// We need to pass through aliases too, because their underlying types may have
// unrolled newtypes. For example,
@ -142,9 +180,9 @@ fn unroll_newtypes_and_aliases<'a, 'env>(
newtype_containers.push(NewtypeKind::Opaque(*name));
}
alias_content = Some(content);
content = env.subs.get_content_without_compacting(*real_var);
var = *real_var;
}
_ => return (newtype_containers, alias_content, content),
_ => return (newtype_containers, alias_content, var),
}
}
}
@ -292,10 +330,9 @@ fn jit_to_ast_help<'a, A: ReplApp<'a>>(
app: &mut A,
main_fn_name: &str,
layout: &Layout<'a>,
content: &Content,
var: Variable,
) -> Result<Expr<'a>, ToAstProblem> {
let (newtype_containers, alias_content, raw_content) =
unroll_newtypes_and_aliases(env, content);
let (newtype_containers, alias_content, raw_var) = unroll_newtypes_and_aliases(env, var);
macro_rules! num_helper {
($ty:ty) => {
@ -306,10 +343,17 @@ fn jit_to_ast_help<'a, A: ReplApp<'a>>(
}
let result = match layout {
Layout::Builtin(Builtin::Bool) => Ok(app
.call_function(main_fn_name, |mem: &A::Memory, num: bool| {
bool_to_ast(env, mem, num, raw_content)
})),
Layout::Builtin(Builtin::Bool) => Ok(app.call_function(
main_fn_name,
|mem: &A::Memory, num: bool| {
bool_to_ast(
env,
mem,
num,
env.subs.get_content_without_compacting(raw_var),
)
},
)),
Layout::Builtin(Builtin::Int(int_width)) => {
use Content::*;
use IntWidth::*;
@ -319,7 +363,12 @@ fn jit_to_ast_help<'a, A: ReplApp<'a>>(
(_, U8) => {
// This is not a number, it's a tag union or something else
app.call_function(main_fn_name, |mem: &A::Memory, num: u8| {
byte_to_ast(env, mem, num, raw_content)
byte_to_ast(
env,
mem,
num,
env.subs.get_content_without_compacting(raw_var),
)
})
}
// The rest are numbers... for now
@ -362,7 +411,14 @@ fn jit_to_ast_help<'a, A: ReplApp<'a>>(
Ok(app.call_function_returns_roc_list(
main_fn_name,
|mem: &A::Memory, (addr, len, _cap)| {
list_to_ast(env, mem, addr, len, elem_layout, raw_content)
list_to_ast(
env,
mem,
addr,
len,
elem_layout,
env.subs.get_content_without_compacting(raw_var),
)
},
))
}
@ -372,7 +428,10 @@ fn jit_to_ast_help<'a, A: ReplApp<'a>>(
let result_stack_size = layout.stack_size(&env.layout_cache.interner, env.target_info);
let struct_addr_to_ast = |mem: &'a A::Memory, addr: usize| match raw_content {
let struct_addr_to_ast = |mem: &'a A::Memory, addr: usize| match env
.subs
.get_content_without_compacting(raw_var)
{
Content::Structure(FlatType::Record(fields, _)) => {
Ok(struct_to_ast(env, mem, addr, *fields))
}
@ -433,7 +492,7 @@ fn jit_to_ast_help<'a, A: ReplApp<'a>>(
addr,
layout,
WhenRecursive::Unreachable,
raw_content,
env.subs.get_root_key_without_compacting(raw_var),
)
},
))
@ -453,7 +512,7 @@ fn jit_to_ast_help<'a, A: ReplApp<'a>>(
addr,
layout,
WhenRecursive::Loop(*layout),
raw_content,
env.subs.get_root_key_without_compacting(raw_var),
)
},
))
@ -474,7 +533,7 @@ fn jit_to_ast_help<'a, A: ReplApp<'a>>(
addr,
layout,
WhenRecursive::Unreachable,
raw_content,
env.subs.get_root_key_without_compacting(raw_var),
)
},
))
@ -501,7 +560,7 @@ fn addr_to_ast<'a, M: ReplAppMemory>(
addr: usize,
layout: &Layout<'a>,
when_recursive: WhenRecursive<'a>,
content: &Content,
var: Variable,
) -> Expr<'a> {
macro_rules! helper {
($method: ident, $ty: ty) => {{
@ -511,8 +570,8 @@ fn addr_to_ast<'a, M: ReplAppMemory>(
}};
}
let (newtype_containers, _alias_content, raw_content) =
unroll_newtypes_and_aliases(env, content);
let (newtype_containers, _alias_content, raw_var) = unroll_newtypes_and_aliases(env, var);
let raw_content = env.subs.get_content_without_compacting(raw_var);
let expr = match (raw_content, layout) {
(Content::Structure(FlatType::Func(_, _, _)), _) | (_, Layout::LambdaSet(_)) => {
@ -594,8 +653,7 @@ fn addr_to_ast<'a, M: ReplAppMemory>(
},
WhenRecursive::Loop(union_layout),
) => {
let content = env.subs.get_content_without_compacting(*structure);
addr_to_ast(env, mem, addr, &union_layout, when_recursive, content)
addr_to_ast(env, mem, addr, &union_layout, when_recursive, *structure)
}
(
@ -607,13 +665,12 @@ fn addr_to_ast<'a, M: ReplAppMemory>(
) => {
// It's possible to hit a recursive pointer before the full type layout; just
// figure out the actual recursive structure layout at this point.
let content = env.subs.get_content_without_compacting(*structure);
let union_layout = env.layout_cache
.from_var(env.arena, *structure, env.subs)
.expect("no layout for structure");
debug_assert!(matches!(union_layout, Layout::Union(..)));
let when_recursive = WhenRecursive::Loop(union_layout);
addr_to_ast(env, mem, addr, &union_layout, when_recursive, content)
addr_to_ast(env, mem, addr, &union_layout, when_recursive, *structure)
}
other => unreachable!("Something had a RecursivePointer layout, but instead of being a RecursionVar and having a known recursive layout, I found {:?}", other),
},
@ -813,7 +870,6 @@ fn addr_to_ast<'a, M: ReplAppMemory>(
let inner_var_index = args.into_iter().next().unwrap();
let inner_var = env.subs[inner_var_index];
let inner_content = env.subs.get_content_without_compacting(inner_var);
let addr_of_inner = mem.deref_usize(addr);
let inner_expr = addr_to_ast(
@ -822,7 +878,7 @@ fn addr_to_ast<'a, M: ReplAppMemory>(
addr_of_inner,
inner_layout,
WhenRecursive::Unreachable,
inner_content,
inner_var,
);
let box_box = env.arena.alloc(Loc::at_zero(Expr::Var {
@ -855,14 +911,12 @@ fn list_to_ast<'a, M: ReplAppMemory>(
elem_layout: &Layout<'a>,
content: &Content,
) -> Expr<'a> {
let elem_content = match content {
let elem_var = match content {
Content::Structure(FlatType::Apply(Symbol::LIST_LIST, vars)) => {
debug_assert_eq!(vars.len(), 1);
let elem_var_index = vars.into_iter().next().unwrap();
let elem_var = env.subs[elem_var_index];
env.subs.get_content_without_compacting(elem_var)
env.subs[elem_var_index]
}
other => {
unreachable!(
@ -880,7 +934,7 @@ fn list_to_ast<'a, M: ReplAppMemory>(
let offset_bytes = index * elem_size;
let elem_addr = addr + offset_bytes;
let (newtype_containers, _alias_content, elem_content) =
unroll_newtypes_and_aliases(env, elem_content);
unroll_newtypes_and_aliases(env, elem_var);
let expr = addr_to_ast(
env,
mem,
@ -942,15 +996,13 @@ where
I: ExactSizeIterator<Item = (Variable, &'a Layout<'a>)>,
{
let arena = env.arena;
let subs = env.subs;
let mut output = Vec::with_capacity_in(sequence.len(), arena);
// We'll advance this as we iterate through the fields
let mut field_addr = addr;
for (var, layout) in sequence {
let content = subs.get_content_without_compacting(var);
let expr = addr_to_ast(env, mem, field_addr, layout, when_recursive, content);
let expr = addr_to_ast(env, mem, field_addr, layout, when_recursive, var);
let loc_expr = Loc::at_zero(expr);
output.push(&*arena.alloc(loc_expr));
@ -979,7 +1031,7 @@ fn struct_to_ast<'a, 'env, M: ReplAppMemory>(
.next()
.unwrap();
let inner_content = env.subs.get_content_without_compacting(field.into_inner());
let inner_var = field.into_inner();
let field_layout = env
.layout_cache
.from_var(arena, field.into_inner(), env.subs)
@ -993,7 +1045,7 @@ fn struct_to_ast<'a, 'env, M: ReplAppMemory>(
addr,
&Layout::struct_no_name_order(inner_layouts),
WhenRecursive::Unreachable,
inner_content,
inner_var,
),
region: Region::zero(),
});
@ -1019,7 +1071,7 @@ fn struct_to_ast<'a, 'env, M: ReplAppMemory>(
// always only sorted alphabetically. We want to arrange the rendered record in the order of
// the type.
for (label, field) in record_fields.sorted_iterator(subs, Variable::EMPTY_RECORD) {
let content = subs.get_content_without_compacting(field.into_inner());
let field_var = field.into_inner();
let field_layout = env
.layout_cache
.from_var(arena, field.into_inner(), env.subs)
@ -1032,7 +1084,7 @@ fn struct_to_ast<'a, 'env, M: ReplAppMemory>(
field_addr,
&field_layout,
WhenRecursive::Unreachable,
content,
field_var,
),
region: Region::zero(),
});
@ -1153,6 +1205,10 @@ fn bool_to_ast<'a, M: ReplAppMemory>(
}
}
}
Alias(Symbol::BOOL_BOOL, _, _, _) => Expr::Var {
module_name: "Bool",
ident: if value { "true" } else { "false" },
},
Alias(_, _, var, _) => {
let content = env.subs.get_content_without_compacting(*var);

View file

@ -54,8 +54,6 @@ pub fn get_values<'a>(
let expr = {
let variable = *variable;
let content = subs.get_content_without_compacting(variable);
// TODO: pass layout_cache to jit_to_ast directly
let mut layout_cache = LayoutCache::new(layout_interner.fork(), target_info);
let layout = layout_cache.from_var(arena, variable, subs).unwrap();
@ -71,7 +69,7 @@ pub fn get_values<'a>(
app,
"expect_repl_main_fn",
proc_layout,
content,
variable,
subs,
interns,
layout_interner.fork(),
@ -853,7 +851,7 @@ mod test {
run_expect_test(
indoc!(
r#"
interface A exposes [] imports []
interface Test exposes [] imports []
NonEmpty := [
First Str U8,
@ -899,7 +897,7 @@ mod test {
run_expect_test(
indoc!(
r#"
interface A exposes [] imports []
interface Test exposes [] imports []
makeForcer : {} -> (Str -> U8)
makeForcer = \{} -> \_ -> 2u8

View file

@ -100,22 +100,46 @@ fn num_ceil_checked_division_success() {
#[test]
fn bool_in_record() {
expect_success("{ x: 1 == 1 }", "{ x: True } : { x : Bool }");
expect_success("{ x: 1 == 1 }", "{ x: Bool.true } : { x : Bool }");
expect_success(
"{ z: { y: { x: 1 == 1 } } }",
"{ z: { y: { x: True } } } : { z : { y : { x : Bool } } }",
"{ z: { y: { x: Bool.true } } } : { z : { y : { x : Bool } } }",
);
expect_success("{ x: 1 != 1 }", "{ x: False } : { x : Bool }");
expect_success("{ x: 1 != 1 }", "{ x: Bool.false } : { x : Bool }");
expect_success(
"{ x: 1 == 1, y: 1 != 1 }",
"{ x: True, y: False } : { x : Bool, y : Bool }",
"{ x: Bool.true, y: Bool.false } : { x : Bool, y : Bool }",
);
}
#[test]
fn bool_basic_equality() {
expect_success("1 == 1", "True : Bool");
expect_success("1 != 1", "False : Bool");
expect_success("1 == 1", "Bool.true : Bool");
expect_success("1 != 1", "Bool.false : Bool");
}
#[test]
fn bool_true() {
expect_success(
indoc!(
r#"
Bool.true
"#
),
r#"Bool.true : Bool"#,
);
}
#[test]
fn bool_false() {
expect_success(
indoc!(
r#"
Bool.false
"#
),
r#"Bool.false : Bool"#,
);
}
#[test]
@ -395,9 +419,9 @@ fn list_concat() {
#[cfg(not(feature = "wasm"))]
#[test]
fn list_contains() {
expect_success("List.contains [] 0", "False : Bool");
expect_success("List.contains [1, 2, 3] 2", "True : Bool");
expect_success("List.contains [1, 2, 3] 4", "False : Bool");
expect_success("List.contains [] 0", "Bool.false : Bool");
expect_success("List.contains [1, 2, 3] 2", "Bool.true : Bool");
expect_success("List.contains [1, 2, 3] 4", "Bool.false : Bool");
}
#[cfg(not(feature = "wasm"))]
@ -1239,3 +1263,24 @@ fn record_of_poly_function_and_string() {
r#"{ a: <function>, b: "b" } : { a : * -> Str, b : Str }"#,
);
}
#[test]
fn newtype_by_void_is_wrapped() {
expect_success(
indoc!(
r#"
Result.try (Err 42) (\x -> Err (x+1))
"#
),
r#"Err 42 : Result b (Num *)"#,
);
expect_success(
indoc!(
r#"
Result.try (Ok 42) (\x -> Ok (x+1))
"#
),
r#"Ok 43 : Result (Num *) err"#,
);
}

View file

@ -203,7 +203,6 @@ pub async fn entrypoint_from_js(src: String) -> Result<String, String> {
&interns,
DebugPrint::NOTHING,
);
let content = subs.get_content_without_compacting(main_fn_var);
let (_, main_fn_layout) = match procedures.keys().find(|(s, _)| *s == main_fn_symbol) {
Some(layout) => *layout,
@ -264,7 +263,7 @@ pub async fn entrypoint_from_js(src: String) -> Result<String, String> {
&mut app,
"", // main_fn_name is ignored (only passed to WasmReplApp methods)
main_fn_layout,
content,
main_fn_var,
&subs,
&interns,
layout_interner.into_global().fork(),

View file

@ -3,7 +3,8 @@ use roc_module::ident::{Ident, Lowercase, ModuleName};
use roc_module::symbol::DERIVABLE_ABILITIES;
use roc_problem::can::PrecedenceProblem::BothNonAssociative;
use roc_problem::can::{
BadPattern, ExtensionTypeKind, FloatErrorKind, IntErrorKind, Problem, RuntimeError, ShadowKind,
BadPattern, CycleEntry, ExtensionTypeKind, FloatErrorKind, IntErrorKind, Problem, RuntimeError,
ShadowKind,
};
use roc_region::all::{LineColumn, LineColumnRegion, LineInfo, Loc, Region};
use roc_types::types::AliasKind;
@ -2025,12 +2026,22 @@ pub fn to_circular_def_doc<'b>(
// TODO tip?
match entries {
[] => unreachable!(),
[first] => alloc
.reflow("The ")
.append(alloc.symbol_unqualified(first.symbol))
.append(alloc.reflow(
" value is defined directly in terms of itself, causing an infinite loop.",
)),
[CycleEntry { symbol, symbol_region, expr_region }] =>
alloc.stack([
alloc.concat([
alloc.symbol_unqualified(*symbol),
alloc.reflow(" is defined directly in terms of itself:"),
]),
alloc.region(lines.convert_region(Region::span_across(symbol_region, expr_region))),
alloc.concat([
alloc.reflow("Since Roc evaluates values strict, running this program would create an infinite number of "),
alloc.symbol_unqualified(*symbol),
alloc.reflow(" values!"),
]),
alloc.hint("").append(alloc.concat([
alloc.reflow("Did you mean to define "),alloc.symbol_unqualified(*symbol),alloc.reflow(" as a function?"),
])),
]),
[first, others @ ..] => {
alloc.stack([
alloc

View file

@ -3100,6 +3100,33 @@ fn to_header_report<'a>(
}
}
EHeader::InconsistentModuleName(region) => {
let doc = alloc.stack([
alloc.reflow(
r"This module name does not correspond with the file path it is defined in:",
),
alloc.region(lines.convert_region(*region)),
alloc.concat([
alloc.reflow("Module names must correspond with the file paths they are defined in. For example, I expect to see "),
alloc.parser_suggestion("BigNum"),
alloc.reflow(" defined in "),
alloc.parser_suggestion("BigNum.roc"),
alloc.reflow(", or "),
alloc.parser_suggestion("Math.Sin"),
alloc.reflow(" defined in "),
alloc.parser_suggestion("Math/Sin.roc"),
alloc.reflow("."),
]),
]);
Report {
filename,
doc,
title: "WEIRD MODULE NAME".to_string(),
severity: Severity::RuntimeError,
}
}
EHeader::AppName(_, pos) => {
let surroundings = Region::new(start, *pos);
let region = LineColumnRegion::from_pos(lines.convert_pos(*pos));

View file

@ -2036,6 +2036,7 @@ pub enum Problem {
BadRigidVar(Lowercase, ErrorType, Option<Symbol>),
OptionalRequiredMismatch(Lowercase),
OpaqueComparedToNonOpaque,
BoolVsBoolTag(TagName),
}
fn problems_to_tip<'b>(
@ -2579,6 +2580,23 @@ fn to_diff<'b>(
}
}
(Alias(Symbol::BOOL_BOOL, _, _, _), TagUnion(tags, _)) | (TagUnion(tags, _), Alias(Symbol::BOOL_BOOL, _, _, _))
if tags.len() == 1
&& tags.keys().all(|t| t.0.as_str() == "True" || t.0.as_str() == "False") =>
{
let written_tag = tags.keys().next().unwrap().clone();
let (left, left_able) = to_doc(alloc, Parens::InFn, type1);
let (right, right_able) = to_doc(alloc, Parens::InFn, type2);
Diff {
left,
right,
status: Status::Different(vec![Problem::BoolVsBoolTag(written_tag)]),
left_able,
right_able,
}
}
(Alias(sym, _, _, AliasKind::Opaque), _) | (_, Alias(sym, _, _, AliasKind::Opaque))
// Skip the hint for numbers; it's not as useful as saying "this type is not a number"
if !OPAQUE_NUM_SYMBOLS.contains(&sym)
@ -3707,6 +3725,18 @@ fn type_problem_to_pretty<'b>(
alloc.type_str("@Age 23"),
alloc.reflow("."),
])),
(BoolVsBoolTag(tag), _) => alloc.tip().append(alloc.concat([
alloc.reflow("Did you mean to use "),
alloc.symbol_qualified(if tag.0.as_str() == "True" {
Symbol::BOOL_TRUE
} else {
Symbol::BOOL_FALSE
}),
alloc.reflow(" rather than "),
alloc.tag_name(tag),
alloc.reflow("?"),
])),
}
}

View file

@ -1,6 +1,6 @@
use roc_module::ident::Ident;
use roc_module::ident::{Lowercase, ModuleName, TagName, Uppercase};
use roc_module::symbol::{Interns, ModuleId, Symbol};
use roc_module::symbol::{Interns, ModuleId, PQModuleName, PackageQualified, Symbol};
use roc_region::all::LineColumnRegion;
use std::fmt;
use std::path::{Path, PathBuf};
@ -458,6 +458,15 @@ impl<'a> RocDocAllocator<'a> {
self.text(name).annotate(Annotation::Module)
}
pub fn pq_module_name(&'a self, name: PQModuleName<'a>) -> DocBuilder<'a, Self, Annotation> {
let name = match name {
PackageQualified::Unqualified(n) => n.to_string(),
PackageQualified::Qualified(prefix, n) => format!("{prefix}.{n}"),
};
self.text(name).annotate(Annotation::Module)
}
pub fn module_name(&'a self, name: ModuleName) -> DocBuilder<'a, Self, Annotation> {
let name = if name.is_empty() {
// Render the app module as "app"

View file

@ -2158,12 +2158,19 @@ mod test_reporting {
f
"#
),
@r#"
@r###"
CIRCULAR DEFINITION /code/proj/Main.roc
The `f` value is defined directly in terms of itself, causing an
infinite loop.
"#
`f` is defined directly in terms of itself:
4 f = f
^^^^^
Since Roc evaluates values strict, running this program would create
an infinite number of `f` values!
Hint: Did you mean to define `f` as a function?
"###
);
// invalid mutual recursion
@ -3616,7 +3623,7 @@ mod test_reporting {
Set
List
Dict
Result
Hash
SYNTAX PROBLEM /code/proj/Main.roc
@ -7559,31 +7566,30 @@ All branches in an `if` must have the same type!
);
test_report!(
#[ignore = "Blocked on https://github.com/roc-lang/roc/issues/3385"]
unimported_modules_reported,
indoc!(
r#"
main : Task.Task {} []
main = "whatever man you don't even know my type"
main
alt : Task.Task {} []
alt = "whatever man you don't even know my type"
alt
"#
),
@r#"
@r###"
MODULE NOT IMPORTED /code/proj/Main.roc
The `Task` module is not imported:
1 main : Task.Task {} []
4 alt : Task.Task {} []
^^^^^^^^^^^^^^^
Is there an import missing? Perhaps there is a typo. Did you mean one
of these?
Test
Hash
List
Num
Box
"#
"###
);
test_report!(
@ -8183,16 +8189,16 @@ All branches in an `if` must have the same type!
r#"
app "test" provides [] to "./platform"
Hash a b c has
hash : a -> U64 | a has Hash
MHash a b c has
hash : a -> U64 | a has MHash
"#
),
@r#"
@r###"
ABILITY HAS TYPE VARIABLES /code/proj/Main.roc
The definition of the `Hash` ability includes type variables:
The definition of the `MHash` ability includes type variables:
3 Hash a b c has
3 MHash a b c has
^^^^^
Abilities cannot depend on type variables, but their member values
@ -8200,14 +8206,14 @@ All branches in an `if` must have the same type!
UNUSED DEFINITION /code/proj/Main.roc
`Hash` is not used anywhere in your code.
`MHash` is not used anywhere in your code.
3 Hash a b c has
^^^^
3 MHash a b c has
^^^^^
If you didn't intend on using `Hash` then remove it so future readers of
your code don't wonder why it is there.
"#
If you didn't intend on using `MHash` then remove it so future readers
of your code don't wonder why it is there.
"###
);
test_report!(
@ -8216,17 +8222,17 @@ All branches in an `if` must have the same type!
r#"
app "test" provides [hash] to "./platform"
Hash has hash : a, b -> Num.U64 | a has Hash, b has Bool.Bool
MHash has hash : a, b -> Num.U64 | a has MHash, b has Bool.Bool
"#
),
@r#"
@r###"
HAS CLAUSE IS NOT AN ABILITY /code/proj/Main.roc
The type referenced in this "has" clause is not an ability:
3 Hash has hash : a, b -> Num.U64 | a has Hash, b has Bool.Bool
3 MHash has hash : a, b -> Num.U64 | a has MHash, b has Bool.Bool
^^^^^^^^^
"#
"###
);
test_report!(
@ -8354,36 +8360,36 @@ All branches in an `if` must have the same type!
r#"
app "test" provides [f] to "./platform"
Hash has hash : (a | a has Hash) -> Num.U64
MHash has hash : (a | a has MHash) -> Num.U64
f : a -> Num.U64 | a has Hash
f : a -> Num.U64 | a has MHash
"#
),
@r#"
@r###"
ILLEGAL HAS CLAUSE /code/proj/Main.roc
A `has` clause is not allowed here:
3 Hash has hash : (a | a has Hash) -> Num.U64
^^^^^^^^^^
3 MHash has hash : (a | a has MHash) -> Num.U64
^^^^^^^^^^^
`has` clauses can only be specified on the top-level type annotations.
ABILITY MEMBER MISSING HAS CLAUSE /code/proj/Main.roc
The definition of the ability member `hash` does not include a `has`
clause binding a type variable to the ability `Hash`:
clause binding a type variable to the ability `MHash`:
3 Hash has hash : (a | a has Hash) -> Num.U64
3 MHash has hash : (a | a has MHash) -> Num.U64
^^^^
Ability members must include a `has` clause binding a type variable to
an ability, like
a has Hash
a has MHash
Otherwise, the function does not need to be part of the ability!
"#
"###
);
test_report!(
@ -8392,9 +8398,9 @@ All branches in an `if` must have the same type!
r#"
app "test" provides [hash] to "./platform"
Hash has hash : a -> U64 | a has Hash
MHash has hash : a -> U64 | a has MHash
Id := U32 has [Hash {hash}]
Id := U32 has [MHash {hash}]
hash = \@Id n -> n
"#
@ -8452,8 +8458,8 @@ All branches in an `if` must have the same type!
r#"
app "test" provides [hash] to "./platform"
Hash has
hash : a -> U64 | a has Hash
MHash has
hash : a -> U64 | a has MHash
hash = \_ -> 0u64
"#
@ -8477,11 +8483,11 @@ All branches in an `if` must have the same type!
r#"
app "test" provides [hash, One, Two] to "./platform"
Hash has
hash : a -> U64 | a has Hash
MHash has
hash : a -> U64 | a has MHash
One := {} has [Hash {hash}]
Two := {} has [Hash {hash}]
One := {} has [MHash {hash}]
Two := {} has [MHash {hash}]
hash = \_ -> 0u64
"#
@ -8493,7 +8499,7 @@ All branches in an `if` must have the same type!
This ability member specialization is already claimed to specialize
another opaque type:
7 Two := {} has [Hash {hash}]
7 Two := {} has [MHash {hash}]
^^^^
Previously, we found it to specialize `hash` for `One`.
@ -8514,7 +8520,7 @@ All branches in an `if` must have the same type!
But the type annotation on `hash` says it must match:
a -> U64 | a has Hash
a -> U64 | a has MHash
Note: The specialized type is too general, and does not provide a
concrete type where a type variable is bound to an ability.
@ -8531,11 +8537,11 @@ All branches in an `if` must have the same type!
r#"
app "test" provides [hash, One, Two] to "./platform"
Hash has
hash : a -> U64 | a has Hash
MHash has
hash : a -> U64 | a has MHash
One := {} has [Hash {hash}]
Two := {} has [Hash {hash}]
One := {} has [MHash {hash}]
Two := {} has [MHash {hash}]
hash = \@One _ -> 0u64
"#
@ -8546,7 +8552,7 @@ All branches in an `if` must have the same type!
This ability member specialization is already claimed to specialize
another opaque type:
7 Two := {} has [Hash {hash}]
7 Two := {} has [MHash {hash}]
^^^^
Previously, we found it to specialize `hash` for `One`.
@ -8587,10 +8593,7 @@ All branches in an `if` must have the same type!
You, You -> Bool
Tip: Type comparisons between an opaque type are only ever equal if
both types are the same opaque type. Did you mean to create an opaque
type by wrapping it? If I have an opaque type Age := U32 I can create
an instance of this opaque type by doing @Age 23.
Tip: Did you mean to use `Bool.false` rather than `False`?
"###
);
@ -8600,10 +8603,10 @@ All branches in an `if` must have the same type!
r#"
app "test" provides [hash] to "./platform"
Hash has
hash : a -> U64 | a has Hash
MHash has
hash : a -> U64 | a has MHash
Id := U64 has [Hash {hash}]
Id := U64 has [MHash {hash}]
hash : Id -> U32
hash = \@Id n -> n
@ -8649,10 +8652,10 @@ All branches in an `if` must have the same type!
r#"
app "test" provides [noGoodVeryBadTerrible] to "./platform"
Hash has
hash : a -> U64 | a has Hash
MHash has
hash : a -> U64 | a has MHash
Id := U64 has [Hash {hash}]
Id := U64 has [MHash {hash}]
hash = \@Id n -> n
@ -8673,7 +8676,7 @@ All branches in an `if` must have the same type!
15 notYet: hash (A 1),
^^^
Roc can't generate an implementation of the `#UserApp.Hash` ability for
Roc can't generate an implementation of the `#UserApp.MHash` ability for
[A (Num a)]b
@ -8686,7 +8689,7 @@ All branches in an `if` must have the same type!
14 nope: hash (@User {}),
^^^^^^^^
The type `User` does not fully implement the ability `Hash`.
The type `User` does not fully implement the ability `MHash`.
"###
);
@ -8697,8 +8700,8 @@ All branches in an `if` must have the same type!
app "test" provides [main] to "./platform"
main =
Hash has
hash : a -> U64 | a has Hash
MHash has
hash : a -> U64 | a has MHash
123
"#
@ -8708,8 +8711,8 @@ All branches in an `if` must have the same type!
This ability definition is not on the top-level of a module:
4> Hash has
5> hash : a -> U64 | a has Hash
4> MHash has
5> hash : a -> U64 | a has MHash
Abilities can only be defined on the top-level of a Roc module.
"#
@ -8721,22 +8724,22 @@ All branches in an `if` must have the same type!
r#"
app "test" provides [hash, hashable] to "./platform"
Hash has
hash : a -> U64 | a has Hash
MHash has
hash : a -> U64 | a has MHash
Id := U64 has [Hash {hash}]
Id := U64 has [MHash {hash}]
hash = \@Id n -> n
hashable : a | a has Hash
hashable : a | a has MHash
hashable = @Id 15
"#
),
@r#"
@r###"
TYPE MISMATCH /code/proj/Main.roc
Something is off with the body of the `hashable` definition:
9 hashable : a | a has Hash
9 hashable : a | a has MHash
10 hashable = @Id 15
^^^^^^
@ -8746,14 +8749,14 @@ All branches in an `if` must have the same type!
But the type annotation on `hashable` says it should be:
a | a has Hash
a | a has MHash
Tip: The type annotation uses the type variable `a` to say that this
definition can produce any value implementing the `Hash` ability. But in
the body I see that it will only produce a `Id` value of a single
definition can produce any value implementing the `MHash` ability. But
in the body I see that it will only produce a `Id` value of a single
specific type. Maybe change the type annotation to be more specific?
Maybe change the code to be more general?
"#
"###
);
test_report!(
@ -8762,50 +8765,50 @@ All branches in an `if` must have the same type!
r#"
app "test" provides [result] to "./platform"
Hash has
hash : a -> U64 | a has Hash
MHash has
hash : a -> U64 | a has MHash
mulHashes : Hash, Hash -> U64
mulHashes = \x, y -> hash x * hash y
mulMHashes : MHash, MHash -> U64
mulMHashes = \x, y -> hash x * hash y
Id := U64 has [Hash {hash: hashId}]
Id := U64 has [MHash {hash: hashId}]
hashId = \@Id n -> n
Three := {} has [Hash {hash: hashThree}]
Three := {} has [MHash {hash: hashThree}]
hashThree = \@Three _ -> 3
result = mulHashes (@Id 100) (@Three {})
result = mulMHashes (@Id 100) (@Three {})
"#
),
@r#"
@r###"
ABILITY USED AS TYPE /code/proj/Main.roc
You are attempting to use the ability `Hash` as a type directly:
You are attempting to use the ability `MHash` as a type directly:
6 mulHashes : Hash, Hash -> U64
^^^^
6 mulMHashes : MHash, MHash -> U64
^^^^^
Abilities can only be used in type annotations to constrain type
variables.
Hint: Perhaps you meant to include a `has` annotation, like
a has Hash
a has MHash
ABILITY USED AS TYPE /code/proj/Main.roc
You are attempting to use the ability `Hash` as a type directly:
You are attempting to use the ability `MHash` as a type directly:
6 mulHashes : Hash, Hash -> U64
^^^^
6 mulMHashes : MHash, MHash -> U64
^^^^^
Abilities can only be used in type annotations to constrain type
variables.
Hint: Perhaps you meant to include a `has` annotation, like
b has Hash
"#
b has MHash
"###
);
test_report!(
@ -9732,9 +9735,9 @@ All branches in an `if` must have the same type!
F a b := b | a has Foo
Hash := {}
MHash := {}
x : F Hash {}
x : F MHash {}
"#
),
@r###"
@ -10090,7 +10093,7 @@ All branches in an `if` must have the same type!
r#"
app "test" provides [hash, Id] to "./platform"
Hash has hash : a -> U64 | a has Hash
MHash has hash : a -> U64 | a has MHash
Id := {}
@ -10116,9 +10119,9 @@ All branches in an `if` must have the same type!
r#"
app "test" provides [hash, Id, Id2] to "./platform"
Hash has hash : a -> U64 | a has Hash
MHash has hash : a -> U64 | a has MHash
Id := {} has [Hash {hash}]
Id := {} has [MHash {hash}]
Id2 := {}
hash = \@Id2 _ -> 0
@ -10768,4 +10771,305 @@ All branches in an `if` must have the same type!
@r###"
"###
);
test_report!(
invalid_toplevel_cycle,
indoc!(
r#"
app "test" imports [] provides [main] to "./platform"
main =
if Bool.true then \{} -> {} else main
"#
),
@r###"
CIRCULAR DEFINITION /code/proj/Main.roc
`main` is defined directly in terms of itself:
3> main =
4> if Bool.true then \{} -> {} else main
Since Roc evaluates values strict, running this program would create
an infinite number of `main` values!
Hint: Did you mean to define `main` as a function?
"###
);
test_report!(
bool_vs_true_tag,
indoc!(
r#"
if True then "" else ""
"#
),
@r###"
TYPE MISMATCH /code/proj/Main.roc
This `if` condition needs to be a Bool:
4 if True then "" else ""
^^^^
This `True` tag has the type:
[True]a
But I need every `if` condition to evaluate to a Booleither `Bool.true`
or `Bool.false`.
Tip: Did you mean to use `Bool.true` rather than `True`?
"###
);
test_report!(
bool_vs_false_tag,
indoc!(
r#"
if False then "" else ""
"#
),
@r###"
TYPE MISMATCH /code/proj/Main.roc
This `if` condition needs to be a Bool:
4 if False then "" else ""
^^^^^
This `False` tag has the type:
[False]a
But I need every `if` condition to evaluate to a Booleither `Bool.true`
or `Bool.false`.
Tip: Did you mean to use `Bool.false` rather than `False`?
"###
);
test_report!(
derive_hash_for_function,
indoc!(
r#"
app "test" provides [A] to "./platform"
A a := a -> a has [Hash]
"#
),
@r###"
INCOMPLETE ABILITY IMPLEMENTATION /code/proj/Main.roc
Roc can't derive an implementation of the `Hash.Hash` for `A`:
3 A a := a -> a has [Hash]
^^^^
Note: `Hash` cannot be generated for functions.
Tip: You can define a custom implementation of `Hash.Hash` for `A`.
"###
);
test_report!(
derive_hash_for_non_hash_opaque,
indoc!(
r#"
app "test" provides [A] to "./platform"
A := B has [Hash]
B := {}
"#
),
@r###"
INCOMPLETE ABILITY IMPLEMENTATION /code/proj/Main.roc
Roc can't derive an implementation of the `Hash.Hash` for `A`:
3 A := B has [Hash]
^^^^
Tip: `B` does not implement `Hash`. Consider adding a custom
implementation or `has Hash.Hash` to the definition of `B`.
Tip: You can define a custom implementation of `Hash.Hash` for `A`.
"###
);
test_report!(
derive_hash_for_other_has_hash,
indoc!(
r#"
app "test" provides [A] to "./platform"
A := B has [Hash]
B := {} has [Hash]
"#
),
@"" // no error
);
test_report!(
derive_hash_for_recursive_deriving,
indoc!(
r#"
app "test" provides [MyNat] to "./platform"
MyNat := [S MyNat, Z] has [Hash]
"#
),
@"" // no error
);
test_report!(
derive_hash_for_record,
indoc!(
r#"
app "test" provides [main] to "./platform"
foo : a -> {} | a has Hash
main = foo {a: "", b: 1}
"#
),
@"" // no error
);
test_report!(
derive_hash_for_tag,
indoc!(
r#"
app "test" provides [main] to "./platform"
foo : a -> {} | a has Hash
t : [A {}, B U8 U64, C Str]
main = foo t
"#
),
@"" // no error
);
test_report!(
cannot_derive_hash_for_function,
indoc!(
r#"
app "test" provides [main] to "./platform"
foo : a -> {} | a has Hash
main = foo (\x -> x)
"#
),
@r###"
TYPE MISMATCH /code/proj/Main.roc
This expression has a type that does not implement the abilities it's expected to:
5 main = foo (\x -> x)
^^^^^^^
Roc can't generate an implementation of the `Hash.Hash` ability for
a -> a
Note: `Hash` cannot be generated for functions.
"###
);
test_report!(
cannot_derive_hash_for_structure_containing_function,
indoc!(
r#"
app "test" provides [main] to "./platform"
foo : a -> {} | a has Hash
main = foo (A (\x -> x) B)
"#
),
@r###"
TYPE MISMATCH /code/proj/Main.roc
This expression has a type that does not implement the abilities it's expected to:
5 main = foo (A (\x -> x) B)
^^^^^^^^^^^^^
Roc can't generate an implementation of the `Hash.Hash` ability for
[A (a -> a) [B]a]b
In particular, an implementation for
a -> a
cannot be generated.
Note: `Hash` cannot be generated for functions.
"###
);
test_report!(
shift_by_negative,
indoc!(
r#"
{
a: Num.shiftLeftBy 1 -1,
b: Num.shiftRightBy 1 -1,
c: Num.shiftRightZfBy 1 -1,
}
"#
),
@r###"
TYPE MISMATCH /code/proj/Main.roc
This 2nd argument to `shiftRightZfBy` has an unexpected type:
7 c: Num.shiftRightZfBy 1 -1,
^^
The argument is a number of type:
I8, I16, F32, I32, F64, I64, I128, or Dec
But `shiftRightZfBy` needs its 2nd argument to be:
U8
TYPE MISMATCH /code/proj/Main.roc
This 2nd argument to `shiftRightBy` has an unexpected type:
6 b: Num.shiftRightBy 1 -1,
^^
The argument is a number of type:
I8, I16, F32, I32, F64, I64, I128, or Dec
But `shiftRightBy` needs its 2nd argument to be:
U8
TYPE MISMATCH /code/proj/Main.roc
This 2nd argument to `shiftLeftBy` has an unexpected type:
5 a: Num.shiftLeftBy 1 -1,
^^
The argument is a number of type:
I8, I16, F32, I32, F64, I64, I128, or Dec
But `shiftLeftBy` needs its 2nd argument to be:
U8
"###
);
}

View file

@ -9,6 +9,7 @@ interface Arg
bool,
str,
i64,
positional,
subCommand,
choice,
withParser,
@ -31,7 +32,8 @@ NamedParser a := {
## needs, consider transforming it into a [NamedParser].
Parser a := [
Succeed a,
Arg Config (List Str -> Result a [NotFound Str, WrongType { arg : Str, expected : Type }]),
Arg ArgConfig (MarkedArgs -> Result { newlyTaken : Taken, val : a } (ParseError [])),
Positional PositionalConfig (MarkedArgs -> Result { newlyTaken : Taken, val : a } (ParseError [])),
# TODO: hiding the record behind an alias currently causes a panic
SubCommand
(List {
@ -44,12 +46,23 @@ Parser a := [
Lazy ({} -> a),
]
## Indices in an arguments list that have already been parsed.
Taken : Set Nat
## A representation of parsed and unparsed arguments in a constant list of
## command-line arguments.
## Used only internally, for efficient representation of parsed and unparsed
## arguments.
MarkedArgs : { args : List Str, taken : Taken }
## Enumerates errors that can occur during parsing a list of command line arguments.
ParseError a : [
## The program name was not found as the first argument to be parsed.
ProgramNameNotProvided Str,
## An argument is required, but it was not found.
MissingRequiredArg Str,
## A positional argument is required, but it was not found.
MissingPositionalArg Str,
## An argument was found, but it didn't have the expected [Type].
WrongType
{
@ -83,13 +96,20 @@ Help : [
Config (List Config),
]
Config : {
ArgConfig : {
long : Str,
short : Str,
help : Str,
type : Type,
}
PositionalConfig : {
name : Str,
help : Str,
}
Config : [Arg ArgConfig, Positional PositionalConfig]
## Generates help metadata from a [Parser].
##
## This is useful if you would like to use this metadata to generate your own
@ -114,7 +134,7 @@ toHelpHelper = \@Parser parser, configs ->
toHelpHelper innerParser (List.append configs config)
Arg config _ ->
List.append configs config
List.append configs (Arg config)
|> Config
SubCommand commands ->
@ -123,24 +143,39 @@ toHelpHelper = \@Parser parser, configs ->
(\{ name, parser: innerParser } -> { name, help: toHelpHelper innerParser [] })
|> SubCommands
findOneArg : Str, Str, List Str -> Result Str [NotFound]*
findOneArg = \long, short, args ->
argMatches = \arg ->
if arg == "--\(long)" then
Bool.true
Positional config _ ->
List.append configs (Positional config)
|> Config
findOneArg : Str, Str, MarkedArgs -> Result { val : Str, newlyTaken : Taken } [NotFound]*
findOneArg = \long, short, { args, taken } ->
argMatches = \{ index, found: _ }, arg ->
if Set.contains taken index || Set.contains taken (index + 1) then
Continue { index: index + 1, found: Bool.false }
else if arg == "--\(long)" then
Break { index, found: Bool.true }
else if Bool.not (Str.isEmpty short) && arg == "-\(short)" then
Break { index, found: Bool.true }
else
Bool.not (Str.isEmpty short) && arg == "-\(short)"
Continue { index: index + 1, found: Bool.false }
# TODO allow = as well, etc.
result = List.findFirstIndex args argMatches
{ index: argIndex, found } = List.walkUntil args { index: 0, found: Bool.false } argMatches
when result is
Ok index ->
if !found then
Err NotFound
else
# Return the next arg after the given one
List.get args (index + 1)
|> Result.mapErr \_ -> NotFound
List.get args (argIndex + 1)
|> Result.mapErr (\_ -> NotFound)
|> Result.map
(\val ->
newUsed = Set.fromList [argIndex, argIndex + 1]
Err NotFound -> Err NotFound
{ val, newlyTaken: newUsed })
updateTaken : MarkedArgs, Taken -> MarkedArgs
updateTaken = \{ args, taken }, taken2 -> { args, taken: Set.union taken taken2 }
# andMap : Parser a, Parser (a -> b) -> Parser b
andMap = \@Parser parser, @Parser mapper ->
@ -162,7 +197,12 @@ andMap = \@Parser parser, @Parser mapper ->
Arg config run ->
Arg config \args ->
run args
|> Result.map fn
|> Result.map (\{ val, newlyTaken } -> { val: fn val, newlyTaken })
Positional config run ->
Positional config \args ->
run args
|> Result.map (\{ val, newlyTaken } -> { val: fn val, newlyTaken })
SubCommand cmds ->
mapSubParser = \{ name, parser: parser2 } ->
@ -176,13 +216,13 @@ andMap = \@Parser parser, @Parser mapper ->
Succeed a ->
Arg config \args ->
when run args is
Ok fn -> Ok (fn a)
Ok { val: fn, newlyTaken } -> Ok { val: fn a, newlyTaken }
Err err -> Err err
Lazy thunk ->
Arg config \args ->
when run args is
Ok fn -> Ok (fn (thunk {}))
Ok { val: fn, newlyTaken } -> Ok { val: fn (thunk {}), newlyTaken }
Err err -> Err err
WithConfig parser2 config2 ->
@ -194,12 +234,83 @@ andMap = \@Parser parser, @Parser mapper ->
# Parse first the one and then the other.
combinedParser = Arg config2 \args ->
when run args is
Ok fn -> run2 args |> Result.map fn
Ok { val: fn, newlyTaken } ->
run2 (updateTaken args newlyTaken)
|> Result.map (\{ val, newlyTaken: newlyTaken2 } -> { val: fn val, newlyTaken: Set.union newlyTaken newlyTaken2 })
Err err -> Err err
# Store the extra config.
@Parser combinedParser
|> WithConfig config
|> WithConfig (Arg config)
Positional config2 run2 ->
combinedParser = Positional config2 \args ->
when run args is
Ok { val: fn, newlyTaken } ->
run2 (updateTaken args newlyTaken)
|> Result.map (\{ val, newlyTaken: newlyTaken2 } -> { val: fn val, newlyTaken: Set.union newlyTaken newlyTaken2 })
Err err -> Err err
# Store the extra config.
@Parser combinedParser
|> WithConfig (Arg config)
SubCommand cmds ->
# For each subcommand, first run the subcommand, then
# push the result through the arg parser.
mapSubParser = \{ name, parser: parser2 } ->
{ name, parser: andMap parser2 (@Parser mapper) }
List.map cmds mapSubParser
|> SubCommand
Positional config run ->
when parser is
Succeed a ->
Positional config \args ->
when run args is
Ok { val: fn, newlyTaken } -> Ok { val: fn a, newlyTaken }
Err err -> Err err
Lazy thunk ->
Positional config \args ->
when run args is
Ok { val: fn, newlyTaken } -> Ok { val: fn (thunk {}), newlyTaken }
Err err -> Err err
WithConfig parser2 config2 ->
parser2
|> andMap (@Parser mapper)
|> WithConfig config2
Arg config2 run2 ->
# Parse first the one and then the other.
combinedParser = Arg config2 \args ->
when run args is
Ok { val: fn, newlyTaken } ->
run2 (updateTaken args newlyTaken)
|> Result.map (\{ val, newlyTaken: newlyTaken2 } -> { val: fn val, newlyTaken: Set.union newlyTaken newlyTaken2 })
Err err -> Err err
# Store the extra config.
@Parser combinedParser
|> WithConfig (Positional config)
Positional config2 run2 ->
combinedParser = Positional config2 \args ->
when run args is
Ok { val: fn, newlyTaken } ->
run2 (updateTaken args newlyTaken)
|> Result.map (\{ val, newlyTaken: newlyTaken2 } -> { val: fn val, newlyTaken: Set.union newlyTaken newlyTaken2 })
Err err -> Err err
# Store the extra config.
@Parser combinedParser
|> WithConfig (Positional config)
SubCommand cmds ->
# For each subcommand, first run the subcommand, then
@ -228,7 +339,12 @@ andMap = \@Parser parser, @Parser mapper ->
Arg config run ->
Arg config \args ->
run args
|> Result.map fn
|> Result.map (\{ val, newlyTaken } -> { val: fn val, newlyTaken })
Positional config run ->
Positional config \args ->
run args
|> Result.map (\{ val, newlyTaken } -> { val: fn val, newlyTaken })
SubCommand cmds ->
mapSubParser = \{ name, parser: parser2 } ->
@ -275,22 +391,26 @@ parse = \@NamedParser parser, args ->
then
Err (ProgramNameNotProvided parser.name)
else
parseHelp parser.parser (List.split args 1).others
markedArgs = { args, taken: Set.single 0 }
parseHelp : Parser a, List Str -> Result a (ParseError *)
parseHelp parser.parser markedArgs
parseHelp : Parser a, MarkedArgs -> Result a (ParseError [])
parseHelp = \@Parser parser, args ->
when parser is
Succeed val -> Ok val
Arg _ run ->
when run args is
Ok val -> Ok val
Err (NotFound long) -> Err (MissingRequiredArg long)
Err (WrongType { arg, expected }) -> Err (WrongType { arg, expected })
run args
|> Result.map .val
Positional _ run ->
run args
|> Result.map .val
SubCommand cmds ->
when List.get args 0 is
Ok cmd ->
argsRest = (List.split args 1).others
when nextUnmarked args is
Ok { index, val: cmd } ->
argsRest = { args & taken: Set.insert args.taken index }
state =
List.walkUntil
cmds
@ -313,6 +433,17 @@ parseHelp = \@Parser parser, args ->
WithConfig parser2 _config ->
parseHelp parser2 args
nextUnmarked : MarkedArgs -> Result { index : Nat, val : Str } [OutOfBounds]
nextUnmarked = \marked ->
help = \index ->
if Set.contains marked.taken index then
help (index + 1)
else
List.get marked.args index
|> Result.map \val -> { index, val }
help 0
## Creates a parser for a boolean flag argument.
## Flags of value "true" and "false" will be parsed as [Bool.true] and [Bool.false], respectively.
## All other values will result in a `WrongType` error.
@ -320,10 +451,12 @@ bool : _ -> Parser Bool # TODO: panics if parameter annotation given
bool = \{ long, short ? "", help ? "" } ->
fn = \args ->
when findOneArg long short args is
Err NotFound -> Err (NotFound long)
Ok "true" -> Ok Bool.true
Ok "false" -> Ok Bool.false
Ok _ -> Err (WrongType { arg: long, expected: Bool })
Err NotFound -> Err (MissingRequiredArg long)
Ok { val, newlyTaken } ->
when val is
"true" -> Ok { val: Bool.true, newlyTaken }
"false" -> Ok { val: Bool.false, newlyTaken }
_ -> Err (WrongType { arg: long, expected: Bool })
@Parser (Arg { long, short, help, type: Bool } fn)
@ -332,8 +465,8 @@ str : _ -> Parser Str # TODO: panics if parameter annotation given
str = \{ long, short ? "", help ? "" } ->
fn = \args ->
when findOneArg long short args is
Err NotFound -> Err (NotFound long)
Ok foundArg -> Ok foundArg
Err NotFound -> Err (MissingRequiredArg long)
Ok { val, newlyTaken } -> Ok { val, newlyTaken }
@Parser (Arg { long, short, help, type: Str } fn)
@ -342,13 +475,24 @@ i64 : _ -> Parser I64 # TODO: panics if parameter annotation given
i64 = \{ long, short ? "", help ? "" } ->
fn = \args ->
when findOneArg long short args is
Err NotFound -> Err (NotFound long)
Ok foundArg ->
Str.toI64 foundArg
|> Result.mapErr \_ -> WrongType { arg: long, expected: I64 }
Err NotFound -> Err (MissingRequiredArg long)
Ok { val, newlyTaken } ->
Str.toI64 val
|> Result.mapErr (\_ -> WrongType { arg: long, expected: I64 })
|> Result.map (\v -> { val: v, newlyTaken })
@Parser (Arg { long, short, help, type: I64 } fn)
## Parses a single positional argument as a string.
positional : _ -> Parser Str
positional = \{ name, help ? "" } ->
fn = \args ->
nextUnmarked args
|> Result.mapErr (\OutOfBounds -> MissingPositionalArg name)
|> Result.map (\{ val, index } -> { val, newlyTaken: Set.insert args.taken index })
@Parser (Positional { name, help } fn)
## Wraps a given parser as a subcommand parser.
##
## When parsing arguments, the subcommand name will be expected to be parsed
@ -390,6 +534,13 @@ indentLevel = 4
mapNonEmptyStr = \s, f -> if Str.isEmpty s then s else f s
filterMap : List a, (a -> [Some b, None]) -> List b
filterMap = \lst, transform ->
List.walk lst [] \all, elem ->
when transform elem is
Some v -> List.append all v
None -> all
# formatHelp : NamedParser a -> Str
formatHelp = \@NamedParser { name, help, parser } ->
fmtHelp =
@ -397,40 +548,88 @@ formatHelp = \@NamedParser { name, help, parser } ->
cmdHelp = toHelp parser
fmtCmdHeading =
when cmdHelp is
SubCommands _ -> "COMMANDS:"
Config _ -> "OPTIONS:"
fmtCmdHelp = formatCmdHelp indentLevel cmdHelp
fmtCmdHelp = formatHelpHelp 0 cmdHelp
"""
\(name)\(fmtHelp)
\(fmtCmdHeading)
\(fmtCmdHelp)
"""
# formatCmdHelp : Nat, Help -> Str <- TODO: layout-gen panics when the following annotation is applied!
formatCmdHelp = \n, help ->
when help is
# formatHelpHelp : Nat, Help -> Str
formatHelpHelp = \n, cmdHelp ->
indented = indent n
when cmdHelp is
SubCommands cmds ->
fmtCmdHelp =
Str.joinWith
(List.map cmds \subCmd -> formatSubCommand n subCmd)
(List.map cmds \subCmd -> formatSubCommand (n + indentLevel) subCmd)
"\n\n"
"""
\(indented)COMMANDS:
\(fmtCmdHelp)
"""
Config configs ->
Str.joinWith (List.map configs \c -> formatConfig n c) "\n"
argConfigs =
filterMap
configs
(\config ->
when config is
Arg c -> Some c
_ -> None)
positionalConfigs =
filterMap
configs
(\config ->
when config is
Positional c -> Some c
_ -> None)
fmtArgsHelp =
if List.isEmpty argConfigs then
""
else
helpStr =
argConfigs
|> List.map (\c -> formatArgConfig (n + indentLevel) c)
|> Str.joinWith "\n"
"""
\(indented)OPTIONS:
\(helpStr)
"""
fmtPositionalsHelp =
if List.isEmpty positionalConfigs then
""
else
helpStr =
positionalConfigs
|> List.map (\c -> formatPositionalConfig (n + indentLevel) c)
|> Str.joinWith "\n"
"""
\(indented)POSITIONAL ARGUMENTS:
\(helpStr)
"""
Str.concat fmtArgsHelp fmtPositionalsHelp
formatSubCommand = \n, { name, help } ->
indented = indent n
fmtHelp = formatCmdHelp (n + indentLevel) help
fmtHelp = formatHelpHelp (n + indentLevel) help
"\(indented)\(name)\n\(fmtHelp)"
"\(indented)\(name)\(fmtHelp)"
formatConfig : Nat, Config -> Str
formatConfig = \n, { long, short, help, type } ->
formatArgConfig : Nat, ArgConfig -> Str
formatArgConfig = \n, { long, short, help, type } ->
indented = indent n
formattedShort =
@ -443,6 +642,15 @@ formatConfig = \n, { long, short, help, type } ->
"\(indented)--\(long)\(formattedShort)\(formattedHelp) (\(formattedType))"
formatPositionalConfig : Nat, PositionalConfig -> Str
formatPositionalConfig = \n, { name, help } ->
indented = indent n
formattedHelp =
mapNonEmptyStr help \h -> " \(h)"
"\(indented)\(name)\(formattedHelp)"
formatType : Type -> Str
formatType = \type ->
when type is
@ -461,6 +669,9 @@ formatError = \err ->
MissingRequiredArg arg ->
"Argument `--\(arg)` is required but was not provided!"
MissingPositionalArg arg ->
"A positional argument for `\(arg)` is required but was not provided!"
WrongType { arg, expected } ->
formattedType = formatType expected
@ -507,107 +718,109 @@ formatError = \err ->
## ```
withParser = \arg1, arg2 -> andMap arg2 arg1
mark = \args -> { args, taken: Set.empty }
# bool undashed long argument is missing
expect
parser = bool { long: "foo" }
parseHelp parser ["foo"] == Err (MissingRequiredArg "foo")
parseHelp parser (mark ["foo"]) == Err (MissingRequiredArg "foo")
# bool dashed long argument without value is missing
expect
parser = bool { long: "foo" }
parseHelp parser ["--foo"] == Err (MissingRequiredArg "foo")
parseHelp parser (mark ["--foo"]) == Err (MissingRequiredArg "foo")
# bool dashed long argument with value is determined true
expect
parser = bool { long: "foo" }
parseHelp parser ["--foo", "true"] == Ok Bool.true
parseHelp parser (mark ["--foo", "true"]) == Ok Bool.true
# bool dashed long argument with value is determined false
expect
parser = bool { long: "foo" }
parseHelp parser ["--foo", "false"] == Ok Bool.false
parseHelp parser (mark ["--foo", "false"]) == Ok Bool.false
# bool dashed long argument with value is determined wrong type
expect
parser = bool { long: "foo" }
parseHelp parser ["--foo", "not-a-bool"] == Err (WrongType { arg: "foo", expected: Bool })
parseHelp parser (mark ["--foo", "not-a-bool"]) == Err (WrongType { arg: "foo", expected: Bool })
# bool dashed short argument with value is determined true
expect
parser = bool { long: "foo", short: "F" }
parseHelp parser ["-F", "true"] == Ok Bool.true
parseHelp parser (mark ["-F", "true"]) == Ok Bool.true
# bool dashed short argument with value is determined false
expect
parser = bool { long: "foo", short: "F" }
parseHelp parser ["-F", "false"] == Ok Bool.false
parseHelp parser (mark ["-F", "false"]) == Ok Bool.false
# bool dashed short argument with value is determined wrong type
expect
parser = bool { long: "foo", short: "F" }
parseHelp parser ["-F", "not-a-bool"] == Err (WrongType { arg: "foo", expected: Bool })
parseHelp parser (mark ["-F", "not-a-bool"]) == Err (WrongType { arg: "foo", expected: Bool })
# string dashed long argument without value is missing
expect
parser = str { long: "foo" }
parseHelp parser ["--foo"] == Err (MissingRequiredArg "foo")
parseHelp parser (mark ["--foo"]) == Err (MissingRequiredArg "foo")
# string dashed long argument with value is determined
expect
parser = str { long: "foo" }
parseHelp parser ["--foo", "itsme"] == Ok "itsme"
parseHelp parser (mark ["--foo", "itsme"]) == Ok "itsme"
# string dashed short argument without value is missing
expect
parser = str { long: "foo", short: "F" }
parseHelp parser ["-F"] == Err (MissingRequiredArg "foo")
parseHelp parser (mark ["-F"]) == Err (MissingRequiredArg "foo")
# string dashed short argument with value is determined
expect
parser = str { long: "foo", short: "F" }
parseHelp parser ["-F", "itsme"] == Ok "itsme"
parseHelp parser (mark ["-F", "itsme"]) == Ok "itsme"
# i64 dashed long argument without value is missing
expect
parser = i64 { long: "foo" }
parseHelp parser ["--foo"] == Err (MissingRequiredArg "foo")
parseHelp parser (mark ["--foo"]) == Err (MissingRequiredArg "foo")
# i64 dashed long argument with value is determined positive
expect
parser = i64 { long: "foo" }
parseHelp parser ["--foo", "1234"] == Ok 1234
parseHelp parser (mark ["--foo", "1234"]) == Ok 1234
# i64 dashed long argument with value is determined negative
expect
parser = i64 { long: "foo" }
parseHelp parser ["--foo", "-1234"] == Ok -1234
parseHelp parser (mark ["--foo", "-1234"]) == Ok -1234
# i64 dashed short argument without value is missing
expect
parser = i64 { long: "foo", short: "F" }
parseHelp parser ["-F"] == Err (MissingRequiredArg "foo")
parseHelp parser (mark ["-F"]) == Err (MissingRequiredArg "foo")
# i64 dashed short argument with value is determined
expect
parser = i64 { long: "foo", short: "F" }
parseHelp parser ["-F", "1234"] == Ok 1234
parseHelp parser (mark ["-F", "1234"]) == Ok 1234
# two string parsers complete cases
expect
@ -622,7 +835,7 @@ expect
["--foo", "true", "--bar", "baz", "--other", "something"],
]
List.all cases \args -> parseHelp parser args == Ok "foo: true bar: baz"
List.all cases \args -> parseHelp parser (mark args) == Ok "foo: true bar: baz"
# one argument is missing out of multiple
expect
@ -633,8 +846,8 @@ expect
List.all
[
parseHelp parser ["--foo", "zaz"] == Err (MissingRequiredArg "bar"),
parseHelp parser ["--bar", "zaz"] == Err (MissingRequiredArg "foo"),
parseHelp parser (mark ["--foo", "zaz"]) == Err (MissingRequiredArg "bar"),
parseHelp parser (mark ["--bar", "zaz"]) == Err (MissingRequiredArg "foo"),
]
(\b -> b)
@ -648,16 +861,16 @@ expect
toHelp parser
== Config [
{ long: "foo", short: "", help: "the foo flag", type: Str },
{ long: "bar", short: "B", help: "", type: Str },
{ long: "bool", short: "", help: "", type: Bool },
Arg { long: "foo", short: "", help: "the foo flag", type: Str },
Arg { long: "bar", short: "B", help: "", type: Str },
Arg { long: "bool", short: "", help: "", type: Bool },
]
# format argument is missing
expect
parser = bool { long: "foo" }
when parseHelp parser ["foo"] is
when parseHelp parser (mark ["foo"]) is
Ok _ -> Bool.false
Err e ->
err = formatError e
@ -668,7 +881,7 @@ expect
expect
parser = bool { long: "foo" }
when parseHelp parser ["--foo", "12"] is
when parseHelp parser (mark ["--foo", "12"]) is
Ok _ -> Bool.false
Err e ->
err = formatError e
@ -719,10 +932,12 @@ expect
COMMANDS:
login
OPTIONS:
--user (string)
--pw (string)
publish
OPTIONS:
--file (string)
--url (string)
"""
@ -741,7 +956,6 @@ expect
COMMANDS:
login
"""
# subcommand parser
@ -786,7 +1000,7 @@ expect
parser =
choice [subCommand (succeed "") "auth", subCommand (succeed "") "publish"]
when parseHelp parser [] is
when parseHelp parser (mark []) is
Ok _ -> Bool.true
Err e ->
err = formatError e
@ -804,7 +1018,7 @@ expect
parser =
choice [subCommand (succeed "") "auth", subCommand (succeed "") "publish"]
when parseHelp parser ["logs"] is
when parseHelp parser (mark ["logs"]) is
Ok _ -> Bool.true
Err e ->
err = formatError e
@ -816,3 +1030,38 @@ expect
The available subcommands are:
\t"auth", "publish"
"""
# parse positional argument
expect
parser = positional { name: "foo" }
parseHelp parser (mark ["myArg"]) == Ok "myArg"
# parse positional argument with argument flag
expect
parser =
succeed (\foo -> \bar -> "foo: \(foo), bar: \(bar)")
|> withParser (str { long: "foo" })
|> withParser (positional { name: "bar" })
cases = [
["--foo", "true", "baz"],
["baz", "--foo", "true"],
]
List.all cases \args -> parseHelp parser (mark args) == Ok "foo: true, bar: baz"
# parse positional argument with subcommand
expect
parser = choice [
positional { name: "bar" }
|> subCommand "hello",
]
parseHelp parser (mark ["hello", "foo"]) == Ok "foo"
# missing positional argument
expect
parser = positional { name: "bar" }
parseHelp parser (mark []) == Err (MissingPositionalArg "bar")

View file

@ -12,6 +12,6 @@ update : Model, Event -> Model
update = \model, _ -> model
render : Model -> List Elem
render = \model -> [Text model.text]
render = \model -> [Text { text: model.text, top: 0, left: 0, size: 40, color: { r: 1, g: 1, b: 1, a: 1 } }]
program = { init, update, render }

View file

@ -6,8 +6,8 @@ Rgba : { r : F32, g : F32, b : F32, a : F32 }
Bounds : { height : F32, width : F32 }
Elem : [Rect { color : Rgba, left : F32, top : F32, width : F32, height : F32 }, Text Str]
Elem : [Rect { color : Rgba, left : F32, top : F32, width : F32, height : F32 }, Text { text : Str, color : Rgba, left : F32, top : F32, size : F32 }]
KeyCode : [Left, Right, Other]
KeyCode : [Left, Right, Other, Up, Down]
Event : [Resize { width : F32, height : F32 }, KeyDown KeyCode, KeyUp KeyCode, Tick U128]

View file

@ -455,7 +455,7 @@ fn to_drawable(
wgpu_glyph::HorizontalAlign::Left
});
let section = owned_section_from_str(text.as_str(), bounds, layout);
let section = owned_section_from_str(text.text.as_str(),text.color, text.size, bounds, layout);
// Calculate the bounds and offset by measuring glyphs
let text_bounds;
@ -481,7 +481,7 @@ fn to_drawable(
}
let drawable = Drawable {
pos: (0.0, 0.0).into(), // TODO store the pos in Text and read it here
pos: (text.left, text.top).into(),
bounds: text_bounds,
content: DrawableContent::Text(section, offset),
};
@ -493,13 +493,11 @@ fn to_drawable(
fn owned_section_from_str(
string: &str,
color: Rgba,
size: f32,
bounds: Bounds,
layout: wgpu_glyph::Layout<wgpu_glyph::BuiltInLineBreaker>,
) -> OwnedSection {
// TODO don't hardcode any of this!
let color = Rgba::WHITE;
let size: f32 = 40.0;
OwnedSection {
bounds: (bounds.width, bounds.height),
layout,

View file

@ -142,9 +142,11 @@ impl RocEvent {
#[allow(unused)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RocKeyCode {
Left = 0,
Down = 0,
Left,
Other,
Right,
Up,
}
impl From<VirtualKeyCode> for RocKeyCode {
@ -154,6 +156,8 @@ impl From<VirtualKeyCode> for RocKeyCode {
match keycode {
Left => RocKeyCode::Left,
Right => RocKeyCode::Right,
Up => RocKeyCode::Up,
Down => RocKeyCode::Down,
_ => RocKeyCode::Other,
}
}
@ -210,7 +214,7 @@ pub struct ElemId(*const RocElemEntry);
#[repr(C)]
pub union RocElemEntry {
pub rect: ManuallyDrop<RocRect>,
pub text: ManuallyDrop<RocStr>,
pub text: ManuallyDrop<RocText>,
}
#[repr(u8)]
@ -284,6 +288,16 @@ pub struct RocRect {
pub width: f32,
}
#[repr(C)]
#[derive(Debug, Clone)]
pub struct RocText {
pub text: RocStr,
pub color: Rgba,
pub left: f32,
pub size: f32,
pub top: f32,
}
impl Clone for RocElem {
fn clone(&self) -> Self {
unsafe {

View file

@ -212,7 +212,7 @@ function updateHistoryEntry(index, ok, outputText) {
outputElem.classList.add("output");
outputElem.classList.add(ok ? "output-ok" : "output-error");
const historyItem = repl.elemHistory.childNodes[index];
const historyItem = repl.elemHistory.children[index];
historyItem.appendChild(outputElem);
repl.elemHistory.scrollTop = repl.elemHistory.scrollHeight;