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

This commit is contained in:
Anton-4 2022-10-04 15:01:25 +02:00
commit dacf542942
No known key found for this signature in database
GPG key ID: A13F4A6E21141925
44 changed files with 1726 additions and 821 deletions

View file

@ -20,7 +20,7 @@ jobs:
steps:
- uses: actions/checkout@v3
with:
ref: "main"
# TODO re-enable>> ref: "main"
clean: "true"
- name: Earthly version

1
Cargo.lock generated
View file

@ -3791,6 +3791,7 @@ dependencies = [
"clap 3.2.20",
"iced-x86",
"indoc",
"libc",
"mach_object",
"memmap2 0.5.7",
"object 0.29.0",

10
FAQ.md
View file

@ -95,20 +95,20 @@ the function might give different answers.
Both of these would make revising code riskier across the entire language, which is very undesirable.
Another option would be to define that function equality always returns `False`. So both of these would evaluate
to `False`:
Another option would be to define that function equality always returns `false`. So both of these would evaluate
to `false`:
- `(\x -> x + 1) == (\x -> 1 + x)`
- `(\x -> x + 1) == (\x -> x + 1)`
This makes function equality effectively useless, while still technically allowing it. It has some other downsides:
- Now if you put a function inside a record, using `==` on that record will still type-check, but it will then return `False`. This could lead to bugs if you didn't realize you had accidentally put a function in there - for example, because you were actually storing a different type (e.g. an opaque type) and didn't realize it had a function inside it.
- Now if you put a function inside a record, using `==` on that record will still type-check, but it will then return `false`. This could lead to bugs if you didn't realize you had accidentally put a function in there - for example, because you were actually storing a different type (e.g. an opaque type) and didn't realize it had a function inside it.
- If you put a function (or a value containing a function) into a `Dict` or `Set`, you'll never be able to get it out again. This is a common problem with [NaN](https://en.wikipedia.org/wiki/NaN), which is also defined not to be equal to itself.
The first of these problems could be addressed by having function equality always return `True` instead of `False` (since that way it would not affect other fields' equality checks in a record), but that design has its own problems:
The first of these problems could be addressed by having function equality always return true instead of false (since that way it would not affect other fields' equality checks in a record), but that design has its own problems:
- Although function equality is still useless, `(\x -> x + 1) == (\x -> x)` returns `True`. Even if it didn't lead to bugs in practice, this would certainly be surprising and confusing to beginners.
- Although function equality is still useless, `(\x -> x + 1) == (\x -> x)` returns `Bool.true`. Even if it didn't lead to bugs in practice, this would certainly be surprising and confusing to beginners.
- Now if you put several different functions into a `Dict` or `Set`, only one of them will be kept; the others will be discarded or overwritten. This could cause bugs if a value stored a function internally, and then other functions relied on that internal function for correctness.
Each of these designs makes Roc a language that's some combination of more error-prone, more confusing, and more

View file

@ -4,7 +4,7 @@ Roc is not ready for a 0.1 release yet, but we do have:
- [**installation** guide](https://github.com/roc-lang/roc/tree/main/getting_started)
- [**tutorial**](https://github.com/roc-lang/roc/blob/main/TUTORIAL.md)
- [some docs for the standard library](https://www.roc-lang.org/builtins/Str)
- [**docs** for the standard library](https://www.roc-lang.org/builtins/Str)
- [frequently asked questions](https://github.com/roc-lang/roc/blob/main/FAQ.md)
- [Zulip chat](https://roc.zulipchat.com) for help, questions and discussions

View file

@ -280,6 +280,9 @@ addAndStringify = \num1, num2 ->
This code is equivalent to writing `else if sum < 0 then` on one line, although the stylistic
convention is to write `else if` on the same line.
> *Note*: In Roc, `if` conditions must always be booleans. (Roc doesn't have a concept of "truthiness,"
> so the compiler will report an error for conditionals like `if 1 then` or `if "true" then`.)
## Records
Currently our `addAndStringify` function takes two arguments. We can instead make
@ -431,12 +434,6 @@ the number `42` or the string `"forty-two"` without defining them first, I can a
the tag `FortyTwo` without defining it first. Also, similarly to how `42 == 42` and
`"forty-two" == "forty-two"`, it's also the case that `FortyTwo == FortyTwo`.
Speaking of equals, if we put `42 == 42` into `roc repl`, the output we'll see is `True`.
This is because booleans in Roc are tags; a boolean is either the tag `True` or the tag
`False`. So I can write `if True then` or `if False then` and it will work as expected,
even though I'd get an error if I wrote `if "true" then` or `if 1 then`. (Roc doesn't
have a concept of "truthiness" - you always have to use booleans for conditionals!)
Let's say we wanted to turn `stoplightColor` from a `Red`, `Green`, or `Yellow` into
a string. Here's one way we could do that:
@ -566,6 +563,25 @@ We refer to whatever comes before a `->` in a `when` expression as a *pattern* -
patterns in branching conditionals like `when` is known as [pattern matching](https://en.wikipedia.org/wiki/Pattern_matching). You may hear people say things like "let's pattern match on `Custom` here" as a way to
suggest making a `when` branch that begins with something like `Custom description ->`.
## Booleans
In many programming languages, `true` and `false` are special language keywords that refer to
the two boolean values. In Roc, booleans do not get special keywords; instead, they are exposed
as the ordinary values `Bool.true` and `Bool.false`.
This design is partly to keep the number of special keywords in the language smaller, but mainly
to suggest how booleans are intended be used in Roc: for
[*boolean logic*](https://en.wikipedia.org/wiki/Boolean_algebra) (`&&`, `||`, and so on) as opposed
to for data modeling. Tags are the preferred choice for data modeling, and having tag values be
more concise than boolean values helps make this preference clear.
As an example of why tags are encouraged for data modeling, in many languages it would be common
to write a record like `{ name: "Richard", isAdmin: Bool.true }`, but in Roc it would be preferable
to write something like `{ name: "Richard", role: Admin }`. At first, the `role` field might only
ever be set to `Admin` or `Normal`, but because the data has been modeled using tags instead of
booleans, it's much easier to add other alternatives in the future, like `Guest` or `Moderator` -
some of which might also want payloads.
## Lists
Another thing we can do in Roc is to make a *list* of values. Here's an example:
@ -606,10 +622,10 @@ In this example, `List.map` calls the function `\num -> num * 2` on each element
We can also give `List.map` a named function, instead of an anonymous one:
For example, the `Num.isOdd` function returns `True` if it's given an odd number, and `False` otherwise.
So `Num.isOdd 5` returns `True` and `Num.isOdd 2` returns `False`.
For example, the `Num.isOdd` function returns `true` if it's given an odd number, and `false` otherwise.
So `Num.isOdd 5` returns `true` and `Num.isOdd 2` returns `false`.
So calling `List.map [1, 2, 3] Num.isOdd` returns a new list of `[True, False, True]`.
As such, calling `List.map [1, 2, 3] Num.isOdd` returns a new list of `[Bool.true, Bool.false, Bool.true]`.
### List element type compatibility
@ -619,7 +635,7 @@ an error at compile time. Here's a valid, and then an invalid example:
```coffee
# working example
List.map [-1, 2, 3, -4] Num.isNegative
# returns [True, False, False, True]
# returns [Bool.true, Bool.false, Bool.false, Bool.true]
```
```coffee
@ -659,7 +675,7 @@ List.map [StrElem "A", StrElem "b", NumElem 1, StrElem "c", NumElem -3] \elem ->
NumElem num -> Num.isNegative num
StrElem str -> Str.isCapitalized str
# returns [True, False, False, False, True]
# returns [Bool.true, Bool.false, Bool.false, Bool.false, Bool.true]
```
Compare this with the example from earlier, which caused a compile-time error:
@ -672,7 +688,7 @@ The version that uses tags works because we aren't trying to call `Num.isNegativ
Instead, we're using a `when` to tell when we've got a string or a number, and then calling either
`Num.isNegative` or `Str.isCapitalized` depending on which type we have.
We could take this as far as we like, adding more different tags (e.g. `BoolElem True`) and then adding
We could take this as far as we like, adding more different tags (e.g. `BoolElem Bool.true`) and then adding
more branches to the `when` to handle them appropriately.
### Using tags as functions
@ -696,29 +712,29 @@ want a function which uses all of its arguments as the payload to the given tag.
### `List.any` and `List.all`
There are several functions that work like `List.map` - they walk through each element of a list and do
something with it. Another is `List.any`, which returns `True` if calling the given function on any element
in the list returns `True`:
something with it. Another is `List.any`, which returns `Bool.true` if calling the given function on any element
in the list returns `true`:
```coffee
List.any [1, 2, 3] Num.isOdd
# returns True because 1 and 3 are odd
# returns `Bool.true` because 1 and 3 are odd
```
```coffee
List.any [1, 2, 3] Num.isNegative
# returns False because none of these is negative
# returns `Bool.false` because none of these is negative
```
There's also `List.all` which only returns `True` if all the elements in the list pass the test:
There's also `List.all` which only returns `true` if all the elements in the list pass the test:
```coffee
List.all [1, 2, 3] Num.isOdd
# returns False because 2 is not odd
# returns `Bool.false` because 2 is not odd
```
```coffee
List.all [1, 2, 3] Num.isPositive
# returns True because all of these are positive
# returns `Bool.true` because all of these are positive
```
### Removing elements from a list
@ -731,7 +747,7 @@ List.dropAt ["Sam", "Lee", "Ari"] 1
```
Another way is to use `List.keepIf`, which passes each of the list's elements to the given
function, and then keeps them only if that function returns `True`.
function, and then keeps them only if that function returns `true`.
```coffee
List.keepIf [1, 2, 3, 4, 5] Num.isEven
@ -828,7 +844,7 @@ Result.withDefault (List.get ["a", "b", "c"] 100) ""
```coffee
Result.isOk (List.get ["a", "b", "c"] 1)
# returns True because `List.get` returned an `Ok` tag. (The payload gets ignored.)
# returns `Bool.true` because `List.get` returned an `Ok` tag. (The payload gets ignored.)
# Note: There's a Result.isErr function that works similarly.
```
@ -998,7 +1014,7 @@ isEmpty : List * -> Bool
The `*` is a *wildcard type* - that is, a type that's compatible with any other type. `List *` is compatible
with any type of `List` - so, `List Str`, `List Bool`, and so on. So you can call
`List.isEmpty ["I am a List Str"]` as well as `List.isEmpty [True]`, and they will both work fine.
`List.isEmpty ["I am a List Str"]` as well as `List.isEmpty [Bool.true]`, and they will both work fine.
The wildcard type also comes up with empty lists. Suppose we have one function that takes a `List Str` and another
function that takes a `List Bool`. We might reasonably expect to be able to pass an empty list (that is, `[]`) to
@ -1013,7 +1029,7 @@ strings : List Str
strings = List.reverse ["a", "b"]
bools : List Bool
bools = List.reverse [True, False]
bools = List.reverse [Bool.true, Bool.false]
```
In the `strings` example, we have `List.reverse` returning a `List Str`. In the `bools` example, it's returning a
@ -1777,7 +1793,7 @@ example = \tag ->
when tag is
Foo str -> Str.isEmpty str
Bar bool -> bool
_ -> False
_ -> Bool.false
```
In contrast, a *closed tag union* (or *closed union*) like `[Foo Str, Bar Bool]` (without the `*`)
@ -1903,7 +1919,7 @@ example = \tag ->
when tag is
Foo str -> Str.isEmpty str
Bar bool -> bool
_ -> False
_ -> Bool.false
```
```coffee
@ -1923,7 +1939,7 @@ example : [Foo Str, Bar Bool]a -> [Foo Str, Bar Bool]a
example = \tag ->
when tag is
Foo str -> Bar (Str.isEmpty str)
Bar _ -> Bar False
Bar _ -> Bar Bool.false
other -> other
```

View file

@ -231,11 +231,6 @@ fn check_if_bench_executables_changed() -> bool {
let main_benches_path_str = [BENCH_FOLDER_MAIN, bench_folder_str].join("");
dbg!(Command::new("tree")
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.output());
let main_bench_hashes = calc_hashes_for_folder(&main_benches_path_str);
let branch_benches_path_str = [BENCH_FOLDER_BRANCH, bench_folder_str].join("");

View file

@ -169,7 +169,7 @@ impl Scope {
aliases.insert(symbol, alias);
}
let idents = Symbol::default_in_scope();
let idents = Symbol::apply_types_in_scope();
let idents: MutMap<_, _> = idents.into_iter().collect();
Scope {

View file

@ -2302,11 +2302,6 @@ fn to_pending_alias_or_opaque<'a>(
opt_derived: Option<&'a Loc<ast::HasAbilities<'a>>>,
kind: AliasKind,
) -> PendingTypeDef<'a> {
let shadow_kind = match kind {
AliasKind::Structural => ShadowKind::Alias,
AliasKind::Opaque => ShadowKind::Opaque,
};
let region = Region::span_across(&name.region, &ann.region);
match scope.introduce_without_shadow_symbol(&Ident::from(name.value), region) {
@ -2361,7 +2356,12 @@ fn to_pending_alias_or_opaque<'a>(
}
}
Err((original_region, loc_shadowed_symbol)) => {
Err((original_sym, original_region, loc_shadowed_symbol)) => {
let shadow_kind = match kind {
AliasKind::Structural => ShadowKind::Alias(original_sym),
AliasKind::Opaque => ShadowKind::Opaque(original_sym),
};
env.problem(Problem::Shadowing {
original_region,
shadow: loc_shadowed_symbol,
@ -2422,11 +2422,11 @@ fn to_pending_type_def<'a>(
.introduce_without_shadow_symbol(&Ident::from(name.value), name.region)
{
Ok(symbol) => Loc::at(name.region, symbol),
Err((original_region, shadowed_symbol)) => {
Err((original_symbol, original_region, shadowed_symbol)) => {
env.problem(Problem::Shadowing {
original_region,
shadow: shadowed_symbol,
kind: ShadowKind::Ability,
kind: ShadowKind::Ability(original_symbol),
});
return PendingTypeDef::AbilityShadows;
}

View file

@ -122,7 +122,7 @@ pub struct ExposedModuleTypes {
#[derive(Debug)]
pub struct Module {
pub module_id: ModuleId,
pub exposed_imports: MutMap<Symbol, Variable>,
pub exposed_imports: MutMap<Symbol, Region>,
pub exposed_symbols: VecSet<Symbol>,
pub referenced_values: VecSet<Symbol>,
pub referenced_types: VecSet<Symbol>,
@ -145,8 +145,7 @@ pub struct ModuleOutput {
pub aliases: MutMap<Symbol, Alias>,
pub rigid_variables: RigidVariables,
pub declarations: Declarations,
pub exposed_imports: MutMap<Symbol, Variable>,
pub lookups: Vec<(Symbol, Variable, Region)>,
pub exposed_imports: MutMap<Symbol, Region>,
pub problems: Vec<Problem>,
pub referenced_values: VecSet<Symbol>,
pub referenced_types: VecSet<Symbol>,
@ -277,7 +276,6 @@ pub fn canonicalize_module_defs<'a>(
let mut can_exposed_imports = MutMap::default();
let mut scope = Scope::new(home, exposed_ident_ids, imported_abilities_state);
let mut env = Env::new(arena, home, dep_idents, module_ids);
let num_deps = dep_idents.len();
for (name, alias) in aliases.into_iter() {
scope.add_alias(
@ -301,7 +299,6 @@ pub fn canonicalize_module_defs<'a>(
// rules multiple times unnecessarily.
crate::operator::desugar_defs(arena, loc_defs);
let mut lookups = Vec::with_capacity(num_deps);
let mut rigid_variables = RigidVariables::default();
// Exposed values are treated like defs that appear before any others, e.g.
@ -319,20 +316,13 @@ pub fn canonicalize_module_defs<'a>(
let first_char = ident.as_inline_str().as_str().chars().next().unwrap();
if first_char.is_lowercase() {
// this is a value definition
let expr_var = var_store.fresh();
match scope.import(ident, symbol, region) {
Ok(()) => {
// Add an entry to exposed_imports using the current module's name
// as the key; e.g. if this is the Foo module and we have
// exposes [Bar.{ baz }] then insert Foo.baz as the key, so when
// anything references `baz` in this Foo module, it will resolve to Bar.baz.
can_exposed_imports.insert(symbol, expr_var);
// This will be used during constraint generation,
// to add the usual Lookup constraint as if this were a normal def.
lookups.push((symbol, expr_var, region));
can_exposed_imports.insert(symbol, region);
}
Err((_shadowed_symbol, _region)) => {
panic!("TODO gracefully handle shadowing in imports.")
@ -795,7 +785,6 @@ pub fn canonicalize_module_defs<'a>(
problems: env.problems,
symbols_from_requires,
pending_derives,
lookups,
loc_expects,
}
}

View file

@ -47,10 +47,13 @@ impl Scope {
initial_ident_ids: IdentIds,
starting_abilities_store: PendingAbilitiesStore,
) -> Scope {
let imports = Symbol::default_in_scope()
.into_iter()
.map(|(a, (b, c))| (a, b, c))
.collect();
let default_imports =
// Add all `Apply` types.
(Symbol::apply_types_in_scope().into_iter())
// Add all tag names we might want to suggest as hints in error messages.
.chain(Symbol::symbols_in_scope_for_hints());
let default_imports = default_imports.map(|(a, (b, c))| (a, b, c)).collect();
Scope {
home,
@ -59,7 +62,7 @@ impl Scope {
aliases: VecMap::default(),
abilities_store: starting_abilities_store,
shadows: VecMap::default(),
imports,
imports: default_imports,
}
}
@ -281,14 +284,14 @@ impl Scope {
&mut self,
ident: &Ident,
region: Region,
) -> Result<Symbol, (Region, Loc<Ident>)> {
) -> Result<Symbol, (Symbol, Region, Loc<Ident>)> {
match self.introduce_help(ident.as_str(), region) {
Err((_, original_region)) => {
Err((symbol, original_region)) => {
let shadow = Loc {
value: ident.clone(),
region,
};
Err((original_region, shadow))
Err((symbol, original_region, shadow))
}
Ok(symbol) => Ok(symbol),
}
@ -692,9 +695,9 @@ mod test {
&[
Ident::from("Str"),
Ident::from("List"),
Ident::from("Box"),
Ident::from("Ok"),
Ident::from("Err"),
Ident::from("Box"),
]
);
}
@ -715,9 +718,9 @@ mod test {
&[
Ident::from("Str"),
Ident::from("List"),
Ident::from("Box"),
Ident::from("Ok"),
Ident::from("Err"),
Ident::from("Box"),
]
);

View file

@ -288,8 +288,17 @@ impl<'a> Formattable for Expr<'a> {
buf.push_str(string)
}
SingleQuote(string) => {
buf.indent(indent);
buf.push('\'');
buf.push_str(string);
for c in string.chars() {
if c == '"' {
buf.push_char_literal('"')
} else {
for escaped in c.escape_default() {
buf.push_char_literal(escaped);
}
}
}
buf.push('\'');
}
&NonBase10Int {

View file

@ -86,6 +86,12 @@ impl<'a> Buf<'a> {
self.text.push_str(s);
}
pub fn push_char_literal(&mut self, c: char) {
self.flush_spaces();
self.text.push(c);
}
pub fn spaces(&mut self, count: usize) {
self.spaces_to_flush += count;
}

View file

@ -5567,6 +5567,21 @@ mod test_fmt {
);
}
#[test]
fn format_chars() {
expr_formats_same(indoc!(
r#"
' '
"#
));
expr_formats_same(indoc!(
r#"
'\n'
"#
));
}
// this is a parse error atm
// #[test]
// fn multiline_apply() {

View file

@ -80,58 +80,6 @@ const MODULE_SEPARATOR: char = '.';
const EXPANDED_STACK_SIZE: usize = 8 * 1024 * 1024;
/// TODO: how can we populate these at compile/runtime from the standard library?
/// Consider updating the macro in symbol to do this?
const PRELUDE_TYPES: [(&str, Symbol); 33] = [
("Num", Symbol::NUM_NUM),
("Int", Symbol::NUM_INT),
("Frac", Symbol::NUM_FRAC),
("Integer", Symbol::NUM_INTEGER),
("FloatingPoint", Symbol::NUM_FLOATINGPOINT),
("Binary32", Symbol::NUM_BINARY32),
("Binary64", Symbol::NUM_BINARY64),
("Signed128", Symbol::NUM_SIGNED128),
("Signed64", Symbol::NUM_SIGNED64),
("Signed32", Symbol::NUM_SIGNED32),
("Signed16", Symbol::NUM_SIGNED16),
("Signed8", Symbol::NUM_SIGNED8),
("Unsigned128", Symbol::NUM_UNSIGNED128),
("Unsigned64", Symbol::NUM_UNSIGNED64),
("Unsigned32", Symbol::NUM_UNSIGNED32),
("Unsigned16", Symbol::NUM_UNSIGNED16),
("Unsigned8", Symbol::NUM_UNSIGNED8),
("Natural", Symbol::NUM_NATURAL),
("Decimal", Symbol::NUM_DECIMAL),
("Nat", Symbol::NUM_NAT),
("I8", Symbol::NUM_I8),
("I16", Symbol::NUM_I16),
("I32", Symbol::NUM_I32),
("I64", Symbol::NUM_I64),
("I128", Symbol::NUM_I128),
("U8", Symbol::NUM_U8),
("U16", Symbol::NUM_U16),
("U32", Symbol::NUM_U32),
("U64", Symbol::NUM_U64),
("U128", Symbol::NUM_U128),
("F32", Symbol::NUM_F32),
("F64", Symbol::NUM_F64),
("Dec", Symbol::NUM_DEC),
];
const MODULE_ENCODE_TYPES: &[(&str, Symbol)] = &[
("Encoder", Symbol::ENCODE_ENCODER),
("Encoding", Symbol::ENCODE_ENCODING),
("EncoderFormatting", Symbol::ENCODE_ENCODERFORMATTING),
];
const MODULE_DECODE_TYPES: &[(&str, Symbol)] = &[
("DecodeError", Symbol::DECODE_DECODE_ERROR),
("DecodeResult", Symbol::DECODE_DECODE_RESULT),
("Decoder", Symbol::DECODE_DECODER_OPAQUE),
("Decoding", Symbol::DECODE_DECODING),
("DecoderFormatting", Symbol::DECODE_DECODERFORMATTING),
];
macro_rules! log {
($($arg:tt)*) => (dbg_do!(ROC_PRINT_LOAD_LOG, println!($($arg)*)))
}
@ -2036,13 +1984,16 @@ fn report_unused_imported_modules<'a>(
constrained_module: &ConstrainedModule,
) {
let mut unused_imported_modules = constrained_module.imported_modules.clone();
let mut unused_imports = constrained_module.module.exposed_imports.clone();
for symbol in constrained_module.module.referenced_values.iter() {
unused_imported_modules.remove(&symbol.module_id());
unused_imports.remove(symbol);
}
for symbol in constrained_module.module.referenced_types.iter() {
unused_imported_modules.remove(&symbol.module_id());
unused_imports.remove(symbol);
}
let existing = match state.module_cache.can_problems.entry(module_id) {
@ -2052,9 +2003,28 @@ fn report_unused_imported_modules<'a>(
for (unused, region) in unused_imported_modules.drain() {
if !unused.is_builtin() {
existing.push(roc_problem::can::Problem::UnusedImport(unused, region));
existing.push(roc_problem::can::Problem::UnusedModuleImport(
unused, region,
));
}
}
for (unused, region) in unused_imports.drain() {
existing.push(roc_problem::can::Problem::UnusedImport(unused, region));
}
}
fn extend_header_with_builtin(header: &mut ModuleHeader, module: ModuleId) {
header
.package_qualified_imported_modules
.insert(PackageQualified::Unqualified(module));
header.imported_modules.insert(module, Region::zero());
let types = Symbol::builtin_types_in_scope(module)
.iter()
.map(|(name, info)| (Ident::from(*name), *info));
header.exposed_imports.extend(types);
}
fn update<'a>(
@ -2158,134 +2128,25 @@ fn update<'a>(
let mut header = header;
if ![ModuleId::RESULT, ModuleId::BOOL].contains(&header.module_id) {
header
.package_qualified_imported_modules
.insert(PackageQualified::Unqualified(ModuleId::RESULT));
header
.imported_modules
.insert(ModuleId::RESULT, Region::zero());
header.exposed_imports.insert(
Ident::from("Result"),
(Symbol::RESULT_RESULT, Region::zero()),
);
extend_header_with_builtin(&mut header, ModuleId::RESULT);
}
if ![ModuleId::NUM, ModuleId::BOOL, ModuleId::RESULT].contains(&header.module_id) {
header
.package_qualified_imported_modules
.insert(PackageQualified::Unqualified(ModuleId::NUM));
header
.imported_modules
.insert(ModuleId::NUM, Region::zero());
for (type_name, symbol) in PRELUDE_TYPES {
header
.exposed_imports
.insert(Ident::from(type_name), (symbol, Region::zero()));
}
extend_header_with_builtin(&mut header, ModuleId::NUM);
}
if header.module_id != ModuleId::BOOL {
header
.package_qualified_imported_modules
.insert(PackageQualified::Unqualified(ModuleId::BOOL));
header
.imported_modules
.insert(ModuleId::BOOL, Region::zero());
header
.exposed_imports
.insert(Ident::from("Bool"), (Symbol::BOOL_BOOL, Region::zero()));
}
if header.module_id == ModuleId::NUM {
header
.exposed_imports
.insert(Ident::from("List"), (Symbol::LIST_LIST, Region::zero()));
if ![ModuleId::BOOL].contains(&header.module_id) {
extend_header_with_builtin(&mut header, ModuleId::BOOL);
}
if !header.module_id.is_builtin() {
header
.package_qualified_imported_modules
.insert(PackageQualified::Unqualified(ModuleId::BOX));
header
.imported_modules
.insert(ModuleId::BOX, Region::zero());
header
.package_qualified_imported_modules
.insert(PackageQualified::Unqualified(ModuleId::STR));
header
.imported_modules
.insert(ModuleId::STR, Region::zero());
header
.package_qualified_imported_modules
.insert(PackageQualified::Unqualified(ModuleId::DICT));
header
.imported_modules
.insert(ModuleId::DICT, Region::zero());
header
.exposed_imports
.insert(Ident::from("Dict"), (Symbol::DICT_DICT, Region::zero()));
header
.package_qualified_imported_modules
.insert(PackageQualified::Unqualified(ModuleId::SET));
header
.imported_modules
.insert(ModuleId::SET, Region::zero());
header
.exposed_imports
.insert(Ident::from("Set"), (Symbol::SET_SET, Region::zero()));
header
.package_qualified_imported_modules
.insert(PackageQualified::Unqualified(ModuleId::LIST));
header
.imported_modules
.insert(ModuleId::LIST, Region::zero());
// ENCODE
header
.package_qualified_imported_modules
.insert(PackageQualified::Unqualified(ModuleId::ENCODE));
header
.imported_modules
.insert(ModuleId::ENCODE, Region::zero());
for (type_name, symbol) in MODULE_ENCODE_TYPES {
header
.exposed_imports
.insert(Ident::from(*type_name), (*symbol, Region::zero()));
}
// DECODE
header
.package_qualified_imported_modules
.insert(PackageQualified::Unqualified(ModuleId::DECODE));
header
.imported_modules
.insert(ModuleId::DECODE, Region::zero());
for (type_name, symbol) in MODULE_DECODE_TYPES {
header
.exposed_imports
.insert(Ident::from(*type_name), (*symbol, Region::zero()));
}
extend_header_with_builtin(&mut header, ModuleId::BOX);
extend_header_with_builtin(&mut header, ModuleId::STR);
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::ENCODE);
extend_header_with_builtin(&mut header, ModuleId::DECODE);
}
state
@ -3687,7 +3548,7 @@ fn send_header<'a>(
}
};
let mut imported: Vec<(QualifiedModuleName, Vec<Ident>, Region)> =
let mut imported: Vec<(QualifiedModuleName, Vec<Loc<Ident>>, Region)> =
Vec::with_capacity(imports.len());
let mut imported_modules: MutMap<ModuleId, Region> = MutMap::default();
let mut scope_size = 0;
@ -3759,7 +3620,11 @@ fn send_header<'a>(
// to the same symbols as the ones we're using here.
let ident_ids = ident_ids_by_module.get_or_insert(module_id);
for ident in exposed_idents {
for Loc {
region,
value: ident,
} in exposed_idents
{
let ident_id = ident_ids.get_or_insert(ident.as_str());
let symbol = Symbol::new(module_id, ident_id);
@ -3898,7 +3763,7 @@ fn send_header_two<'a>(
let declared_name: ModuleName = "".into();
let mut symbols_from_requires = Vec::with_capacity(requires.len());
let mut imported: Vec<(QualifiedModuleName, Vec<Ident>, Region)> =
let mut imported: Vec<(QualifiedModuleName, Vec<Loc<Ident>>, Region)> =
Vec::with_capacity(imports.len());
let mut imported_modules: MutMap<ModuleId, Region> = MutMap::default();
@ -3983,7 +3848,11 @@ fn send_header_two<'a>(
// to the same symbols as the ones we're using here.
let ident_ids = ident_ids_by_module.get_or_insert(module_id);
for ident in exposed_idents {
for Loc {
region,
value: ident,
} in exposed_idents
{
let ident_id = ident_ids.get_or_insert(ident.as_str());
let symbol = Symbol::new(module_id, ident_id);
@ -4805,7 +4674,7 @@ fn parse<'a>(arena: &'a Bump, header: ModuleHeader<'a>) -> Result<Msg<'a>, Loadi
Ok(Msg::Parsed(parsed))
}
fn exposed_from_import<'a>(entry: &ImportsEntry<'a>) -> (QualifiedModuleName<'a>, Vec<Ident>) {
fn exposed_from_import<'a>(entry: &ImportsEntry<'a>) -> (QualifiedModuleName<'a>, Vec<Loc<Ident>>) {
use roc_parse::header::ImportsEntry::*;
match entry {
@ -4813,7 +4682,7 @@ fn exposed_from_import<'a>(entry: &ImportsEntry<'a>) -> (QualifiedModuleName<'a>
let mut exposed = Vec::with_capacity(exposes.len());
for loc_entry in exposes.iter() {
exposed.push(ident_from_exposed(&loc_entry.value));
exposed.push(loc_entry.map(ident_from_exposed));
}
let qualified_module_name = QualifiedModuleName {
@ -4828,7 +4697,7 @@ fn exposed_from_import<'a>(entry: &ImportsEntry<'a>) -> (QualifiedModuleName<'a>
let mut exposed = Vec::with_capacity(exposes.len());
for loc_entry in exposes.iter() {
exposed.push(ident_from_exposed(&loc_entry.value));
exposed.push(loc_entry.map(ident_from_exposed));
}
let qualified_module_name = QualifiedModuleName {

View file

@ -1,6 +1,6 @@
interface Primary
exposes [blah2, blah3, str, alwaysThree, identity, z, w, succeed, withDefault, yay]
imports [Dep1, Dep2.{ two, foo }, Dep3.Blah.{ bar }, Res]
imports [Dep1, Dep2.{ two }, Dep3.Blah.{ bar }, Res]
blah2 = Dep2.two
blah3 = bar

View file

@ -1,6 +1,6 @@
interface Primary
exposes [blah2, blah3, str, alwaysThree, identity, z, w, succeed, withDefault, yay]
imports [Dep1, Dep2.{ two, foo }, Dep3.Blah.{ bar }, Res]
imports [Dep1, Dep2.{ two }, Dep3.Blah.{ bar }, Res]
blah2 = Dep2.two
blah3 = bar

View file

@ -779,7 +779,7 @@ fn opaque_wrapped_unwrapped_outside_defining_module() {
is imported from another module:
1 interface Main exposes [twenty, readAge] imports [Age.{ Age }]
^^^^^^^^^^^
^^^
Note: Opaque types can only be wrapped and unwrapped in the module they are defined in!
@ -793,7 +793,7 @@ fn opaque_wrapped_unwrapped_outside_defining_module() {
is imported from another module:
1 interface Main exposes [twenty, readAge] imports [Age.{ Age }]
^^^^^^^^^^^
^^^
Note: Opaque types can only be wrapped and unwrapped in the module they are defined in!

View file

@ -790,7 +790,10 @@ macro_rules! define_builtins {
$(
$module_id:literal $module_const:ident: $module_name:literal => {
$(
$ident_id:literal $ident_const:ident: $ident_name:literal $($imported:ident)?
$ident_id:literal $ident_const:ident: $ident_name:literal
$(exposed_apply_type=$exposed_apply_type:literal)?
$(exposed_type=$exposed_type:literal)?
$(in_scope_for_hints=$in_scope_for_hints:literal)?
)*
}
)+
@ -941,23 +944,63 @@ macro_rules! define_builtins {
)*
)+
/// The default idents that should be in scope,
/// The default `Apply` types that should be in scope,
/// and what symbols they should resolve to.
///
/// This is for type aliases like `Int` and `Str` and such.
pub fn default_in_scope() -> VecMap<Ident, (Symbol, Region)> {
/// This is for type aliases that don't have a concrete Roc representation and as such
/// we hide their implementation, like `Str` and `List`.
pub fn apply_types_in_scope() -> VecMap<Ident, (Symbol, Region)> {
let mut scope = VecMap::default();
$(
$(
$(
// TODO is there a cleaner way to do this?
// The goal is to make sure that we only
// actually import things into scope if
// they are tagged as "imported" in define_builtins!
let $imported = true;
if $exposed_apply_type {
scope.insert($ident_name.into(), (Symbol::new(ModuleId::$module_const, IdentId($ident_id)), Region::zero()));
}
)?
)*
)+
if $imported {
scope
}
/// Types from a builtin module that should always be added to the default scope.
#[track_caller]
pub fn builtin_types_in_scope(module_id: ModuleId) -> &'static [(&'static str, (Symbol, Region))] {
match module_id {
$(
ModuleId::$module_const => {
const LIST : &'static [(&'static str, (Symbol, Region))] = &[
$(
$(
if $exposed_type {
($ident_name, (Symbol::new(ModuleId::$module_const, IdentId($ident_id)), Region::zero()))
} else {
unreachable!()
},
)?
)*
];
LIST
}
)+
m => roc_error_macros::internal_error!("{:?} is not a builtin module!", m),
}
}
/// Symbols that should be added to the default scope, for hints as suggestions of
/// names you might want to use.
///
/// TODO: this is a hack to get tag names to show up in error messages as suggestions,
/// really we should be extracting tag names from candidate type aliases in scope.
pub fn symbols_in_scope_for_hints() -> VecMap<Ident, (Symbol, Region)> {
let mut scope = VecMap::default();
$(
$(
$(
if $in_scope_for_hints {
scope.insert($ident_name.into(), (Symbol::new(ModuleId::$module_const, IdentId($ident_id)), Region::zero()));
}
)?
@ -1038,21 +1081,21 @@ define_builtins! {
2 DERIVED_GEN: "#Derived_gen" => {
}
3 NUM: "Num" => {
0 NUM_NUM: "Num" // the Num.Num type alias
1 NUM_I128: "I128" // the Num.I128 type alias
2 NUM_U128: "U128" // the Num.U128 type alias
3 NUM_I64: "I64" // the Num.I64 type alias
4 NUM_U64: "U64" // the Num.U64 type alias
5 NUM_I32: "I32" // the Num.I32 type alias
6 NUM_U32: "U32" // the Num.U32 type alias
7 NUM_I16: "I16" // the Num.I16 type alias
8 NUM_U16: "U16" // the Num.U16 type alias
9 NUM_I8: "I8" // the Num.I8 type alias
10 NUM_U8: "U8" // the Num.U8 type alias
11 NUM_INTEGER: "Integer" // Int : Num Integer
12 NUM_F64: "F64" // the Num.F64 type alias
13 NUM_F32: "F32" // the Num.F32 type alias
14 NUM_FLOATINGPOINT: "FloatingPoint" // Float : Num FloatingPoint
0 NUM_NUM: "Num" exposed_type=true // the Num.Num type alias
1 NUM_I128: "I128" exposed_type=true // the Num.I128 type alias
2 NUM_U128: "U128" exposed_type=true // the Num.U128 type alias
3 NUM_I64: "I64" exposed_type=true // the Num.I64 type alias
4 NUM_U64: "U64" exposed_type=true // the Num.U64 type alias
5 NUM_I32: "I32" exposed_type=true // the Num.I32 type alias
6 NUM_U32: "U32" exposed_type=true // the Num.U32 type alias
7 NUM_I16: "I16" exposed_type=true // the Num.I16 type alias
8 NUM_U16: "U16" exposed_type=true // the Num.U16 type alias
9 NUM_I8: "I8" exposed_type=true // the Num.I8 type alias
10 NUM_U8: "U8" exposed_type=true // the Num.U8 type alias
11 NUM_INTEGER: "Integer" exposed_type=true // Int : Num Integer
12 NUM_F64: "F64" exposed_type=true // the Num.F64 type alias
13 NUM_F32: "F32" exposed_type=true // the Num.F32 type alias
14 NUM_FLOATINGPOINT: "FloatingPoint" exposed_type=true // Float : Num FloatingPoint
15 NUM_MAX_F32: "maxF32"
16 NUM_MIN_F32: "minF32"
17 NUM_ABS: "abs"
@ -1095,18 +1138,18 @@ define_builtins! {
54 NUM_ATAN: "atan"
55 NUM_ACOS: "acos"
56 NUM_ASIN: "asin"
57 NUM_SIGNED128: "Signed128"
58 NUM_SIGNED64: "Signed64"
59 NUM_SIGNED32: "Signed32"
60 NUM_SIGNED16: "Signed16"
61 NUM_SIGNED8: "Signed8"
62 NUM_UNSIGNED128: "Unsigned128"
63 NUM_UNSIGNED64: "Unsigned64"
64 NUM_UNSIGNED32: "Unsigned32"
65 NUM_UNSIGNED16: "Unsigned16"
66 NUM_UNSIGNED8: "Unsigned8"
67 NUM_BINARY64: "Binary64"
68 NUM_BINARY32: "Binary32"
57 NUM_SIGNED128: "Signed128" exposed_type=true
58 NUM_SIGNED64: "Signed64" exposed_type=true
59 NUM_SIGNED32: "Signed32" exposed_type=true
60 NUM_SIGNED16: "Signed16" exposed_type=true
61 NUM_SIGNED8: "Signed8" exposed_type=true
62 NUM_UNSIGNED128: "Unsigned128" exposed_type=true
63 NUM_UNSIGNED64: "Unsigned64" exposed_type=true
64 NUM_UNSIGNED32: "Unsigned32" exposed_type=true
65 NUM_UNSIGNED16: "Unsigned16" exposed_type=true
66 NUM_UNSIGNED8: "Unsigned8" exposed_type=true
67 NUM_BINARY64: "Binary64" exposed_type=true
68 NUM_BINARY32: "Binary32" exposed_type=true
69 NUM_BITWISE_AND: "bitwiseAnd"
70 NUM_BITWISE_XOR: "bitwiseXor"
71 NUM_BITWISE_OR: "bitwiseOr"
@ -1119,14 +1162,14 @@ define_builtins! {
78 NUM_MUL_WRAP: "mulWrap"
79 NUM_MUL_CHECKED: "mulChecked"
80 NUM_MUL_SATURATED: "mulSaturated"
81 NUM_INT: "Int"
82 NUM_FRAC: "Frac"
83 NUM_NATURAL: "Natural"
84 NUM_NAT: "Nat"
81 NUM_INT: "Int" exposed_type=true
82 NUM_FRAC: "Frac" exposed_type=true
83 NUM_NATURAL: "Natural" exposed_type=true
84 NUM_NAT: "Nat" exposed_type=true
85 NUM_INT_CAST: "intCast"
86 NUM_IS_MULTIPLE_OF: "isMultipleOf"
87 NUM_DECIMAL: "Decimal"
88 NUM_DEC: "Dec" // the Num.Dectype alias
87 NUM_DECIMAL: "Decimal" exposed_type=true
88 NUM_DEC: "Dec" exposed_type=true // the Num.Dectype alias
89 NUM_BYTES_TO_U16: "bytesToU16"
90 NUM_BYTES_TO_U32: "bytesToU32"
91 NUM_CAST_TO_NAT: "#castToNat"
@ -1186,7 +1229,7 @@ define_builtins! {
145 NUM_BYTES_TO_U32_LOWLEVEL: "bytesToU32Lowlevel"
}
4 BOOL: "Bool" => {
0 BOOL_BOOL: "Bool" // the Bool.Bool type alias
0 BOOL_BOOL: "Bool" exposed_type=true // the Bool.Bool type alias
1 BOOL_FALSE: "false"
2 BOOL_TRUE: "true"
3 BOOL_AND: "and"
@ -1197,7 +1240,7 @@ define_builtins! {
8 BOOL_NEQ: "isNotEq"
}
5 STR: "Str" => {
0 STR_STR: "Str" imported // the Str.Str type alias
0 STR_STR: "Str" exposed_apply_type=true // the Str.Str type alias
1 STR_IS_EMPTY: "isEmpty"
2 STR_APPEND: "#append" // unused
3 STR_CONCAT: "concat"
@ -1252,7 +1295,7 @@ define_builtins! {
52 STR_REPLACE_LAST: "replaceLast"
}
6 LIST: "List" => {
0 LIST_LIST: "List" imported // the List.List type alias
0 LIST_LIST: "List" exposed_apply_type=true // the List.List type alias
1 LIST_IS_EMPTY: "isEmpty"
2 LIST_GET: "get"
3 LIST_SET: "set"
@ -1329,11 +1372,9 @@ define_builtins! {
74 LIST_MAP_TRY: "mapTry"
}
7 RESULT: "Result" => {
0 RESULT_RESULT: "Result" // the Result.Result type alias
1 RESULT_OK: "Ok" imported // Result.Result a e = [Ok a, Err e]
// NB: not strictly needed; used for finding tag names in error suggestions
2 RESULT_ERR: "Err" imported // Result.Result a e = [Ok a, Err e]
// NB: not strictly needed; used for finding tag names in error suggestions
0 RESULT_RESULT: "Result" exposed_type=true // the Result.Result type alias
1 RESULT_OK: "Ok" in_scope_for_hints=true // Result.Result a e = [Ok a, Err e]
2 RESULT_ERR: "Err" in_scope_for_hints=true // Result.Result a e = [Ok a, Err e]
3 RESULT_MAP: "map"
4 RESULT_MAP_ERR: "mapErr"
5 RESULT_WITH_DEFAULT: "withDefault"
@ -1343,7 +1384,7 @@ define_builtins! {
9 RESULT_ON_ERR: "onErr"
}
8 DICT: "Dict" => {
0 DICT_DICT: "Dict" // the Dict.Dict type alias
0 DICT_DICT: "Dict" exposed_type=true // the Dict.Dict type alias
1 DICT_EMPTY: "empty"
2 DICT_SINGLE: "single"
3 DICT_GET: "get"
@ -1365,7 +1406,7 @@ define_builtins! {
16 DICT_CAPACITY: "capacity"
}
9 SET: "Set" => {
0 SET_SET: "Set" // the Set.Set type alias
0 SET_SET: "Set" exposed_type=true // the Set.Set type alias
1 SET_EMPTY: "empty"
2 SET_SINGLE: "single"
3 SET_LEN: "len"
@ -1383,15 +1424,15 @@ define_builtins! {
15 SET_CAPACITY: "capacity"
}
10 BOX: "Box" => {
0 BOX_BOX_TYPE: "Box" imported // the Box.Box opaque type
0 BOX_BOX_TYPE: "Box" exposed_apply_type=true // the Box.Box opaque type
1 BOX_BOX_FUNCTION: "box" // Box.box
2 BOX_UNBOX: "unbox"
}
11 ENCODE: "Encode" => {
0 ENCODE_ENCODER: "Encoder"
1 ENCODE_ENCODING: "Encoding"
0 ENCODE_ENCODER: "Encoder" exposed_type=true
1 ENCODE_ENCODING: "Encoding" exposed_type=true
2 ENCODE_TO_ENCODER: "toEncoder"
3 ENCODE_ENCODERFORMATTING: "EncoderFormatting"
3 ENCODE_ENCODERFORMATTING: "EncoderFormatting" exposed_type=true
4 ENCODE_U8: "u8"
5 ENCODE_U16: "u16"
6 ENCODE_U32: "u32"
@ -1416,12 +1457,12 @@ define_builtins! {
25 ENCODE_TO_BYTES: "toBytes"
}
12 DECODE: "Decode" => {
0 DECODE_DECODE_ERROR: "DecodeError"
1 DECODE_DECODE_RESULT: "DecodeResult"
2 DECODE_DECODER_OPAQUE: "Decoder"
3 DECODE_DECODING: "Decoding"
0 DECODE_DECODE_ERROR: "DecodeError" exposed_type=true
1 DECODE_DECODE_RESULT: "DecodeResult" exposed_type=true
2 DECODE_DECODER_OPAQUE: "Decoder" exposed_type=true
3 DECODE_DECODING: "Decoding" exposed_type=true
4 DECODE_DECODER: "decoder"
5 DECODE_DECODERFORMATTING: "DecoderFormatting"
5 DECODE_DECODERFORMATTING: "DecoderFormatting" exposed_type=true
6 DECODE_U8: "u8"
7 DECODE_U16: "u16"
8 DECODE_U32: "u32"

View file

@ -22,16 +22,17 @@ pub enum BadPattern {
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum ShadowKind {
Variable,
Alias,
Opaque,
Ability,
Alias(Symbol),
Opaque(Symbol),
Ability(Symbol),
}
/// Problems that can occur in the course of canonicalization.
#[derive(Clone, Debug, PartialEq)]
pub enum Problem {
UnusedDef(Symbol, Region),
UnusedImport(ModuleId, Region),
UnusedImport(Symbol, Region),
UnusedModuleImport(ModuleId, Region),
ExposedButNotDefined(Symbol),
UnknownGeneratesWith(Loc<Ident>),
/// First symbol is the name of the closure with that argument

View file

@ -328,7 +328,7 @@ fn encode_use_stdlib() {
indoc!(
r#"
app "test"
imports [Encode.{ toEncoder }, Json]
imports [Encode, Json]
provides [main] to "./platform"
HelloWorld := {} has [Encoding {toEncoder}]
@ -356,7 +356,7 @@ fn encode_use_stdlib_without_wrapping_custom() {
indoc!(
r#"
app "test"
imports [Encode.{ toEncoder }, Json]
imports [Encode, Json]
provides [main] to "./platform"
HelloWorld := {} has [Encoding {toEncoder}]
@ -381,7 +381,7 @@ fn to_encoder_encode_custom_has_capture() {
indoc!(
r#"
app "test"
imports [Encode.{ toEncoder }, Json]
imports [Encode, Json]
provides [main] to "./platform"
HelloWorld := Str has [Encoding {toEncoder}]
@ -421,7 +421,7 @@ mod encode_immediate {
assert_evals_to!(
indoc!(
r#"
app "test" imports [Encode.{ toEncoder }, Json] provides [main] to "./platform"
app "test" imports [Encode, Json] provides [main] to "./platform"
main =
when Str.fromUtf8 (Encode.toBytes "foo" Json.toUtf8) is
@ -442,7 +442,7 @@ mod encode_immediate {
assert_evals_to!(
&format!(indoc!(
r#"
app "test" imports [Encode.{{ toEncoder }}, Json] provides [main] to "./platform"
app "test" imports [Encode, Json] provides [main] to "./platform"
main =
when Str.fromUtf8 (Encode.toBytes {}{} Json.toUtf8) is
@ -481,7 +481,7 @@ fn encode_derived_record_one_field_string() {
indoc!(
r#"
app "test"
imports [Encode.{ toEncoder }, Json]
imports [Encode, Json]
provides [main] to "./platform"
main =
@ -503,7 +503,7 @@ fn encode_derived_record_two_fields_strings() {
indoc!(
r#"
app "test"
imports [Encode.{ toEncoder }, Json]
imports [Encode, Json]
provides [main] to "./platform"
main =
@ -526,7 +526,7 @@ fn encode_derived_nested_record_string() {
indoc!(
r#"
app "test"
imports [Encode.{ toEncoder }, Json]
imports [Encode, Json]
provides [main] to "./platform"
main =
@ -550,7 +550,7 @@ fn encode_derived_tag_one_payload_string() {
indoc!(
r#"
app "test"
imports [Encode.{ toEncoder }, Json]
imports [Encode, Json]
provides [main] to "./platform"
main =
@ -573,7 +573,7 @@ fn encode_derived_tag_two_payloads_string() {
indoc!(
r#"
app "test"
imports [Encode.{ toEncoder }, Json]
imports [Encode, Json]
provides [main] to "./platform"
main =
@ -596,7 +596,7 @@ fn encode_derived_nested_tag_string() {
indoc!(
r#"
app "test"
imports [Encode.{ toEncoder }, Json]
imports [Encode, Json]
provides [main] to "./platform"
main =
@ -620,7 +620,7 @@ fn encode_derived_nested_record_tag_record() {
indoc!(
r#"
app "test"
imports [Encode.{ toEncoder }, Json]
imports [Encode, Json]
provides [main] to "./platform"
main =
@ -644,7 +644,7 @@ fn encode_derived_list_string() {
indoc!(
r#"
app "test"
imports [Encode.{ toEncoder }, Json]
imports [Encode, Json]
provides [main] to "./platform"
main =
@ -668,7 +668,7 @@ fn encode_derived_list_of_records() {
indoc!(
r#"
app "test"
imports [Encode.{ toEncoder }, Json]
imports [Encode, Json]
provides [main] to "./platform"
main =
@ -692,7 +692,7 @@ fn encode_derived_list_of_lists_of_strings() {
indoc!(
r#"
app "test"
imports [Encode.{ toEncoder }, Json]
imports [Encode, Json]
provides [main] to "./platform"
main =
@ -716,7 +716,7 @@ fn encode_derived_record_with_many_types() {
indoc!(
r#"
app "test"
imports [Encode.{ toEncoder }, Json]
imports [Encode, Json]
provides [main] to "./platform"
main =

View file

@ -144,7 +144,7 @@ pub fn helper(
for problem in can_problems.into_iter() {
// Ignore "unused" problems
match problem {
UnusedDef(_, _) | UnusedArgument(_, _, _, _) | UnusedImport(_, _) => {
UnusedDef(_, _) | UnusedArgument(_, _, _, _) | UnusedModuleImport(_, _) => {
delayed_errors.push(problem);
continue;
}

View file

@ -129,7 +129,7 @@ fn create_llvm_module<'a>(
// Ignore "unused" problems
UnusedDef(_, _)
| UnusedArgument(_, _, _, _)
| UnusedImport(_, _)
| UnusedModuleImport(_, _)
| RuntimeError(_)
| UnsupportedPattern(_, _)
| ExposedButNotDefined(_) => {

View file

@ -12,8 +12,20 @@ use std::process;
use strum::IntoEnumIterator;
use target_lexicon::Triple;
pub struct IgnoreErrors {
pub can: bool,
}
impl IgnoreErrors {
const NONE: Self = IgnoreErrors { can: false };
}
pub fn generate(input_path: &Path, output_path: &Path) -> io::Result<i32> {
match load_types(input_path.to_path_buf(), Threading::AllAvailable) {
match load_types(
input_path.to_path_buf(),
Threading::AllAvailable,
IgnoreErrors::NONE,
) {
Ok(types_and_targets) => {
let mut file = File::create(output_path).unwrap_or_else(|err| {
eprintln!(
@ -67,6 +79,7 @@ pub fn generate(input_path: &Path, output_path: &Path) -> io::Result<i32> {
pub fn load_types(
full_file_path: PathBuf,
threading: Threading,
ignore_errors: IgnoreErrors,
) -> Result<Vec<(Types, TargetInfo)>, io::Error> {
let target_info = (&Triple::host()).into();
@ -108,7 +121,7 @@ pub fn load_types(
let can_problems = can_problems.remove(&home).unwrap_or_default();
let type_problems = type_problems.remove(&home).unwrap_or_default();
if !can_problems.is_empty() || !type_problems.is_empty() {
if (!ignore_errors.can && !can_problems.is_empty()) || !type_problems.is_empty() {
todo!(
"Gracefully report compilation problems during glue generation: {:?}, {:?}",
can_problems,

View file

@ -1,4 +1,4 @@
use roc_glue::load::load_types;
use roc_glue::load::{load_types, IgnoreErrors};
use roc_glue::rust_glue;
use roc_load::Threading;
use std::env;
@ -33,7 +33,12 @@ pub fn generate_bindings(decl_src: &str) -> String {
let mut file = File::create(file_path).unwrap();
writeln!(file, "{}", &src).unwrap();
let result = load_types(full_file_path, Threading::Single);
let result = load_types(
full_file_path,
Threading::Single,
// required `nothing` is unused; that error is okay
IgnoreErrors { can: true },
);
dir.close().expect("Unable to close tempdir");

View file

@ -30,3 +30,4 @@ tempfile = "3.2.0"
[dev-dependencies]
indoc = "1.0.7"
libc = "0.2.133"

View file

@ -2,9 +2,9 @@ use iced_x86::{Decoder, DecoderOptions, Instruction, OpCodeOperandKind, OpKind};
use memmap2::MmapMut;
use object::{elf, endian};
use object::{
CompressedFileRange, CompressionFormat, LittleEndian, NativeEndian, Object, ObjectSection,
ObjectSymbol, RelocationKind, RelocationTarget, Section, SectionIndex, SectionKind, Symbol,
SymbolIndex, SymbolSection,
CompressedFileRange, CompressionFormat, LittleEndian as LE, NativeEndian, Object,
ObjectSection, ObjectSymbol, RelocationKind, RelocationTarget, Section, SectionIndex,
SectionKind, Symbol, SymbolIndex, SymbolSection,
};
use roc_collections::all::MutMap;
use roc_error_macros::{internal_error, user_error};
@ -15,7 +15,6 @@ use std::mem;
use std::os::raw::c_char;
use std::path::Path;
use std::time::{Duration, Instant};
use target_lexicon::Triple;
use crate::metadata::{self, Metadata, VirtualOffset};
@ -266,7 +265,7 @@ impl<'a> Surgeries<'a> {
/// Constructs a `metadata::Metadata` from a host executable binary, and writes it to disk
pub(crate) fn preprocess_elf(
target: &Triple,
endianness: target_lexicon::Endianness,
host_exe_path: &Path,
metadata_path: &Path,
preprocessed_path: &Path,
@ -410,10 +409,7 @@ pub(crate) fn preprocess_elf(
let scanning_dynamic_deps_duration;
let platform_gen_start;
let out_mmap = match target
.endianness()
.unwrap_or(target_lexicon::Endianness::Little)
{
let out_mmap = match endianness {
target_lexicon::Endianness::Little => {
let scanning_dynamic_deps_start = Instant::now();
@ -510,7 +506,7 @@ fn gen_elf_le(
shared_lib_index: usize,
verbose: bool,
) -> MmapMut {
let exec_header = load_struct_inplace::<elf::FileHeader64<LittleEndian>>(exec_data, 0);
let exec_header = load_struct_inplace::<elf::FileHeader64<LE>>(exec_data, 0);
let ph_offset = exec_header.e_phoff.get(NativeEndian);
let ph_ent_size = exec_header.e_phentsize.get(NativeEndian);
let ph_num = exec_header.e_phnum.get(NativeEndian);
@ -541,7 +537,7 @@ fn gen_elf_le(
out_mmap[..ph_end].copy_from_slice(&exec_data[..ph_end]);
let program_headers = load_structs_inplace_mut::<elf::ProgramHeader64<LittleEndian>>(
let program_headers = load_structs_inplace_mut::<elf::ProgramHeader64<LE>>(
&mut out_mmap,
ph_offset as usize,
ph_num as usize,
@ -572,23 +568,17 @@ fn gen_elf_le(
let p_offset = ph.p_offset.get(NativeEndian);
if (p_type == elf::PT_LOAD && p_offset == 0) || p_type == elf::PT_PHDR {
// Extend length for the first segment and the program header.
ph.p_filesz = endian::U64::new(
LittleEndian,
ph.p_filesz.get(NativeEndian) + md.added_byte_count,
);
ph.p_memsz = endian::U64::new(
LittleEndian,
ph.p_memsz.get(NativeEndian) + md.added_byte_count,
);
ph.p_filesz = endian::U64::new(LE, ph.p_filesz.get(NativeEndian) + md.added_byte_count);
ph.p_memsz = endian::U64::new(LE, ph.p_memsz.get(NativeEndian) + md.added_byte_count);
} else {
// Shift if needed.
if physical_shift_start <= p_offset {
ph.p_offset = endian::U64::new(LittleEndian, p_offset + md.added_byte_count);
ph.p_offset = endian::U64::new(LE, p_offset + md.added_byte_count);
}
let p_vaddr = ph.p_vaddr.get(NativeEndian);
if virtual_shift_start <= p_vaddr {
ph.p_vaddr = endian::U64::new(LittleEndian, p_vaddr + md.added_byte_count);
ph.p_paddr = endian::U64::new(LittleEndian, p_vaddr + md.added_byte_count);
ph.p_vaddr = endian::U64::new(LE, p_vaddr + md.added_byte_count);
ph.p_paddr = endian::U64::new(LE, p_vaddr + md.added_byte_count);
}
}
}
@ -611,7 +601,7 @@ fn gen_elf_le(
.copy_from_slice(&exec_data[physical_shift_start as usize..]);
// Update all sections for shift for extra program headers.
let section_headers = load_structs_inplace_mut::<elf::SectionHeader64<LittleEndian>>(
let section_headers = load_structs_inplace_mut::<elf::SectionHeader64<LE>>(
&mut out_mmap,
sh_offset as usize + md.added_byte_count as usize,
sh_num as usize,
@ -623,10 +613,10 @@ fn gen_elf_le(
let sh_offset = sh.sh_offset.get(NativeEndian);
let sh_addr = sh.sh_addr.get(NativeEndian);
if physical_shift_start <= sh_offset {
sh.sh_offset = endian::U64::new(LittleEndian, sh_offset + md.added_byte_count);
sh.sh_offset = endian::U64::new(LE, sh_offset + md.added_byte_count);
}
if virtual_shift_start <= sh_addr {
sh.sh_addr = endian::U64::new(LittleEndian, sh_addr + md.added_byte_count);
sh.sh_addr = endian::U64::new(LE, sh_addr + md.added_byte_count);
}
// Record every relocation section.
@ -652,34 +642,33 @@ fn gen_elf_le(
// Update all relocations for shift for extra program headers.
for (sec_offset, sec_size) in rel_sections {
let relocations = load_structs_inplace_mut::<elf::Rel64<LittleEndian>>(
let relocations = load_structs_inplace_mut::<elf::Rel64<LE>>(
&mut out_mmap,
sec_offset as usize + md.added_byte_count as usize,
sec_size as usize / mem::size_of::<elf::Rel64<LittleEndian>>(),
sec_size as usize / mem::size_of::<elf::Rel64<LE>>(),
);
for rel in relocations.iter_mut() {
let r_offset = rel.r_offset.get(NativeEndian);
if virtual_shift_start <= r_offset {
rel.r_offset = endian::U64::new(LittleEndian, r_offset + md.added_byte_count);
rel.r_offset = endian::U64::new(LE, r_offset + md.added_byte_count);
}
}
}
for (sec_offset, sec_size) in rela_sections {
let relocations = load_structs_inplace_mut::<elf::Rela64<LittleEndian>>(
let relocations = load_structs_inplace_mut::<elf::Rela64<LE>>(
&mut out_mmap,
sec_offset as usize + md.added_byte_count as usize,
sec_size as usize / mem::size_of::<elf::Rela64<LittleEndian>>(),
sec_size as usize / mem::size_of::<elf::Rela64<LE>>(),
);
for (i, rel) in relocations.iter_mut().enumerate() {
let r_offset = rel.r_offset.get(NativeEndian);
if virtual_shift_start <= r_offset {
rel.r_offset = endian::U64::new(LittleEndian, r_offset + md.added_byte_count);
rel.r_offset = endian::U64::new(LE, r_offset + md.added_byte_count);
// Deal with potential adjusts to absolute jumps.
// TODO: Verify other relocation types.
if rel.r_type(LittleEndian, false) == elf::R_X86_64_RELATIVE {
let r_addend = rel.r_addend.get(LittleEndian);
rel.r_addend
.set(LittleEndian, r_addend + md.added_byte_count as i64);
if rel.r_type(LE, false) == elf::R_X86_64_RELATIVE {
let r_addend = rel.r_addend.get(LE);
rel.r_addend.set(LE, r_addend + md.added_byte_count as i64);
}
}
// If the relocation goes to a roc function, we need to surgically link it and change it to relative.
@ -688,9 +677,9 @@ fn gen_elf_le(
let r_sym = rel.r_sym(NativeEndian, false);
for (name, index) in got_app_syms.iter() {
if *index as u32 == r_sym {
rel.set_r_info(LittleEndian, false, 0, elf::R_X86_64_RELATIVE);
rel.set_r_info(LE, false, 0, elf::R_X86_64_RELATIVE);
let addend_addr = sec_offset as usize
+ i * mem::size_of::<elf::Rela64<LittleEndian>>()
+ i * mem::size_of::<elf::Rela64<LE>>()
// This 16 skips the first 2 fields and gets to the addend field.
+ 16;
md.surgeries
@ -710,7 +699,7 @@ fn gen_elf_le(
// Update dynamic table entries for shift for extra program headers.
let dyn_offset = md.dynamic_section_offset + md.added_byte_count;
let dyns = load_structs_inplace_mut::<elf::Dyn64<LittleEndian>>(
let dyns = load_structs_inplace_mut::<elf::Dyn64<LE>>(
&mut out_mmap,
dyn_offset as usize,
dynamic_lib_count,
@ -749,7 +738,7 @@ fn gen_elf_le(
| elf::DT_VERNEED => {
let d_addr = d.d_val.get(NativeEndian);
if virtual_shift_start <= d_addr {
d.d_val = endian::U64::new(LittleEndian, d_addr + md.added_byte_count);
d.d_val = endian::U64::new(LE, d_addr + md.added_byte_count);
}
}
_ => {}
@ -760,30 +749,30 @@ fn gen_elf_le(
let symtab_offset = md.symbol_table_section_offset + md.added_byte_count;
let symtab_size = md.symbol_table_size as usize;
let symbols = load_structs_inplace_mut::<elf::Sym64<LittleEndian>>(
let symbols = load_structs_inplace_mut::<elf::Sym64<LE>>(
&mut out_mmap,
symtab_offset as usize,
symtab_size / mem::size_of::<elf::Sym64<LittleEndian>>(),
symtab_size / mem::size_of::<elf::Sym64<LE>>(),
);
for sym in symbols {
let addr = sym.st_value.get(NativeEndian);
if virtual_shift_start <= addr {
sym.st_value = endian::U64::new(LittleEndian, addr + md.added_byte_count);
sym.st_value = endian::U64::new(LE, addr + md.added_byte_count);
}
}
// Update all data in the global offset table.
for (offset, size) in got_sections {
let global_offsets = load_structs_inplace_mut::<endian::U64<LittleEndian>>(
let global_offsets = load_structs_inplace_mut::<endian::U64<LE>>(
&mut out_mmap,
*offset + md.added_byte_count as usize,
size / mem::size_of::<endian::U64<LittleEndian>>(),
size / mem::size_of::<endian::U64<LE>>(),
);
for go in global_offsets.iter_mut() {
let go_addr = go.get(NativeEndian);
if physical_shift_start <= go_addr {
go.set(LittleEndian, go_addr + md.added_byte_count);
go.set(LE, go_addr + md.added_byte_count);
}
}
}
@ -801,17 +790,16 @@ fn gen_elf_le(
}
// Update main elf header for extra data.
let mut file_header =
load_struct_inplace_mut::<elf::FileHeader64<LittleEndian>>(&mut out_mmap, 0);
let mut file_header = load_struct_inplace_mut::<elf::FileHeader64<LE>>(&mut out_mmap, 0);
file_header.e_shoff = endian::U64::new(
LittleEndian,
LE,
file_header.e_shoff.get(NativeEndian) + md.added_byte_count,
);
let e_entry = file_header.e_entry.get(NativeEndian);
if virtual_shift_start <= e_entry {
file_header.e_entry = endian::U64::new(LittleEndian, e_entry + md.added_byte_count);
file_header.e_entry = endian::U64::new(LE, e_entry + md.added_byte_count);
}
file_header.e_phnum = endian::U16::new(LittleEndian, ph_num + added_header_count as u16);
file_header.e_phnum = endian::U16::new(LE, ph_num + added_header_count as u16);
out_mmap
}
@ -1068,7 +1056,7 @@ fn surgery_elf_help(
if !elf64 || !litte_endian {
internal_error!("Only 64bit little endian elf currently supported for surgery");
}
let exec_header = load_struct_inplace::<elf::FileHeader64<LittleEndian>>(exec_mmap, 0);
let exec_header = load_struct_inplace::<elf::FileHeader64<LE>>(exec_mmap, 0);
let ph_offset = exec_header.e_phoff.get(NativeEndian);
let ph_ent_size = exec_header.e_phentsize.get(NativeEndian);
@ -1332,7 +1320,7 @@ fn surgery_elf_help(
// Add 2 new sections and segments.
let new_section_count = 2;
offset += new_section_count * sh_ent_size as usize;
let section_headers = load_structs_inplace_mut::<elf::SectionHeader64<LittleEndian>>(
let section_headers = load_structs_inplace_mut::<elf::SectionHeader64<LE>>(
exec_mmap,
new_sh_offset as usize,
sh_num as usize + new_section_count,
@ -1346,39 +1334,39 @@ fn surgery_elf_help(
// set the new rodata section header
section_headers[section_headers.len() - 2] = elf::SectionHeader64 {
sh_name: endian::U32::new(LittleEndian, 0),
sh_type: endian::U32::new(LittleEndian, elf::SHT_PROGBITS),
sh_flags: endian::U64::new(LittleEndian, (elf::SHF_ALLOC) as u64),
sh_addr: endian::U64::new(LittleEndian, new_rodata_section_vaddr as u64),
sh_offset: endian::U64::new(LittleEndian, new_rodata_section_offset as u64),
sh_size: endian::U64::new(LittleEndian, new_rodata_section_size),
sh_link: endian::U32::new(LittleEndian, 0),
sh_info: endian::U32::new(LittleEndian, 0),
sh_addralign: endian::U64::new(LittleEndian, 16),
sh_entsize: endian::U64::new(LittleEndian, 0),
sh_name: endian::U32::new(LE, 0),
sh_type: endian::U32::new(LE, elf::SHT_PROGBITS),
sh_flags: endian::U64::new(LE, elf::SHF_ALLOC as u64),
sh_addr: endian::U64::new(LE, new_rodata_section_vaddr as u64),
sh_offset: endian::U64::new(LE, new_rodata_section_offset as u64),
sh_size: endian::U64::new(LE, new_rodata_section_size),
sh_link: endian::U32::new(LE, 0),
sh_info: endian::U32::new(LE, 0),
sh_addralign: endian::U64::new(LE, 16),
sh_entsize: endian::U64::new(LE, 0),
};
// set the new text section header
section_headers[section_headers.len() - 1] = elf::SectionHeader64 {
sh_name: endian::U32::new(LittleEndian, 0),
sh_type: endian::U32::new(LittleEndian, elf::SHT_PROGBITS),
sh_flags: endian::U64::new(LittleEndian, (elf::SHF_ALLOC | elf::SHF_EXECINSTR) as u64),
sh_addr: endian::U64::new(LittleEndian, new_text_section_vaddr),
sh_offset: endian::U64::new(LittleEndian, new_text_section_offset as u64),
sh_size: endian::U64::new(LittleEndian, new_text_section_size),
sh_link: endian::U32::new(LittleEndian, 0),
sh_info: endian::U32::new(LittleEndian, 0),
sh_addralign: endian::U64::new(LittleEndian, 16),
sh_entsize: endian::U64::new(LittleEndian, 0),
sh_name: endian::U32::new(LE, 0),
sh_type: endian::U32::new(LE, elf::SHT_PROGBITS),
sh_flags: endian::U64::new(LE, (elf::SHF_ALLOC | elf::SHF_EXECINSTR) as u64),
sh_addr: endian::U64::new(LE, new_text_section_vaddr),
sh_offset: endian::U64::new(LE, new_text_section_offset as u64),
sh_size: endian::U64::new(LE, new_text_section_size),
sh_link: endian::U32::new(LE, 0),
sh_info: endian::U32::new(LE, 0),
sh_addralign: endian::U64::new(LE, 16),
sh_entsize: endian::U64::new(LE, 0),
};
// Reload and update file header and size.
let file_header = load_struct_inplace_mut::<elf::FileHeader64<LittleEndian>>(exec_mmap, 0);
file_header.e_shoff = endian::U64::new(LittleEndian, new_sh_offset as u64);
file_header.e_shnum = endian::U16::new(LittleEndian, sh_num + new_section_count as u16);
let file_header = load_struct_inplace_mut::<elf::FileHeader64<LE>>(exec_mmap, 0);
file_header.e_shoff = endian::U64::new(LE, new_sh_offset as u64);
file_header.e_shnum = endian::U16::new(LE, sh_num + new_section_count as u16);
// Add 2 new segments that match the new sections.
let program_headers = load_structs_inplace_mut::<elf::ProgramHeader64<LittleEndian>>(
let program_headers = load_structs_inplace_mut::<elf::ProgramHeader64<LE>>(
exec_mmap,
ph_offset as usize,
ph_num as usize,
@ -1386,27 +1374,27 @@ fn surgery_elf_help(
// set the new rodata section program header
program_headers[program_headers.len() - 2] = elf::ProgramHeader64 {
p_type: endian::U32::new(LittleEndian, elf::PT_LOAD),
p_flags: endian::U32::new(LittleEndian, elf::PF_R),
p_offset: endian::U64::new(LittleEndian, new_rodata_section_offset as u64),
p_vaddr: endian::U64::new(LittleEndian, new_rodata_section_vaddr as u64),
p_paddr: endian::U64::new(LittleEndian, new_rodata_section_vaddr as u64),
p_filesz: endian::U64::new(LittleEndian, new_rodata_section_size),
p_memsz: endian::U64::new(LittleEndian, new_rodata_section_virtual_size),
p_align: endian::U64::new(LittleEndian, md.load_align_constraint),
p_type: endian::U32::new(LE, elf::PT_LOAD),
p_flags: endian::U32::new(LE, elf::PF_R),
p_offset: endian::U64::new(LE, new_rodata_section_offset as u64),
p_vaddr: endian::U64::new(LE, new_rodata_section_vaddr as u64),
p_paddr: endian::U64::new(LE, new_rodata_section_vaddr as u64),
p_filesz: endian::U64::new(LE, new_rodata_section_size),
p_memsz: endian::U64::new(LE, new_rodata_section_virtual_size),
p_align: endian::U64::new(LE, md.load_align_constraint),
};
// set the new text section program header
let new_text_section_index = program_headers.len() - 1;
program_headers[new_text_section_index] = elf::ProgramHeader64 {
p_type: endian::U32::new(LittleEndian, elf::PT_LOAD),
p_flags: endian::U32::new(LittleEndian, elf::PF_R | elf::PF_X),
p_offset: endian::U64::new(LittleEndian, new_text_section_offset as u64),
p_vaddr: endian::U64::new(LittleEndian, new_text_section_vaddr),
p_paddr: endian::U64::new(LittleEndian, new_text_section_vaddr),
p_filesz: endian::U64::new(LittleEndian, new_text_section_size),
p_memsz: endian::U64::new(LittleEndian, new_text_section_size),
p_align: endian::U64::new(LittleEndian, md.load_align_constraint),
p_type: endian::U32::new(LE, elf::PT_LOAD),
p_flags: endian::U32::new(LE, elf::PF_R | elf::PF_X),
p_offset: endian::U64::new(LE, new_text_section_offset as u64),
p_vaddr: endian::U64::new(LE, new_text_section_vaddr),
p_paddr: endian::U64::new(LE, new_text_section_vaddr),
p_filesz: endian::U64::new(LE, new_text_section_size),
p_memsz: endian::U64::new(LE, new_text_section_size),
p_align: endian::U64::new(LE, md.load_align_constraint),
};
// Update calls from platform and dynamic symbols.
@ -1480,14 +1468,14 @@ fn surgery_elf_help(
}
if let Some(i) = md.dynamic_symbol_indices.get(func_name) {
let sym = load_struct_inplace_mut::<elf::Sym64<LittleEndian>>(
let sym = load_struct_inplace_mut::<elf::Sym64<LE>>(
exec_mmap,
dynsym_offset as usize + *i as usize * mem::size_of::<elf::Sym64<LittleEndian>>(),
dynsym_offset as usize + *i as usize * mem::size_of::<elf::Sym64<LE>>(),
);
sym.st_shndx = endian::U16::new(LittleEndian, new_text_section_index as u16);
sym.st_value = endian::U64::new(LittleEndian, func_virt_offset as u64);
sym.st_shndx = endian::U16::new(LE, new_text_section_index as u16);
sym.st_value = endian::U64::new(LE, func_virt_offset as u64);
sym.st_size = endian::U64::new(
LittleEndian,
LE,
match app_func_size_map.get(func_name) {
Some(size) => *size,
None => internal_error!("Size missing for: {func_name}"),
@ -1504,6 +1492,9 @@ fn surgery_elf_help(
mod tests {
use super::*;
use indoc::indoc;
use target_lexicon::Triple;
const ELF64_DYNHOST: &[u8] = include_bytes!("../dynhost_benchmarks_elf64") as &[_];
#[test]
@ -1558,4 +1549,147 @@ mod tests {
keys.as_slice()
)
}
#[allow(dead_code)]
fn zig_host_app_help(dir: &Path) {
let host_zig = indoc!(
r#"
const std = @import("std");
extern fn roc_magic1(usize) callconv(.C) [*]const u8;
pub fn main() !void {
const stdout = std.io.getStdOut().writer();
try stdout.print("Hello {s}\n", .{roc_magic1(0)[0..3]});
}
"#
);
let app_zig = indoc!(
r#"
const X = [_][]const u8 { "foo" };
export fn roc_magic1(index: usize) [*]const u8 {
return X[index].ptr;
}
"#
);
let zig = std::env::var("ROC_ZIG").unwrap_or_else(|_| "zig".into());
std::fs::write(dir.join("host.zig"), host_zig.as_bytes()).unwrap();
std::fs::write(dir.join("app.zig"), app_zig.as_bytes()).unwrap();
// we need to compile the app first
let output = std::process::Command::new(&zig)
.current_dir(dir)
.args(&[
"build-obj",
"app.zig",
"-fPIC",
"-target",
"x86_64-linux-gnu",
"-OReleaseFast",
])
.output()
.unwrap();
if !output.status.success() {
use std::io::Write;
std::io::stdout().write_all(&output.stdout).unwrap();
std::io::stderr().write_all(&output.stderr).unwrap();
panic!("zig build-obj failed");
}
// open our app object; we'll copy sections from it later
let file = std::fs::File::open(dir.join("app.o")).unwrap();
let roc_app = unsafe { memmap2::Mmap::map(&file) }.unwrap();
let names: Vec<String> = {
let object = object::File::parse(&*roc_app).unwrap();
object
.symbols()
.filter(|s| !s.is_local())
.map(|e| e.name().unwrap().to_string())
.collect()
};
let dylib_bytes = crate::generate_dylib::create_dylib_elf64(&names).unwrap();
std::fs::write(dir.join("libapp.so"), dylib_bytes).unwrap();
// now we can compile the host (it uses libapp.obj, hence the order here)
let output = std::process::Command::new(&zig)
.current_dir(dir)
.args(&[
"build-exe",
"libapp.so",
"host.zig",
"-fPIE",
"-lc",
"-target",
"x86_64-linux-gnu",
"-OReleaseFast",
])
.output()
.unwrap();
if !output.status.success() {
use std::io::Write;
std::io::stdout().write_all(&output.stdout).unwrap();
std::io::stderr().write_all(&output.stderr).unwrap();
panic!("zig build-exe failed");
}
preprocess_elf(
target_lexicon::Endianness::Little,
&dir.join("host"),
&dir.join("metadata"),
&dir.join("preprocessedhost"),
&dir.join("libapp.so"),
false,
false,
);
std::fs::copy(&dir.join("preprocessedhost"), &dir.join("final")).unwrap();
surgery_elf(
&*roc_app,
&dir.join("metadata"),
&dir.join("final"),
false,
false,
);
}
#[cfg(target_os = "linux")]
#[test]
fn zig_host_app() {
let dir = tempfile::tempdir().unwrap();
let dir = dir.path();
zig_host_app_help(dir);
let output = std::process::Command::new(&dir.join("final"))
.current_dir(dir)
.output()
.unwrap();
if !output.status.success() {
use std::io::Write;
std::io::stdout().write_all(&output.stdout).unwrap();
std::io::stderr().write_all(&output.stderr).unwrap();
panic!("app.exe failed");
}
let output = String::from_utf8_lossy(&output.stdout);
assert_eq!("Hello foo\n", output);
}
}

View file

@ -6,6 +6,9 @@ mod pe;
#[cfg(test)]
pub(crate) use pe::synthetic_dll;
#[cfg(test)]
pub(crate) use elf64::create_dylib_elf64;
pub(crate) use pe::APP_DLL;
pub fn generate(target: &Triple, custom_names: &[String]) -> object::read::Result<Vec<u8>> {

View file

@ -195,10 +195,14 @@ fn preprocess(
println!("Targeting: {}", target);
}
let endianness = target
.endianness()
.unwrap_or(target_lexicon::Endianness::Little);
match target.binary_format {
target_lexicon::BinaryFormat::Elf => {
crate::elf::preprocess_elf(
target,
endianness,
host_exe_path,
metadata_path,
preprocessed_path,
@ -368,3 +372,69 @@ pub(crate) fn open_mmap_mut(path: &Path, length: usize) -> MmapMut {
unsafe { MmapMut::map_mut(&out_file).unwrap_or_else(|e| internal_error!("{e}")) }
}
/// # dbg_hex
/// display dbg result in hexadecimal `{:#x?}` format.
#[macro_export]
macro_rules! dbg_hex {
// NOTE: We cannot use `concat!` to make a static string as a format argument
// of `eprintln!` because `file!` could contain a `{` or
// `$val` expression could be a block (`{ .. }`), in which case the `eprintln!`
// will be malformed.
() => {
eprintln!("[{}:{}]", file!(), line!());
};
($val:expr $(,)?) => {
// Use of `match` here is intentional because it affects the lifetimes
// of temporaries - https://stackoverflow.com/a/48732525/1063961
match $val {
tmp => {
eprintln!("[{}:{}] {} = {:#x?}",
file!(), line!(), stringify!($val), &tmp);
tmp
}
}
};
($($val:expr),+ $(,)?) => {
($($crate::dbg_hex!($val)),+,)
};
}
// These functions don't end up in the final Roc binary but Windows linker needs a definition inside the crate.
// On Windows, there seems to be less dead-code-elimination than on Linux or MacOS, or maybe it's done later.
#[cfg(test)]
#[cfg(windows)]
#[allow(unused_imports)]
use windows_roc_platform_functions::*;
#[cfg(test)]
#[cfg(windows)]
mod windows_roc_platform_functions {
use core::ffi::c_void;
/// # Safety
/// The Roc application needs this.
#[no_mangle]
pub unsafe fn roc_alloc(size: usize, _alignment: u32) -> *mut c_void {
libc::malloc(size)
}
/// # Safety
/// The Roc application needs this.
#[no_mangle]
pub unsafe fn roc_realloc(
c_ptr: *mut c_void,
new_size: usize,
_old_size: usize,
_alignment: u32,
) -> *mut c_void {
libc::realloc(c_ptr, new_size)
}
/// # Safety
/// The Roc application needs this.
#[no_mangle]
pub unsafe fn roc_dealloc(c_ptr: *mut c_void, _alignment: u32) {
libc::free(c_ptr)
}
}

View file

@ -16,7 +16,7 @@ use object::{
};
use serde::{Deserialize, Serialize};
use roc_collections::MutMap;
use roc_collections::{MutMap, VecMap};
use roc_error_macros::internal_error;
use crate::{
@ -194,7 +194,7 @@ pub(crate) fn surgery_pe(executable_path: &Path, metadata_path: &Path, roc_app_b
let md = PeMetadata::read_from_file(metadata_path);
let app_obj_sections = AppSections::from_data(roc_app_bytes);
let mut symbols = app_obj_sections.symbols;
let mut symbols = app_obj_sections.roc_symbols;
let image_base: u64 = md.image_base;
let file_alignment = md.file_alignment as usize;
@ -225,6 +225,15 @@ pub(crate) fn surgery_pe(executable_path: &Path, metadata_path: &Path, roc_app_b
let mut data_bytes_added = 0;
let mut file_bytes_added = 0;
// relocations between the sections of the roc application
// (as opposed to relocations for symbols the app imports from the host)
let inter_app_relocations = process_internal_relocations(
&app_obj_sections.sections,
&app_obj_sections.other_symbols,
(app_code_section_va - image_base) as u32,
section_alignment,
);
for kind in [SectionKind::Text, SectionKind::ReadOnlyData] {
let length: usize = app_obj_sections
.sections
@ -287,38 +296,43 @@ pub(crate) fn surgery_pe(executable_path: &Path, metadata_path: &Path, roc_app_b
address,
} = app_relocation;
match md.exports.get(name) {
Some(destination) => {
match relocation.kind() {
object::RelocationKind::Relative => {
// we implicitly only do 32-bit relocations
debug_assert_eq!(relocation.size(), 32);
if let Some(destination) = md.exports.get(name) {
match relocation.kind() {
object::RelocationKind::Relative => {
// we implicitly only do 32-bit relocations
debug_assert_eq!(relocation.size(), 32);
let delta = destination
- virtual_address as i64
- *offset_in_section as i64
let delta =
destination - virtual_address as i64 - *offset_in_section as i64
+ relocation.addend();
executable[offset + *offset_in_section as usize..][..4]
.copy_from_slice(&(delta as i32).to_le_bytes());
}
_ => todo!(),
executable[offset + *offset_in_section as usize..][..4]
.copy_from_slice(&(delta as i32).to_le_bytes());
}
_ => todo!(),
}
None => {
match relocation.kind() {
object::RelocationKind::Relative => {
// we implicitly only do 32-bit relocations
debug_assert_eq!(relocation.size(), 32);
} else if let Some(destination) = inter_app_relocations.get(name) {
// we implicitly only do 32-bit relocations
debug_assert_eq!(relocation.size(), 32);
let delta = *address as i64 - *offset_in_section as i64
+ relocation.addend();
let delta = destination - virtual_address as i64 - *offset_in_section as i64
+ relocation.addend();
executable[offset + *offset_in_section as usize..][..4]
.copy_from_slice(&(delta as i32).to_le_bytes());
}
_ => todo!(),
executable[offset + *offset_in_section as usize..][..4]
.copy_from_slice(&(delta as i32).to_le_bytes());
} else {
match relocation.kind() {
object::RelocationKind::Relative => {
// we implicitly only do 32-bit relocations
debug_assert_eq!(relocation.size(), 32);
let delta =
*address as i64 - *offset_in_section as i64 + relocation.addend();
executable[offset + *offset_in_section as usize..][..4]
.copy_from_slice(&(delta as i32).to_le_bytes());
}
_ => todo!(),
}
}
}
@ -506,6 +520,7 @@ struct Preprocessor {
new_section_count: usize,
old_headers_size: usize,
new_headers_size: usize,
section_alignment: usize,
}
impl Preprocessor {
@ -538,6 +553,7 @@ impl Preprocessor {
// next multiple of `file_alignment`.
let old_headers_size = nt_headers.optional_header.size_of_headers.get(LE) as usize;
let file_alignment = nt_headers.optional_header.file_alignment.get(LE) as usize;
let section_alignment = nt_headers.optional_header.section_alignment.get(LE) as usize;
let extra_sections_width = extra_sections.len() * Self::SECTION_HEADER_WIDTH;
// in a better world `extra_sections_width.div_ceil(file_alignment)` would be stable
@ -559,6 +575,7 @@ impl Preprocessor {
new_section_count: sections.len() + extra_sections.len(),
old_headers_size,
new_headers_size,
section_alignment,
}
}
@ -586,11 +603,26 @@ impl Preprocessor {
}
fn write_dummy_sections(&self, result: &mut MmapMut, extra_sections: &[[u8; 8]]) {
const W: usize = std::mem::size_of::<ImageSectionHeader>();
let previous_section_header =
load_struct_inplace::<ImageSectionHeader>(result, self.extra_sections_start - W);
let previous_section_header_end = previous_section_header.virtual_address.get(LE)
+ previous_section_header.virtual_size.get(LE);
let mut next_virtual_address =
next_multiple_of(previous_section_header_end as usize, self.section_alignment);
for (i, name) in extra_sections.iter().enumerate() {
let header = ImageSectionHeader {
name: *name,
virtual_size: Default::default(),
virtual_address: Default::default(),
// NOTE: the virtual_size CANNOT BE ZERO! the binary is invalid if a section has
// zero virtual size. Setting it to 1 works, (because this one byte is not backed
// up by space on disk, the loader will zero the memory if you run the executable)
virtual_size: object::U32::new(LE, 1),
// NOTE: this must be a valid virtual address, using 0 is invalid!
virtual_address: object::U32::new(LE, next_virtual_address as u32),
size_of_raw_data: Default::default(),
pointer_to_raw_data: Default::default(),
pointer_to_relocations: Default::default(),
@ -600,11 +632,11 @@ impl Preprocessor {
characteristics: Default::default(),
};
let header_array: [u8; std::mem::size_of::<ImageSectionHeader>()] =
unsafe { std::mem::transmute(header) };
let header_array: [u8; W] = unsafe { std::mem::transmute(header) };
result[self.extra_sections_start + i * header_array.len()..][..header_array.len()]
.copy_from_slice(&header_array);
result[self.extra_sections_start + i * W..][..W].copy_from_slice(&header_array);
next_virtual_address += self.section_alignment;
}
}
@ -630,6 +662,16 @@ impl Preprocessor {
.size_of_headers
.set(LE, self.new_headers_size as u32);
// adding new sections increased the size of the image. We update this value so the
// preprocessedhost is, in theory, runnable. In practice for roc programs it will crash
// because there are missing symbols (those that the app should provide), but for testing
// being able to run the preprocessedhost is nice.
nt_headers.optional_header.size_of_image.set(
LE,
nt_headers.optional_header.size_of_image.get(LE)
+ (self.section_alignment * extra_sections.len()) as u32,
);
// update the section file offsets
//
// Sections:
@ -767,6 +809,7 @@ fn redirect_dummy_dll_functions(
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[repr(u8)]
enum SectionKind {
Text,
// Data,
@ -786,6 +829,7 @@ struct Section {
file_range: Range<usize>,
kind: SectionKind,
relocations: MutMap<String, AppRelocation>,
app_section_index: SectionIndex,
}
#[derive(Debug)]
@ -798,7 +842,44 @@ struct AppSymbol {
#[derive(Debug, Default)]
struct AppSections {
sections: Vec<Section>,
symbols: Vec<AppSymbol>,
roc_symbols: Vec<AppSymbol>,
other_symbols: Vec<(SectionIndex, AppSymbol)>,
}
/// Process relocations between two places within the app. This a bit different from doing a
/// relocation of a symbol that will be "imported" from the host
fn process_internal_relocations(
sections: &[Section],
other_symbols: &[(SectionIndex, AppSymbol)],
first_host_section_virtual_address: u32,
section_alignment: usize,
) -> VecMap<String, i64> {
let mut result = VecMap::default();
let mut host_section_virtual_address = first_host_section_virtual_address;
for kind in [SectionKind::Text, SectionKind::ReadOnlyData] {
let it = sections.iter().filter(|s| s.kind == kind);
for section in it {
for (s_index, app_symbol) in other_symbols.iter() {
if *s_index == section.app_section_index {
result.insert(
app_symbol.name.clone(),
app_symbol.offset_in_section as i64 + host_section_virtual_address as i64,
);
}
}
}
let length: usize = sections
.iter()
.filter(|s| s.kind == kind)
.map(|s| s.file_range.end - s.file_range.start)
.sum();
host_section_virtual_address += next_multiple_of(length, section_alignment) as u32;
}
result
}
impl AppSections {
@ -866,6 +947,7 @@ impl AppSections {
}
let section = Section {
app_section_index: index,
file_range,
kind,
relocations,
@ -874,7 +956,8 @@ impl AppSections {
sections.push(section);
}
let mut symbols = Vec::new();
let mut roc_symbols = Vec::new();
let mut other_symbols = Vec::new();
for symbol in file.symbols() {
use object::ObjectSymbol;
@ -889,12 +972,26 @@ impl AppSections {
offset_in_section: (offset_in_host_section + symbol.address()) as usize,
};
symbols.push(symbol);
roc_symbols.push(symbol);
}
} else if let object::SymbolSection::Section(index) = symbol.section() {
if let Some((kind, offset_in_host_section)) = section_starts.get(&index) {
let symbol = AppSymbol {
name: symbol.name().unwrap_or_default().to_string(),
section_kind: *kind,
offset_in_section: (offset_in_host_section + symbol.address()) as usize,
};
other_symbols.push((index, symbol));
}
}
}
AppSections { sections, symbols }
AppSections {
sections,
roc_symbols,
other_symbols,
}
}
}
@ -988,9 +1085,6 @@ fn write_section_header(
mod test {
const PE_DYNHOST: &[u8] = include_bytes!("../dynhost_benchmarks_windows.exe") as &[_];
use std::ops::Deref;
use object::pe::ImageFileHeader;
use object::read::pe::PeFile64;
use object::{pe, LittleEndian as LE, Object};
@ -1276,81 +1370,7 @@ mod test {
increase_number_of_sections_help(PE_DYNHOST, &new_sections, &path);
}
fn redirect_dummy_dll_functions_test(
executable: &mut [u8],
dynamic_relocations: &DynamicRelocationsPe,
function_definition_vas: &[(String, u64)],
) {
let object = object::read::pe::PeFile64::parse(&*executable).unwrap();
let imports: Vec<_> = object
.imports()
.unwrap()
.iter()
.filter(|import| import.library() == APP_DLL.as_bytes())
.map(|import| {
std::str::from_utf8(import.name())
.unwrap_or_default()
.to_owned()
})
.collect();
// and get the offset in the file of 0x1400037f0
let thunks_start_offset = find_thunks_start_offset(executable, dynamic_relocations);
redirect_dummy_dll_functions(
executable,
function_definition_vas,
&imports,
thunks_start_offset,
)
}
fn link_zig_host_and_app_help(dir: &Path) {
use object::ObjectSection;
let host_zig = indoc!(
r#"
const std = @import("std");
extern const roc_one: u64;
extern const roc_three: u64;
extern fn roc_magic1() callconv(.C) u64;
extern fn roc_magic2() callconv(.C) u8;
pub export fn roc_alloc() u64 {
return 123456;
}
pub export fn roc_realloc() u64 {
return 111111;
}
pub fn main() !void {
const stdout = std.io.getStdOut().writer();
try stdout.print("Hello, {} {} {} {}!\n", .{roc_magic1(), roc_magic2(), roc_one, roc_three});
}
"#
);
let app_zig = indoc!(
r#"
export const roc_one: u64 = 1;
export const roc_three: u64 = 3;
extern fn roc_alloc() u64;
extern fn roc_realloc() u64;
export fn roc_magic1() u64 {
return roc_alloc() + roc_realloc();
}
export fn roc_magic2() u8 {
return 32;
}
"#
);
fn zig_host_app(dir: &Path, host_zig: &str, app_zig: &str) {
let zig = std::env::var("ROC_ZIG").unwrap_or_else(|_| "zig".into());
std::fs::write(dir.join("host.zig"), host_zig.as_bytes()).unwrap();
@ -1385,7 +1405,7 @@ mod test {
let roc_app = unsafe { memmap2::Mmap::map(&file) }.unwrap();
let roc_app_sections = AppSections::from_data(&*roc_app);
let mut symbols = roc_app_sections.symbols;
let symbols = roc_app_sections.roc_symbols;
// make the dummy dylib based on the app object
let names: Vec<_> = symbols.iter().map(|s| s.name.clone()).collect();
@ -1419,186 +1439,31 @@ mod test {
panic!("zig build-exe failed");
}
let data = std::fs::read(dir.join("host.exe")).unwrap();
let new_sections = [*b".text\0\0\0", *b".rdata\0\0"];
increase_number_of_sections_help(&data, &new_sections, &dir.join("dynhost.exe"));
preprocess_windows(
&dir.join("host.exe"),
&dir.join("metadata"),
&dir.join("preprocessedhost"),
false,
false,
)
.unwrap();
// hardcoded for now, should come from the precompiled metadata in the future
let image_base: u64 = 0x140000000;
let file_alignment = 0x200;
let section_alignment = 0x1000;
let last_host_section_index = 5;
std::fs::copy(&dir.join("preprocessedhost"), &dir.join("app.exe")).unwrap();
let roc_app_sections_size: usize = roc_app_sections
.sections
.iter()
.map(|s| next_multiple_of(s.file_range.end - s.file_range.start, file_alignment))
.sum();
let dynhost_bytes = std::fs::read(dir.join("dynhost.exe")).unwrap();
let mut executable = open_mmap_mut(
&dir.join("app.exe"),
dynhost_bytes.len() + roc_app_sections_size,
);
// copying over all of the dynhost.exe bytes
executable[..dynhost_bytes.len()].copy_from_slice(&dynhost_bytes);
let file = PeFile64::parse(executable.deref()).unwrap();
let last_host_section = file.sections().nth(last_host_section_index).unwrap();
let exports: MutMap<String, i64> = file
.exports()
.unwrap()
.into_iter()
.map(|e| {
(
String::from_utf8(e.name().to_vec()).unwrap(),
(e.address() - image_base) as i64,
)
})
.collect();
let optional_header_offset = file.dos_header().nt_headers_offset() as usize
+ std::mem::size_of::<u32>()
+ std::mem::size_of::<ImageFileHeader>();
let extra_code_section_va = last_host_section.address()
+ next_multiple_of(
last_host_section.size() as usize,
section_alignment as usize,
) as u64;
let mut section_header_start = 624;
let mut section_file_offset = dynhost_bytes.len();
let mut virtual_address = (extra_code_section_va - image_base) as u32;
let mut code_bytes_added = 0;
let mut data_bytes_added = 0;
let mut file_bytes_added = 0;
for kind in [SectionKind::Text, SectionKind::ReadOnlyData] {
let length: usize = roc_app_sections
.sections
.iter()
.filter(|s| s.kind == kind)
.map(|s| s.file_range.end - s.file_range.start)
.sum();
// offset_in_section now becomes a proper virtual address
for symbol in symbols.iter_mut() {
if symbol.section_kind == kind {
symbol.offset_in_section += image_base as usize + virtual_address as usize;
}
}
let virtual_size = length as u32;
let size_of_raw_data = next_multiple_of(length, file_alignment) as u32;
match kind {
SectionKind::Text => {
code_bytes_added += size_of_raw_data;
write_section_header(
&mut executable,
*b".text1\0\0",
pe::IMAGE_SCN_MEM_READ | pe::IMAGE_SCN_CNT_CODE | pe::IMAGE_SCN_MEM_EXECUTE,
section_header_start,
section_file_offset,
virtual_size,
virtual_address,
size_of_raw_data,
);
}
SectionKind::ReadOnlyData => {
data_bytes_added += size_of_raw_data;
write_section_header(
&mut executable,
*b".rdata1\0",
pe::IMAGE_SCN_MEM_READ | pe::IMAGE_SCN_CNT_INITIALIZED_DATA,
section_header_start,
section_file_offset,
virtual_size,
virtual_address,
size_of_raw_data,
);
}
}
let mut offset = section_file_offset;
let it = roc_app_sections.sections.iter().filter(|s| s.kind == kind);
for section in it {
let slice = &roc_app[section.file_range.start..section.file_range.end];
executable[offset..][..slice.len()].copy_from_slice(slice);
for (name, app_relocation) in section.relocations.iter() {
let destination = exports[name];
let AppRelocation {
offset_in_section,
relocation,
..
} = app_relocation;
match relocation.kind() {
object::RelocationKind::Relative => {
// we implicitly only do 32-bit relocations
debug_assert_eq!(relocation.size(), 32);
let delta =
destination - virtual_address as i64 - *offset_in_section as i64
+ relocation.addend();
executable[offset + *offset_in_section as usize..][..4]
.copy_from_slice(&(delta as i32).to_le_bytes());
}
_ => todo!(),
}
}
offset += slice.len();
}
section_header_start += std::mem::size_of::<ImageSectionHeader>();
section_file_offset += size_of_raw_data as usize;
virtual_address += next_multiple_of(length, section_alignment) as u32;
file_bytes_added += next_multiple_of(length, section_alignment) as u32;
}
update_optional_header(
&mut executable,
optional_header_offset,
code_bytes_added as u32,
file_bytes_added as u32,
data_bytes_added as u32,
);
let dynamic_relocations = DynamicRelocationsPe::new(&executable);
let symbols: Vec<_> = symbols
.into_iter()
.map(|s| (s.name, s.offset_in_section as u64))
.collect();
redirect_dummy_dll_functions_test(&mut executable, &dynamic_relocations, &symbols);
remove_dummy_dll_import_table_test(
&mut executable,
dynamic_relocations.data_directories_offset_in_file,
dynamic_relocations.imports_offset_in_file,
dynamic_relocations.dummy_import_index,
);
surgery_pe(&dir.join("app.exe"), &dir.join("metadata"), &*roc_app);
}
#[cfg(windows)]
#[test]
fn link_zig_host_and_app_windows() {
#[allow(dead_code)]
fn windows_test<F>(runner: F) -> String
where
F: Fn(&Path),
{
let dir = tempfile::tempdir().unwrap();
let dir = dir.path();
link_zig_host_and_app_help(dir);
runner(dir);
let output = std::process::Command::new("app.exe")
let output = std::process::Command::new(&dir.join("app.exe"))
.current_dir(dir)
.output()
.unwrap();
@ -1612,18 +1477,18 @@ mod test {
panic!("app.exe failed");
}
let output = String::from_utf8_lossy(&output.stdout);
assert_eq!("Hello, 234567 32 1 3!\n", output);
String::from_utf8(output.stdout.to_vec()).unwrap()
}
#[ignore]
#[test]
fn link_zig_host_and_app_wine() {
#[allow(dead_code)]
fn wine_test<F>(runner: F) -> String
where
F: Fn(&Path),
{
let dir = tempfile::tempdir().unwrap();
let dir = dir.path();
link_zig_host_and_app_help(dir);
runner(dir);
let output = std::process::Command::new("wine")
.current_dir(dir)
@ -1640,8 +1505,171 @@ mod test {
panic!("wine failed");
}
let output = String::from_utf8_lossy(&output.stdout);
String::from_utf8(output.stdout.to_vec()).unwrap()
}
assert_eq!("Hello, 234567 32 1 3!\n", output);
/// Basics of linking: symbols imported and exported by the host and app, both values and
/// functions
#[allow(dead_code)]
fn test_basics(dir: &Path) {
zig_host_app(
dir,
indoc!(
r#"
const std = @import("std");
extern const roc_one: u64;
extern const roc_three: u64;
extern fn roc_magic1() callconv(.C) u64;
extern fn roc_magic2() callconv(.C) u8;
pub export fn roc_alloc() u64 {
return 123456;
}
pub export fn roc_realloc() u64 {
return 111111;
}
pub fn main() !void {
const stdout = std.io.getStdOut().writer();
try stdout.print("Hello, {} {} {} {}!\n", .{roc_magic1(), roc_magic2(), roc_one, roc_three});
}
"#
),
indoc!(
r#"
export const roc_one: u64 = 1;
export const roc_three: u64 = 3;
extern fn roc_alloc() u64;
extern fn roc_realloc() u64;
export fn roc_magic1() u64 {
return roc_alloc() + roc_realloc();
}
export fn roc_magic2() u8 {
return 32;
}
"#
),
);
}
#[cfg(windows)]
#[test]
#[ignore = "does not work yet"]
fn basics_windows() {
assert_eq!("Hello, 234567 32 1 3!\n", windows_test(test_basics))
}
#[test]
#[ignore]
fn basics_wine() {
assert_eq!("Hello, 234567 32 1 3!\n", wine_test(test_basics))
}
/// This zig code sample has a relocation in the text section that points into the rodata
/// section. That means we need to correctly track where each app section ends up in the host.
#[allow(dead_code)]
fn test_internal_relocations(dir: &Path) {
zig_host_app(
dir,
indoc!(
r#"
const std = @import("std");
extern fn roc_magic1(usize) callconv(.C) [*]const u8;
pub fn main() !void {
const stdout = std.io.getStdOut().writer();
try stdout.print("Hello {s}\n", .{roc_magic1(0)[0..3]});
}
"#
),
indoc!(
r#"
const X = [_][]const u8 { "foo" };
export fn roc_magic1(index: usize) [*]const u8 {
return X[index].ptr;
}
"#
),
);
}
#[cfg(windows)]
#[test]
fn app_internal_relocations_windows() {
assert_eq!("Hello foo\n", windows_test(test_internal_relocations))
}
#[ignore]
#[test]
fn app_internal_relocations_wine() {
assert_eq!("Hello foo\n", wine_test(test_internal_relocations))
}
/// Run our preprocessing on an all-zig host. There is no app here to simplify things.
fn preprocessing_help(dir: &Path) {
let zig = std::env::var("ROC_ZIG").unwrap_or_else(|_| "zig".into());
let host_zig = indoc!(
r#"
const std = @import("std");
pub fn main() !void {
const stdout = std.io.getStdOut().writer();
try stdout.print("Hello there\n", .{});
}
"#
);
std::fs::write(dir.join("host.zig"), host_zig.as_bytes()).unwrap();
let output = std::process::Command::new(&zig)
.current_dir(dir)
.args(&[
"build-exe",
"host.zig",
"-lc",
"-target",
"x86_64-windows-gnu",
"-rdynamic",
"--strip",
"-OReleaseFast",
])
.output()
.unwrap();
if !output.status.success() {
use std::io::Write;
std::io::stdout().write_all(&output.stdout).unwrap();
std::io::stderr().write_all(&output.stderr).unwrap();
panic!("zig build-exe failed");
}
let host_bytes = std::fs::read(dir.join("host.exe")).unwrap();
let host_bytes = host_bytes.as_slice();
let extra_sections = [*b"\0\0\0\0\0\0\0\0", *b"\0\0\0\0\0\0\0\0"];
Preprocessor::preprocess(&dir.join("app.exe"), host_bytes, extra_sections.as_slice());
}
#[cfg(windows)]
#[test]
fn preprocessing_windows() {
assert_eq!("Hello there\n", windows_test(preprocessing_help))
}
#[test]
#[ignore]
fn preprocessing_wine() {
assert_eq!("Hello there\n", wine_test(preprocessing_help))
}
}

View file

@ -44,7 +44,29 @@ pub const WELCOME_MESSAGE: &str = concatcp!(
END_COL,
"\n\n"
);
pub const INSTRUCTIONS: &str = "Enter an expression, or :help, or :q to quit.\n";
// For when nothing is entered in the repl
// TODO add link to repl tutorial(does not yet exist).
pub const SHORT_INSTRUCTIONS: &str = "Enter an expression, or :help, or :q to quit.\n\n";
// TODO add link to repl tutorial(does not yet exist).
pub const TIPS: &str = concatcp!(
BLUE,
" - ",
END_COL,
"Entered code needs to return something. For example:\n\n",
PINK,
" » foo = 1\n … foo\n\n",
END_COL,
BLUE,
" - ",
END_COL,
":q to quit\n\n",
BLUE,
" - ",
END_COL,
":help\n"
);
pub const PROMPT: &str = concatcp!("\n", BLUE, "»", END_COL, " ");
pub const CONT_PROMPT: &str = concatcp!(BLUE, "", END_COL, " ");
@ -392,7 +414,7 @@ pub fn main() -> io::Result<()> {
// To debug rustyline:
// <UNCOMMENT> env_logger::init();
// <RUN WITH:> RUST_LOG=rustyline=debug cargo run repl 2> debug.log
print!("{}{}", WELCOME_MESSAGE, INSTRUCTIONS);
print!("{}{}", WELCOME_MESSAGE, TIPS);
let mut prev_line_blank = false;
let mut editor = Editor::<ReplHelper>::new();
@ -415,7 +437,7 @@ pub fn main() -> io::Result<()> {
match trim_line.to_lowercase().as_str() {
"" => {
if pending_src.is_empty() {
print!("\n{}", INSTRUCTIONS);
print!("\n{}", SHORT_INSTRUCTIONS);
} else if prev_line_blank {
// After two blank lines in a row, give up and try parsing it
// even though it's going to fail. This way you don't get stuck.
@ -437,7 +459,8 @@ pub fn main() -> io::Result<()> {
}
}
":help" => {
println!("Use :exit or :quit or :q to exit.");
// TODO add link to repl tutorial(does not yet exist).
println!("Use :q to exit.");
}
":exit" | ":quit" | ":q" => {
break;

View file

@ -3,7 +3,7 @@ use std::io::Write;
use std::path::PathBuf;
use std::process::{Command, ExitStatus, Stdio};
use roc_repl_cli::{INSTRUCTIONS, WELCOME_MESSAGE};
use roc_repl_cli::{TIPS, WELCOME_MESSAGE};
use roc_test_utils::assert_multiline_str_eq;
const ERROR_MESSAGE_START: char = '─';
@ -75,7 +75,7 @@ fn repl_eval(input: &str) -> Out {
// Remove the initial instructions from the output.
let expected_instructions = format!("{}{}", WELCOME_MESSAGE, INSTRUCTIONS);
let expected_instructions = format!("{}{}", WELCOME_MESSAGE, TIPS);
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(

View file

@ -87,7 +87,24 @@ pub fn can_problem<'b>(
title = UNUSED_DEF.to_string();
severity = Severity::Warning;
}
Problem::UnusedImport(module_id, region) => {
Problem::UnusedImport(symbol, region) => {
doc = alloc.stack([
alloc.concat([
alloc.symbol_qualified(symbol),
alloc.reflow(" is not used in this module."),
]),
alloc.region(lines.convert_region(region)),
alloc.concat([
alloc.reflow("Since "),
alloc.symbol_qualified(symbol),
alloc.reflow(" isn't used, you don't need to import it."),
]),
]);
title = UNUSED_IMPORT.to_string();
severity = Severity::Warning;
}
Problem::UnusedModuleImport(module_id, region) => {
doc = alloc.stack([
alloc.concat([
alloc.reflow("Nothing from "),
@ -257,9 +274,11 @@ pub fn can_problem<'b>(
shadow,
kind,
} => {
doc = report_shadowing(alloc, lines, original_region, shadow, kind);
let (res_title, res_doc) =
report_shadowing(alloc, lines, original_region, shadow, kind);
title = DUPLICATE_NAME.to_string();
doc = res_doc;
title = res_title.to_string();
severity = Severity::RuntimeError;
}
Problem::CyclicAlias(symbol, region, others, alias_kind) => {
@ -1371,28 +1390,48 @@ fn report_shadowing<'b>(
original_region: Region,
shadow: Loc<Ident>,
kind: ShadowKind,
) -> RocDocBuilder<'b> {
let what = match kind {
ShadowKind::Variable => "variables",
ShadowKind::Alias => "aliases",
ShadowKind::Opaque => "opaques",
ShadowKind::Ability => "abilities",
) -> (&'static str, RocDocBuilder<'b>) {
let (what, what_plural, is_builtin) = match kind {
ShadowKind::Variable => ("variable", "variables", false),
ShadowKind::Alias(sym) => ("alias", "aliases", sym.is_builtin()),
ShadowKind::Opaque(sym) => ("opaque type", "opaque types", sym.is_builtin()),
ShadowKind::Ability(sym) => ("ability", "abilities", sym.is_builtin()),
};
alloc.stack([
alloc
.text("The ")
.append(alloc.ident(shadow.value))
.append(alloc.reflow(" name is first defined here:")),
alloc.region(lines.convert_region(original_region)),
alloc.reflow("But then it's defined a second time here:"),
alloc.region(lines.convert_region(shadow.region)),
alloc.concat([
alloc.reflow("Since these "),
alloc.reflow(what),
alloc.reflow(" have the same name, it's easy to use the wrong one on accident. Give one of them a new name."),
]),
])
let doc = if is_builtin {
alloc.stack([
alloc.concat([
alloc.reflow("This "),
alloc.reflow(what),
alloc.reflow(" has the same name as a builtin:"),
]),
alloc.region(lines.convert_region(shadow.region)),
alloc.concat([
alloc.reflow("All builtin "),
alloc.reflow(what_plural),
alloc.reflow(" are in scope by default, so I need this "),
alloc.reflow(what),
alloc.reflow(" to have a different name!"),
]),
])
} else {
alloc.stack([
alloc
.text("The ")
.append(alloc.ident(shadow.value))
.append(alloc.reflow(" name is first defined here:")),
alloc.region(lines.convert_region(original_region)),
alloc.reflow("But then it's defined a second time here:"),
alloc.region(lines.convert_region(shadow.region)),
alloc.concat([
alloc.reflow("Since these "),
alloc.reflow(what_plural),
alloc.reflow(" have the same name, it's easy to use the wrong one on accident. Give one of them a new name."),
]),
])
};
(DUPLICATE_NAME, doc)
}
fn pretty_runtime_error<'b>(
@ -1420,8 +1459,7 @@ fn pretty_runtime_error<'b>(
shadow,
kind,
} => {
doc = report_shadowing(alloc, lines, original_region, shadow, kind);
title = DUPLICATE_NAME;
(title, doc) = report_shadowing(alloc, lines, original_region, shadow, kind);
}
RuntimeError::LookupNotInScope(loc_name, options) => {

View file

@ -6198,18 +6198,13 @@ All branches in an `if` must have the same type!
@r###"
DUPLICATE NAME /code/proj/Main.roc
The `Result` name is first defined here:
1 app "test" provides [main] to "./platform"
But then it's defined a second time here:
This alias has the same name as a builtin:
4 Result a b : [Ok a, Err b]
^^^^^^^^^^^^^^^^^^^^^^^^^^
Since these aliases have the same name, it's easy to use the wrong one
on accident. Give one of them a new name.
All builtin aliases are in scope by default, so I need this alias to
have a different name!
TOO FEW TYPE ARGUMENTS /code/proj/Main.roc
@ -6241,18 +6236,13 @@ All branches in an `if` must have the same type!
@r###"
DUPLICATE NAME /code/proj/Main.roc
The `Result` name is first defined here:
1 app "test" provides [main] to "./platform"
But then it's defined a second time here:
This alias has the same name as a builtin:
4 Result a b : [Ok a, Err b]
^^^^^^^^^^^^^^^^^^^^^^^^^^
Since these aliases have the same name, it's easy to use the wrong one
on accident. Give one of them a new name.
All builtin aliases are in scope by default, so I need this alias to
have a different name!
TOO MANY TYPE ARGUMENTS /code/proj/Main.roc
@ -7435,18 +7425,13 @@ All branches in an `if` must have the same type!
@r###"
DUPLICATE NAME /code/proj/Main.roc
The `Result` name is first defined here:
1 app "test" provides [main] to "./platform"
But then it's defined a second time here:
This alias has the same name as a builtin:
4 Result a b : [Ok a, Err b]
^^^^^^^^^^^^^^^^^^^^^^^^^^
Since these aliases have the same name, it's easy to use the wrong one
on accident. Give one of them a new name.
All builtin aliases are in scope by default, so I need this alias to
have a different name!
"###
);
@ -10710,4 +10695,77 @@ All branches in an `if` must have the same type!
be safely removed!
"###
);
test_report!(
custom_type_conflicts_with_builtin,
indoc!(
r#"
Nat := [ S Nat, Z ]
""
"#
),
@r###"
DUPLICATE NAME /code/proj/Main.roc
This opaque type has the same name as a builtin:
4 Nat := [ S Nat, Z ]
^^^^^^^^^^^^^^^^^^^
All builtin opaque types are in scope by default, so I need this
opaque type to have a different name!
"###
);
test_report!(
unused_value_import,
indoc!(
r#"
app "test" imports [List.{ concat }] provides [main] to "./platform"
main = ""
"#
),
@r###"
UNUSED IMPORT /code/proj/Main.roc
`List.concat` is not used in this module.
1 app "test" imports [List.{ concat }] provides [main] to "./platform"
^^^^^^
Since `List.concat` isn't used, you don't need to import it.
"###
);
test_report!(
#[ignore = "https://github.com/roc-lang/roc/issues/4096"]
unnecessary_builtin_module_import,
indoc!(
r#"
app "test" imports [Str] provides [main] to "./platform"
main = Str.concat "" ""
"#
),
@r###"
"###
);
test_report!(
#[ignore = "https://github.com/roc-lang/roc/issues/4096"]
unnecessary_builtin_type_import,
indoc!(
r#"
app "test" imports [Decode.{ DecodeError }] provides [main, E] to "./platform"
E : DecodeError
main = ""
"#
),
@r###"
"###
);
}

548
design/Abilities.md Normal file
View file

@ -0,0 +1,548 @@
# Proposal: Abilities in Roc
Status: we invite you to try out abilities for beta use, and are working on resolving known limitations (see issue [#2463](https://github.com/roc-lang/roc/issues/2463)).
This design idea addresses a variety of problems in Roc at once. It also unlocks some very exciting benefits that I didn't expect at the outset! It's a significant addition to the language, but it also means two other language features can be removed, and numbers can get a lot simpler.
Thankfully it's a nonbreaking change for most Roc code, and in the few places where it actually is a breaking change, the fix should consist only of shifting a handful of characters around. Still, it feels like a big change because of all the implications it brings. Here we go!
## Background
Elm has a few specially constrained type variables: `number`, `comparable`, `appendable`, and the lesser-known `compappend`. Roc does not have these; it has no `appendable` or `compappend` equivalent, and instead of `number` and `comparable` it has:
- `Num *` as the type of number literals, with type aliases like `I64 : Num (Integer Signed64)`
- The functionless constraint for type variables; for example, the type of `Bool.isEq` is `'var, 'var -> Bool` - and the apostrophe at the beginning of `'var` means that it must represent a type that has no functions anywhere in it.
There are a few known problems with this design, as well as some missed opportunities.
### Problem 1: Nonsense numbers type-check
Right now in Roc, the following type-checks:
```coffee
x : Num [ Whatever Str, Blah (List {} -> Bool) ]
x = 5
```
This type-checks because the number literal 5 has the type `Num *`, which unifies with any `Num` - even if the type variable is complete nonsense.
It's not clear what should happen here after type-checking. What machine instructions should Roc generate for this nonsense number type? Suppose I later wrote (`if x + 1 > 0 then … `) - what hardware addition instruction should be generated there?
Arguably the compiler should throw an error - but when? We could do it at compile time, during code generation, but that goes against the design goal of "you can always run your Roc program, even if there are compile-time errors, and it will get as far as it can."
So then do we generate a runtime exception as soon as you encounter this code? Now Roc's type system is arguably unsound, because this is a runtime type error which the type checker approved.
Do we add an extra special type constraint just for Num to detect this during the type-checking phase? Now Num is special-cased in a way that no other type is…
None of these potential solutions have ever felt great to me.
### Problem 2: Custom number types can't use arithmetic operators
Roc's ordinary numbers should be enough for most use cases, but there are nice packages like [elm-units](https://package.elm-lang.org/packages/ianmackenzie/elm-units/latest/) which can prevent [really expensive errors](https://spacemath.gsfc.nasa.gov/weekly/6Page53.pdf) by raising compile-time errors for mismatched units...at the cost of having to sacrifice normal arithmetic operators. You can't use `+` on your unit-ful numbers, because `+` in Roc desugars to `Num.add`, not (for example) `Quantity.add`.
Also, if 128-bit integers aren't big enough, because the numbers you're working with are outside the undecillion range (perhaps recording the distance between the Earth and the edge of the universe in individual atoms or something?) maybe you want to make an arbitrary-sized integer package. Again, you can do that, but you can't use `+` with it. Same with vector packages, matrix packages, etc.
This might not sound like a big problem (e.g. people deal with it in Java land), but in domains where you want to use custom numeric types, not having this is (so I've heard) a significant incentive to use plain numbers instead of more helpful data types.
### Problem 3: Decoders are still hard to learn
Roc is currently no different from Elm in this regard. I only recently realized that the design I'm about to describe can also address this problem, but I was very excited to discover that!
### Problem 4: Custom collection equality
Let's suppose I'm creating a custom data structure: a dictionary, possibly backed by a hash map or a tree. We'll ignore the internal structure of the storage field for now, but the basic technique we'd use would be a private tag wrapper to make an opaque type:
```coffee
Dict k v : [ @Dict { storage : … } ]
```
Today in Roc I can make a very nice API for this dictionary, but one thing I can't do is get `==` to do the right thing if my internal storage representation is sensitive to insertion order.
For example, suppose this `Dict` has an internal storage of a binary tree, which means it's possible to get two different internal storage representations depending on the order in which someone makes the same `Dict.insert` calls. Insertion order shouldn't affect equality - what matters is if the two dictionaries contain the same elements! - but by default it does, because `==` only knows how to check if the internal structures match exactly.
This feels like a significantly bigger problem in Roc than it is in Elm, because:
- It's more likely that people will have applications where custom data structures are valuable, e.g. to efficiently store and retrieve millions of values in memory on a server. (This wouldn't likely happen in a browser-based UI.) Discord [ran into a use case like this](https://discord.com/blog/using-rust-to-scale-elixir-for-11-million-concurrent-users) in Elixir, and ended up turning to Rust FFI to get the performance they needed; I'm optimistic that we can get acceptable performance for use cases like this out of pure Roc data structure implementations, and pure Roc data structures would be much more ergonomic than interop - since having to use Task for every operation would be a significant downside for a data structure.
- I want to make testing low-friction, especially within the editor, and some of the ideas I have for how to do that rely on `==` being automatically used behind the scenes to compare values against known good values. If someone wrote tests that relied on `==` and then wanted to swap out a data structure for a custom one (e.g. because they ran into the scaling issues Discord did), it would be extra bad if the new data structure stopped working with all the existing tests and they all had to be rewritten to no longer use these convenient testing features and instead use a custom `Dict.contentsEq` or something instead.
This is one of the most serious problems on this list. Not for the short term, but for the long term.
### Problem 5: How to specify functionlessness in documentation
In Roc's current design, certain types have a functionless constraint. For example, in `Bool.isEq : 'val, 'val -> Bool`, the type variable `'val` means "a type that contains no functions, which we are naming val here."
In this design, it's necessarily a breaking change when a type goes from functionless to function-ful, because that type can no longer be used with the `==` operator (among other things).
How do we report on that breaking change? What's the type diff? Just write the sentence "The type Foo was functionless before, but now it isn't" and call it a day? There are solutions to this, but I haven't encountered any I'm particularly fond of.
There's also a related problem with how to display it in documentation. If I have an opaque type that is functionless (as they usually will be), how should the docs display that? A little icon or something? It's more noteworthy when a type is function-ful, so should that be displayed as an icon instead even though there's only syntax in the language for function-less?
This is definitely solvable, but once again I can't name any solutions I love.
### Problem 6: No nice way to specify editor-specific code
One of the goals for Roc is to have packages ship with editor integrations.
For example, let's say I'm making a custom data structure like the `Dict` from earlier. I want to be able to render an interactive "expando" style `Dict` in the editor, so when someone is in the editor looking at a trace of the values running through their program, they can expand the dictionary to look at just its key-value pairs instead of having to wade through its potentially gnarly internal storage representation. It's a similar problem to equality: as the author of `Dict`, I want to customize that!
The question is how I should specify the rendering function for `Dict`. There isn't an obvious answer in current Roc. Would I write a view function in `Dict.roc`, and the editor just looks for a function by that name? If so, would I expose it directly from that module? If so, then does that mean the API docs for Dict will include a view function that's only there for the editor's benefit? Should there be some special language keyword to annotate it as "editor-only" so it doesn't clutter up the rest of the API docs?
As with the `Num *` problem, there are various ways to solve this using the current language primitives, but I haven't found any that seem really nice.
### Problem 7: Record-of-function passing
This is a minor problem, but worth noting briefly.
In Roc's development backend, we do mostly the same thing when generating X86-64 instructions and ARM instructions. However, there are also several points in the process where slightly different things need to happen depending on what architecture we're targeting.
In Rust, we can use traits to specialize these function calls in a way where Rust's compiler will monomorphize specialized versions of one generic function for each architecture, such that each specialized function does direct calls to the appropriate architecture-specific functions at the appropriate moments. It's exactly as efficient as if those specialized functions had each been written by hand, except they all get to share code.
In Roc, you can achieve this same level of reuse by passing around a record of functions, and calling them at the appropriate moments. While this works, it has strictly more overhead potential than the trait-based approach we're using in Rust. Maybe after a bunch of LLVM inlining and optimization passes, it will end up being equivalent, but presumably there will be cases where it does not.
Is the amount of overhead we're talking about here a big deal? Maybe, maybe not, depending on the use case. This is definitely a niche situation, but nevertheless a missed opportunity for some amount of speed compared to what other languages can do.
## Proposal: Abilities
This proposal is about a new language feature called "abilities," which addresses all of these problems in a nice way, while also making some other things possible in the language.
Abilities are similar to traits in Rust. Here's how the type of addition would change from today's world to the Abilities world:
**Today:**
```coffee
Num.add : Num a, Num a -> Num a
```
**Abilities:**
```coffee
Num.add : number, number -> number
where number has Num
```
The new language keywords are emphasized in bold.
That where `number` has `Num` part is saying that whatever type gets used in place of the number type variable needs to have the Num ability. All the current number types (`I64`, `Nat`, etc.) would have the `Num` ability, the integer types would have the `Int` ability, and the fractional types would have the `Frac` ability.
All of those numeric abilities would be builtins, but you could also define your own custom abilities. Like Rust traits today (that is, Rust 1.56), abilities would not be higher-kinded. The explicit plan would be that they would never be higher-kinded, so it would never be possible to make a `Functor` or `Monad` ability.
### Number Literals
Abilities can require other abilities. For example, to have the `Int` ability, you also need to have the `Num` ability. This means that **has `Int`** is strictly more constraining than **has `Num`**, which in turn means that we can change the type of number literals to be "an unbound variable that has the `Num` ability," similarly to what Haskell does.
Here's how that would look in the REPL:
**Today:**
```coffee
» 5
5 : Num *
```
**Abilities:**
```coffee
» 5
5 : number
where number has Num
```
I'm not sure which version is more beginner-friendly, to be honest.
The latter is more verbose, but it's much easier to guess roughly what it means. The `*` in `Num *` isn't really self-descriptive, so a beginner playing around in the repl who hasn't learned about type variables yet (let alone wildcard type variables) seems less likely to have any useful intuition about what `Num *` is saying compared to what `where number has Num` is saying.
This change to number literals would solve [Problem #1](#problem-1-nonsense-numbers-type-check) (nonsense numbers type-check) completely. The following would no longer type-check:
```coffee
x : Num [ Whatever Str, Blah (List {} -> Bool) ]
x = 5
```
You could write any of these instead:
```coffee
x : number
where number has Num
x = 5
```
```coffee
x : integer
where integer has Int
x = 5
```
```coffee
x : fraction where fraction has Frac # single-line ok!
x = 5
```
```coffee
x : Nat
x = 5
```
...but there's no opportunity to inject any nonsense that the type checker would accept.
Since you can add abilities to your own custom types (as we'll see later), this means you can add `Num` to your own custom number types (as well as `Int` or `Frac`) and then use them with all the usual arithmetic operators. This solves [Problem #2](#problem-2-custom-number-types-cant-use-arithmetic-operators).
### Functionless Constraints
Here's how the type of `Bool.isEq` would change from the current world (using the functionless constraint with the ' syntax) to an Abilities world:
**Today:**
```coffee
Bool.isEq : 'val, 'val -> Bool
```
**Abilities:**
```coffee
Bool.isEq : val, val -> Bool
where val has Eq
```
Similarly, a hash map collection could have:
```coffee
Dict.insert : k, v, Dict k v -> Dict k v
where k has Hash
```
If Hash doesn't require `Eq` for some reason (although it probably should), then `Dict.insert` could require multiple abilities as part of the annotation, e.g. `where k has Hash, Eq`
In the Abilities world, Roc no longer needs the concept of the *functionless* constraint, and it can be removed from the language. Abilities can cover all those use cases.
### Default Abilities
One of the many things I like about Elm is that I can make anonymous records and tuples have them Just Work with the `==` operator. In contrast, in Rust I have to name the struct and then add `#[deriving(Eq)]` to it if I want `==` to work on it.
However, in Rust, tuples work basically like how they do in Elm: equality Just Works as long as all the elements in the tuple have `Eq`. In fact, Rust tuples automatically derive a bunch of traits. We can do something similar in Roc.
Specifically, the idea would be to have all records and tags automatically have the following abilities by default, wherever possible. (For example, types that contain functions wouldn't get these abilities, because these operations are unsupported for functions!)
1. Eq
2. Hash
3. Sort
4. Encode
5. Decode
Eq and Hash work like they do in Rust, although as previously noted, I think Hash should probably require Eq. Sort is like Ord in Rust, although I prefer the name Sort because I think it should only be for sorting and not general ordering (e.g. I think the <, >, <=, and >= operators should continue to only accept numbers, not other sortable types like strings and booleans).
As for Encode and Decode...to put it mildly, they are exciting.
### Encode and Decode
[serde](https://docs.serde.rs/serde/) is among the most widely used Rust crates in the world - maybe the absolute most. It's for **ser**ializing and **de**serializing; hence, **serde**.
The way it works is that it provides `Serializable` and `Deserializable` traits that you can derive for your types (e.g. for your User type), as well as `Serializer` and `Deserializer` traits that anyone can define for their encoding formats (e.g. a JSON serializer).
[Putting these together](https://github.com/serde-rs/json#parsing-json-as-strongly-typed-data-structures), I can add `#[deriving(Serialiable, Deserializable)]` to my struct User definition, and then run something like `let user: User = serde_json::from_str(json)?` to turn my JSON into a User while handling failed decoding along the way via a `Result`.
Having spent a lot of time teaching JSON decoders to beginning Elm programmers, I can confidently say this seems massively easier for beginners to learn - even if it means they will abruptly have a lot to learn on the day where they want to do some more advanced decoding. It's also much more concise.
In the Abilities world, we can take this a step further than Rust does. We can have `Encode` and `Decode` as builtin abilities (and then also `Encoder` and `Decoder`, except they work like Serializers and Deserializers do in serde; you have an Encoder or Decoder for a particular encoding - e.g. JSON or XML - rather than for the value you want to encode or decode), and we can have the compiler automatically define them when possible, just like it does for `Eq` and the others.
This would mean that in Roc you could do, without any setup other than importing a package to get a generic **Json.decoder**, the following:
```coffee
result : Result User [ JsonDecodingErr ]*
result = Decode.decode Json.decoder jsonStr
```
So it would be like serde in Rust, except that - like with Elm records - you wouldn't even need to mark your User as deriving Encode and Decode; those abilities would already be there by default, just like `Eq`, `Hash`, and `Sort`.
This would solve [Problem #3](#problem-3-decoders-are-still-hard-to-learn), eliminating the need for a beginner curriculum to include the one technique I've seen beginning Elm programmers struggle the most to learn. That's a very big deal to me! I don't know whether decoding serialized data will be as common in Roc as it is in Elm, but I certainly expect it to come up often.
Other nice things about this design:
- Since Encode and Decode are builtins, no packages need to depend on anything to make use of them. In Rust, it's currently a bit awkward that all packages that want to offer serializability have to depend on serde; it has become a nearly ubiquitous dependency in the Cargo ecosystem. By making it a builtin, Roc can avoid that problem.
- Since Encode and Decode are agnostic to the actual encoding format, anyone can write a new Encoder and Decoder for whatever their new format is (e.g. XSON, the format that looks at XML and JSON and says "why not both?" - which I just made up) and have every serializable Roc type across the entire ecosystem instantly able to be serialized to/from that format.
- This design still allows for evolving a default decoder into a bespoke decoder that can cover the same use cases that elm/json does (and a potentially very similar API).
I haven't looked into the details of what the exact design of this system would be, but at a glance it seems like based on the design of abilities and the design of serde, it should work out. (There may always be unexpected issues though!)
## Adding Abilities to a Type
So we've talked about default abilities, and how various builtins would use them. What about custom types? How would I make an actual `Dict` type with its own definition of equality?
To do that, we need to talk about a change to the language that was originally motivated by abilities, but which ultimately seems like a good change even if abilities weren't a thing.
### Newtypes
Let's suppose Roc no longer has private tags, but does have this syntax:
```coffee
UserId := U64
```
This declares a new concrete type in scope called `UserId`, which at runtime is a `U64` with no additional overhead.
To create one of these `UserId` values, we put a @ before the type and call it:
```coffee
userId : UserId
userId = @UserId 0
```
The expression `@UserId` has the type `U64 -> UserId`, which the compiler knows because this declaration is in scope:
```coffee
UserId := U64
```
Trying to use `@UserId` when a `UserId :=` declaration isn't in scope would give a compiler error.
`@UserId` can also be used in a pattern, to destructure the wrapped `U64`:
```coffee
getU64 : UserId -> U64
getU64 = \@UserId u64 -> u64
```
In this way, `@UserId` can be used almost identically to how private tags work today: call (`@UserId someU64`) to create a wrapped `U64`, and pattern match on `\@UserId someU64 ->` to destructure it. The only difference is that the resulting type is `UserId` instead of `[ @UserId ]`.
Because the `@` prefix syntax can only refer to a newtype declaration that's currently in scope, the newtype's implementation is hidden from other modules by default. (Of course you can still expose the type and functions to work on it.)
This design has a few advantages over private tags:
1. It's more focused. Wrapper types with hidden implementations are really the exact use case that private tags were designed for; the concept of a union of multiple private tags was never really necessary, and in this world it doesn't even exist.
2. It means there's just one "tags" concept, just like there's one "records" concept. No more "global tags and private tags" split.
3. The `UserId := U64` declaration is more concise than the private tag equivalent of `UserId : [ @UserId U64 ]`, and it speeds up type checking because there would be (many) fewer type aliases for the compiler to resolve.
4. It enables traditional phantom types, which Roc currently lacks - e.g.
`Quantity count units := count`
in Roc would make units a phantom type like in this Elm declaration:
`type Quantity count units = Quantity count`
Even considered completely separately from Abilities, this "newtypes" design seems like a better design than private tags.
### Newtypes and Abilities
Another advantage the newtypes design has over private tags is that it offers a natural place to declare what abilities a type has.
With private tags, this isn't really possible because I can use @Foo in multiple different places in the same module, with multiple different payload arities and types - and even if I use a type alias to give it a shorter name, that type alias is still just an alias; it can't alter the characteristics of the type it's referring to. With the newtypes design, I can refer to a specific concrete type, and not just an alias of it - meaning I actually can alter its characteristics.
As an example, let's make a newtype declaration for Dict, and once again ignore the internal structure of the storage field for now:
```coffee
Dict k v := { storage : … }
```
This lets us construct a Dict by calling `@Dict { storage }` and destructure it similarly.
As discussed earlier, one problem with creating custom data structures like this in today's Roc is that `==` doesn't necessarily do the right thing. Here's a way to solve this issue:
```coffee
Dict k v := { storage : … } has
[ Eq { isEq, isNotEq } ]
```
This says (among other things) that the `Dict` type has the `Eq` ability. For a type to have `Eq`, it must provide two functions: `isEq` and `isNotEq`. Here's how those look:
```coffee
isEq : Dict k v, Dict k v -> Bool
isNotEq : Dict k v, Dict k v -> Bool
```
In this `Eq { isEq, isNotEq }` declaration, I'm saying that `isEq` and `isNotEq` are functions already in scope. I could also choose different names using record literal syntax, e.g. `Eq { isEq: dictIsEq, isNotEq: dictIsNotEq }` - the relevant part is that I'm specifying the names of the functions (which must also be in scope) which specify how `Eq` for Dict should work.
Now that I've specified this, when I use `==` on two `Dict` values, this `isEq` function will get run instead of the default `==` implementation. This solves [Problem #3](#problem-3-decoders-are-still-hard-to-learn)!
I can also write something like has `Num` and provide the relevant functions to obtain a unit-ful number type - which solves [Problem #2](#problem-2-custom-number-types-cant-use-arithmetic-operators).
### Default Abilities for Newtypes
By default, if I don't use the has keyword when defining a newtype, Roc will give the type all the default builtin abilities it's eligible to have - so for example, it would get `Eq` and `Hash` by default unless it contains a function, in which case it's not eligible.
In this example, because I wrote has, the `Dict` type has `Eq` as well as the other default ones. I could instead use has `only`, which means `Dict` should not have any of the default abilities, and should instead have only the ones I list.
```coffee
Dict k v := { storage : … } has
[
Eq { isEq, isNotEq },
Hash { hash },
Sort { compare },
Foo { bar: baz }
]
```
Using `has` means if new default abilities are later added to the language, `Dict` will get them automatically. This may or may not be desirable, depending on what the ability is; maybe, like equality, it will be wrong by default for `Dict`, and maybe I'll wish I had chosen has `only`.
On the other hand, if everyone uses has `only` everywhere as a precaution, and a new default ability gets added to the language, a staggering amount of collective hours would be spent going around adding it to all the has `only` declarations for `UserId` and such. So a good guideline might be for custom collections like `Dict` to recommend using has `only`, and for thin wrappers like `UserId` to use `has custom`.
Of note, this syntax neatly solves [Problem #5](#problem-5-how-to-specify-functionlessness-in-documentation) - where functionlessness is awkward to talk about in API type diffs and documentation. This is a straightforward way to render the `Dict` type in documentation:
```coffee
Dict k v has only
[ Eq, Hash, Sort ]
```
I can immediately see exactly what abilities this type has. The same is true if I used has `custom` or omitted the has clause entirely. API diffs can use this same representation, with a diff like +Eq -Sort to show which abilities were added or removed.
### Encode and Hash
I'm not sure if we actually need separate Hash and Encode abilities. At a high level, hashing is essentially encoding a value as an integer. Since all default types will get Encode anyway, maybe all we need is to have "hashers" be implemented as Encoders. This would mean there's one less default ability in the mix, which would be a nice simplification.
However, I'm not sure what the differences are between Rust's Hash trait and Hasher type, and serde's Serializable trait and Serializer types. Maybe there's a relevant difference that would justify having a separate Hash ability. I'm not sure! I figure it's at least worth exploring.
It might look surprising at first for a `Dict` implemented as a hash map to require that its keys have `Encode`, but I don't think that's a significant downside.
### Encode and toStr
Similarly, anyone could write a `toStr` function that works on any type that has `Encode`, by using an Encoder which encodes strings.
In Elm, having a general toString function proved error-prone (because it was so flexible it masked type mismatches - at work I saw this cause a production bug!) which was why it was replaced by String.fromInt and String.fromFloat. I had originally planned to do the same in Roc, but Encode would mean that anyone can write a flexible toStr and publish it as a package without acknowledging the potential for masking bugs.
Knowing that there's a 100% chance that would happen eventually, it seems like it would be better to just publish an Encode.str which encodes values as strings, and which can be used like toStr except you have to actually call (`Encode.encode Encode.str value`) instead of `toStr`. This would mean that although it's an option, it's (by design!) less ergonomic than a flexible function like Num.fromStr, which means the path of least resistance (and least error-proneness) is to use `Num.fromStr` instead of this.
One benefit to having something like `Encode.str` available in the language is that it can be nice for logging - e.g. when sending tracing information to a server that only programmers will ever see, not users. That's the only situation where I've ever personally missed the old Elm `toString`.
## Defining New Abilities
Here's how I might have defined Eq if it weren't already a builtin ability:
```coffee
Eq has { isEq, isNotEq }
isEq : val, val -> Bool where val has Eq
isNotEq : val, val -> Bool where val has Eq
```
There are two steps here:
1. Define what functions Eq has
2. Declare those functions as top-level type annotations with no bodies
Having done both, now if anyone wants to say that another type **has Eq**, that type needs to implement these two functions. I can also expose these functions from this module directly - so for example, if I'm in the Bool module, I can have it do `exposes [ isEq, isNotEq ]`, and now anyone can call `Bool.isEq` and it will run this function (or rather, the implementation of this function on whatever type that **has Eq** which was passed to `Bool.isEq`!)
Within these `isEq` and `isNotEq` functions' types, **has Eq** is allowed even though those functions are part of the definition of what `Eq` means. The compiler detects these and treats them essentially like "Self" in Rust - that is, when I say that my `Dict k v` newtype **has Eq**, its `isEq` implementation will have the type` Dict k v, Dict k v -> Bool` because the compiler will have replaced the val in the `Eq` definition with `Dict k v`.
The compiler knew to do that substitution with **val** because of **val has Eq** in the declaration of `isEq` itself. If `isEq` also had other abilities in its has clause, e.g. **val has Eq, foo has Sort**, it wouldn't do the substitutions with foo because **Sort** is not the name of the ability currently being defined.
For this reason, if you are defining a function on **Eq** (such as **isEq**), and you have more than one type variable which **has Eq**, the result is a compiler error. This would be like trying to have more than one `Self` in Rust!
### Abilities that depend on other abilities
I mentioned earlier that in order to have either Int or Frac, a type must also have the Num ability. You can add those constraints after the **has** keyword, like so:
```coffee
Int has Num, { …functions go here as normal… }
```
Now whenever someone wants to make a newtype which **has Int**, that newtype must also explicitly specify that it **has Num** - otherwise, they'll get a compiler error. Similarly, any function which requires that an argument **has Num** will also accept any type that **has Int**.
### Defining abilities for existing types after the fact
It's conceivable that defining a new ability could support adding that ability to existing types. For example, maybe I make a new ability called Foo, and I want all numbers to have Foo.
It's too late for me to go back and get Num's newtype declaration to specify has Foo, because Num existed before Foo did!
It's possible that Roc could support a way to do this when defining a new ability. It could say for example `Eq has {...} with [ Num { isEq: numIsEq, … } ]`
However, upon reflection, I think this idea is fatally flawed and we shouldn't do it.
On the positive side, this wouldn't introduce any ambiguity. Because Roc doesn't allow cyclic imports, it's already impossible to define two conflicting definitions for a given ability function (e.g. if I define isEq for numbers when defining Num, then Num must import the module where Eq is defined, meaning I can't possibly have Eq's definition mention Num - or else the module where Eq is defined would have had to import Num as well, creating an import cycle!) so that can't happen.
This also wouldn't necessarily introduce any "you need to import a trait for this to work" compiler errors like we see in Rust.
If I'm passing a newtype named Blah to a function which expects that it **has Baz**, then by virtue of the fact that I have a Blah at all, I must have the module where it's defined already loaded in the current build (maybe not as a direct dependency of my module, but definitely as an indirect dependency). Similarly, because I'm calling a function that **has Baz**, I must also (at least indirectly) have the module where Baz is defined loaded. If both modules are loaded, I will definitely be able to find the function implementation(s) I need in either the one or the other, and because Roc wouldn't support orphan instances, I don't need to check any other modules.
However, this can cause some serious problems. Once I've done this, now the module where the type is defined can never import the module where the ability is defined. What if the author of that module wants to define that a different type defined in that module has this ability? Tough luck; can't import the ability module, because that would create an import cycle. Gotta move that type out of that module, even if that would create other problems.
This is even worse if the type and the ability are in different packages; now your entire package can't even depend on the package where the ability is defined! What if the reason the author of the ability added it to that other type was just to avoid having to coordinate with the author of the other package (or just to save them some time)? Now they've locked that author out from controlling their own type!
From this point, even if both authors coordinate, the only way to permit the author of the type to take back control over the implementation of that ability on that type is if the ability author releases a breaking change of that package which drops the ability from the type - so that the author of the type can finally import it without causing a cyclic dependency. I want to incentivize strong backwards compatibility commitments for package authors once their APIs have settled, and this feature would make such commitments unworkable.
All of this makes me think that "if you want a type to have the ability you're defining, you should coordinate with that author" is the best policy to encourage, and in that world, the feature makes no sense except perhaps in the very specific case of builtin types (which necessarily can't depend on packages). Since there are a (small) finite number of those, it seems plausible that the ability author can do one-off special-case workarounds for those instead of needing a separate language feature.
### Abilities for Editor-Specific Code
I don't know exactly what the API for editor plugins should be yet, but they do have some characteristics that are important:
* Making or modifying editor plugins should be so easy, basically everyone does it. This means that code for editor plugins should be written in normal Roc, and the API should have a shallow learning curve.
* Editor plugins should ship with packages (or even just modules within a local project), but should have no impact on runtime performance of those modules/packages. So it's located there, but can't affect the surrounding code.
* There's more than one way to integrate with the editor. For example:
* You can add entries to context menus for certain types
* You can override the default way a type would be rendered in the editor (e.g. an expando for a custom collection)
* You can make big, interactive integrations like a [regex explorer](https://www.youtube.com/watch?v=ZnYa99QoznE&t=6105s)
Abilities offer a nice way to address all of these.
* They can ship with modules and packages without affecting runtime performance. They describe a new ability for a type (namely, an editor integration for that type), but as long as no production code uses it, runtime performance is unaffected - they're just functions that never get called, and won't even be present in the final optimized binary.
* Since abilities are used elsewhere in the language, there's nothing editor-specific to learn other than the APIs themselves (which is unavoidable), so the learning curve for how to add editor plugins is minimal: just declare that your newtype has a particular ability, and the editor will pick up on it.
* Since any given type can have multiple abilities, the different ways to integrate with the editor can too. There can be one ability for adding context menu items, another for specifying how the type renders, etc.
In this way, abilities solve [problem #6](#problem-6-no-nice-way-to-specify-editor-specific-code).
### Avoiding the Classification Trap
Although I think custom user-defined abilities are worth having in the language because they address [Problem #7](#problem-7-record-of-function-passing), I hope they are used rarely in practice.
I chose the name "ability" rather than like Trait or Typeclass because I don't want to encourage *classification* - that is, using the language feature to spend a bunch of time thinking about how to classify types by what they "are."
This seems to be a common exercise in statically typed languages with classes; see for example the well-known introductory example "`a Bicycle is a Vehicle`" which to me is primarily teaching students how to waste time adding complexity to their code bases for the satisfaction of classifying things, and no practical benefit.
(This happens in FP too; I doubt [Semiring](https://pursuit.purescript.org/packages/purescript-prelude/5.0.1/docs/Data.Semiring) ends up in a standard library because people kept opening issues saying they were unable to write some really valuable production code without it. A more likely history of that design decision is that a semiring is the mathematically proper way to `classify` those particular `types`, and `typeclasses` encourage classifying types right there in the name.)
In my view, type classification is a tempting but ultimately counterproductive exercise that puts a tax on a community which grows linearly with the size of that community: once enough people start doing it, everyone becomes under pressure to do the same, lest their code look suspiciously under-classified. I don't want this to happen in Roc.
Hopefully the name "abilities" will frame the feature as giving a type a new ability and nothing more. It's not about saying what the type *is*, but rather what you can do with it.

View file

@ -11,6 +11,8 @@ hosted Effect
envDict,
envVar,
cwd,
setCwd,
exePath,
stdoutLine,
stderrLine,
stdinLine,
@ -34,6 +36,8 @@ fileReadBytes : List U8 -> Effect (Result (List U8) InternalFile.ReadErr)
dirList : List U8 -> Effect (Result (List (List U8)) InternalDir.ReadErr)
envDict : Effect (Dict Str Str)
envVar : Str -> Effect (Result Str {})
exePath : Effect (Result (List U8) {})
setCwd : List U8 -> Effect (Result {} {})
# If we encounter a Unicode error in any of the args, it will be replaced with
# the Unicode replacement char where necessary.

View file

@ -1,9 +1,10 @@
interface Env
exposes [cwd, dict, var, decode] imports [Task.{ Task }, Path.{ Path }, InternalPath, Effect, InternalTask, EnvDecoding]
exposes [cwd, dict, var, decode, exePath, setCwd]
imports [Task.{ Task }, Path.{ Path }, InternalPath, Effect, InternalTask, EnvDecoding]
## Reads the [current working directory](https://en.wikipedia.org/wiki/Working_directory)
## from the environment. File operations on relative [Path]s are relative to this directory.
cwd : Task Path [CwdUnavailable]* [Env]*
cwd : Task Path [CwdUnavailable]* [Read [Env]*]*
cwd =
effect = Effect.map Effect.cwd \bytes ->
if List.isEmpty bytes then
@ -13,20 +14,32 @@ cwd =
InternalTask.fromEffect effect
# ## Sets the [current working directory](https://en.wikipedia.org/wiki/Working_directory)
# ## in the environment. After changing it, file operations on relative [Path]s will be relative
# ## to this directory.
# setCwd : Path -> Task {} [InvalidCwd]* [Env]*
# setCwd = InternalTask.fromEffect Effect.setCwd
# ## Gets the path to the currently-running executable.
# exePath : Task Path [ExePathUnavailable]* [Env]*
# exePath = InternalTask.fromEffect Effect.setCwd
## Sets the [current working directory](https://en.wikipedia.org/wiki/Working_directory)
## in the environment. After changing it, file operations on relative [Path]s will be relative
## to this directory.
setCwd : Path -> Task {} [InvalidCwd]* [Write [Env]*]*
setCwd = \path ->
Effect.setCwd (InternalPath.toBytes path)
|> Effect.map (\result -> Result.mapErr result \{} -> InvalidCwd)
|> InternalTask.fromEffect
## Gets the path to the currently-running executable.
exePath : Task Path [ExePathUnavailable]* [Read [Env]*]*
exePath =
effect =
Effect.map Effect.exePath \result ->
when result is
Ok bytes -> Ok (InternalPath.fromOsBytes bytes)
Err {} -> Err ExePathUnavailable
InternalTask.fromEffect effect
## Reads the given environment variable.
##
## If the value is invalid Unicode, the invalid parts will be replaced with the
## [Unicode replacement character](https://unicode.org/glossary/#replacement_character)
## (`<60>`).
var : Str -> Task Str [VarNotFound]* [Env]*
var : Str -> Task Str [VarNotFound]* [Read [Env]*]*
var = \name ->
Effect.envVar name
|> Effect.map (\result -> Result.mapErr result \{} -> VarNotFound)
@ -38,7 +51,7 @@ var = \name ->
## if this ends up being used like a `Task U16 …` then the environment variable
## will be decoded as a string representation of a `U16`.
##
## getU16Var : Str -> Task U16 [VarNotFound, DecodeErr DecodeError]* [Env]*
## getU16Var : Str -> Task U16 [VarNotFound, DecodeErr DecodeError]* [Read [Env]*]*
## getU16Var = \var -> Env.decode var
## # If the environment contains a variable NUM_THINGS=123, then calling
## # (getU16Var "NUM_THINGS") would return a task which succeeds with the U16 number 123.
@ -52,7 +65,7 @@ var = \name ->
## - comma-separated lists (of either strings or numbers), as long as there are no spaces after the commas
##
## Trying to decode into any other types will always fail with a `DecodeErr`.
decode : Str -> Task val [VarNotFound, DecodeErr DecodeError]* [Env]* | val has Decoding
decode : Str -> Task val [VarNotFound, DecodeErr DecodeError]* [Read [Env]*]* | val has Decoding
decode = \name ->
Effect.envVar name
|> Effect.map
@ -70,7 +83,7 @@ decode = \name ->
##
## If any key or value contains invalid Unicode, the [Unicode replacement character](https://unicode.org/glossary/#replacement_character)
## (`<60>`) will be used in place of any parts of keys or values that are invalid Unicode.
dict : Task (Dict Str Str) * [Env]*
dict : Task (Dict Str Str) * [Read [Env]*]*
dict =
Effect.envDict
|> Effect.map Ok
@ -92,7 +105,7 @@ dict =
# ##
# ## If any key or value contains invalid Unicode, the [Unicode replacement character](https://unicode.org/glossary/#replacement_character)
# ## (`<60>`) will be used in place of any parts of keys or values that are invalid Unicode.
# walk : state, (state, Str, Str -> state) -> Task state [NonUnicodeEnv state]* [Env]*
# walk : state, (state, Str, Str -> state) -> Task state [NonUnicodeEnv state]* [Read [Env]*]*
# walk = \state, walker ->
# Effect.envWalk state walker
# |> InternalTask.fromEffect

View file

@ -147,6 +147,22 @@ pub extern "C" fn roc_fx_envVar(roc_str: &RocStr) -> RocResult<RocStr, ()> {
}
}
#[no_mangle]
pub extern "C" fn roc_fx_setCwd(roc_path: &RocList<u8>) -> RocResult<(), ()> {
match std::env::set_current_dir(path_from_roc_path(roc_path)) {
Ok(()) => RocResult::ok(()),
Err(_) => RocResult::err(()),
}
}
#[no_mangle]
pub extern "C" fn roc_fx_exePath(roc_str: &RocStr) -> RocResult<RocList<u8>, ()> {
match std::env::current_exe() {
Ok(path_buf) => RocResult::ok(os_str_to_roc_path(path_buf.as_path().as_os_str())),
Err(_) => RocResult::err(()),
}
}
#[no_mangle]
pub extern "C" fn roc_fx_stdinLine() -> RocStr {
use std::io::{self, BufRead};

View file

@ -15,7 +15,7 @@ app "file-io"
main : Program
main = Program.noArgs mainTask
mainTask : Task ExitCode [] [Write [File, Stdout, Stderr], Read [File], Env]
mainTask : Task ExitCode [] [Write [File, Stdout, Stderr], Read [File, Env]]
mainTask =
path = Path.fromStr "out.txt"
task =

View file

@ -13,8 +13,8 @@ interface Parser.CSV
f64,
]
imports [
Parser.Core.{ Parser, parse, buildPrimitiveParser, fail, const, alt, map, map2, apply, many, maybe, oneorMore, sepBy1, between, ignore, flatten, sepBy },
Parser.Str.{ RawStr, parseStrPartial, oneOf, codeunit, codeunitSatisfies, scalar, digits, strFromRaw },
Parser.Core.{ Parser, parse, buildPrimitiveParser, alt, map, many, sepBy1, between, ignore, flatten, sepBy },
Parser.Str.{ RawStr, oneOf, codeunit, codeunitSatisfies, strFromRaw },
]
## This is a CSV parser which follows RFC4180

View file

@ -18,7 +18,7 @@ interface Parser.Str
digits,
strFromRaw,
]
imports [Parser.Core.{ Parser, ParseResult, const, fail, map, map2, apply, many, oneOrMore, parse, parsePartial, buildPrimitiveParser, between }]
imports [Parser.Core.{ Parser, ParseResult, map, oneOrMore, parse, parsePartial, buildPrimitiveParser }]
# Specific string-based parsers:
RawStr : List U8

View file

@ -56,4 +56,3 @@ enumerate = \elements ->
last
|> List.prepend (inits |> Str.joinWith ", ")
|> Str.joinWith " and "

View file

@ -30,7 +30,7 @@ roc run helloWorld.roc
Some examples like `crates/cli_testing_examples/benchmarks/NQueens.roc` require input after running.
For NQueens, input 10 in the terminal and press enter.
[crates/cli_testing_examples/benchmarks](crates/cli_testing_examples/benchmarks) contains larger examples.
[crates/cli_testing_examples/benchmarks](https://github.com/roc-lang/roc/tree/main/crates/cli_testing_examples/benchmarks) contains larger examples.
**Tip:** when programming in roc, we recommend to execute `./roc check myproject/Foo.roc` before `./roc myproject/Foo.roc` or `./roc build myproject/Foo.roc`. `./roc check` can produce clear error messages in cases where building/running may panic.

View file

@ -393,6 +393,12 @@ when number is
_ -> ""
```
## Booleans
Roc has a `Bool` module (with operations like `Bool.and` and `Bool.or`; Roc does not have
a `Basics` module), and `Bool` is an opaque type. The values `Bool.true` and `Bool.false` work
like `True` and `False` do in Elm.
## Custom Types
This is the biggest semantic difference between Roc and Elm.
@ -527,11 +533,11 @@ the type of the union it goes in.
Here are some examples of using tags in a REPL:
```coffee
> True
True : [True]*
> Red
Red : [Red]*
> False
False : [False]*
> Blue
Blue : [Blue]*
> Ok "hi"
Ok "hi" : [Ok Str]*
@ -643,7 +649,7 @@ by removing the `*`. For example, if you changed the annotation to this...
alwaysFoo : [Foo Str, Bar Bool] -> [Foo Str]*
```
...then the function would only accept tags like `Foo "hi"` and `Bar False`. By writing
...then the function would only accept tags like `Foo "hi"` and `Bar (x == y)`. By writing
out your own annotations, you can get the same level of restriction you get with
traditional algebraic data types (which, after all, come with the requirement that
you write out their annotations). Using annotations, you can restrict even
@ -1285,7 +1291,7 @@ Roc's standard library has these modules:
Some differences to note:
- All these standard modules are imported by default into every module. They also expose all their types (e.g. `Bool`, `List`, `Result`) but they do not expose any values - not even `negate` or `not`. (`True`, `False`, `Ok`, and `Err` are all tags, so they do not need to be exposed; they are globally available regardless!)
- All these standard modules are imported by default into every module. They also expose all their types (e.g. `Bool`, `List`, `Result`) but they do not expose any values - not even `negate` or `not`. (`Ok` and `Err` are ordinary tags, so they do not need to be exposed; they are globally available regardless!)
- In Roc it's called `Str` instead of `String`.
- `List` refers to something more like Elm's `Array`, as noted earlier.
- No `Char`. This is by design. What most people think of as a "character" is a rendered glyph. However, rendered glyphs are comprised of [grapheme clusters](https://stackoverflow.com/a/27331885), which are a variable number of Unicode code points - and there's no upper bound on how many code points there can be in a single cluster. In a world of emoji, I think this makes `Char` error-prone and it's better to have `Str` be the only first-class unit. For convenience when working with unicode code points (e.g. for performance-critical tasks like parsing), the single-quote syntax is sugar for the corresponding `U32` code point - for example, writing `'鹏'` is exactly the same as writing `40527`. Like Rust, you get a compiler error if you put something in single quotes that's not a valid [Unicode scalar value](http://www.unicode.org/glossary/#unicode_scalar_value).

View file

@ -1,8 +1,9 @@
[toolchain]
channel = "1.61.0" # make sure to update the rust version in Earthfile as well
# channel = "nightly-2022-04-03" # nightly to be able to use unstable features
profile = "default"
components = [
# for usages of rust-analyzer or similar tools inside `nix develop`
"rust-src"
]
targets = [ "x86_64-unknown-linux-gnu" ]
targets = [ "x86_64-unknown-linux-gnu" ]