Merge branch 'main' into pass-args-to-cli

This commit is contained in:
Richard Feldman 2022-09-12 23:53:55 -04:00
commit ddbfd48ca3
No known key found for this signature in database
GPG key ID: F1F21AA5B1D9E43B
28 changed files with 697 additions and 189 deletions

26
Cargo.lock generated
View file

@ -475,9 +475,9 @@ dependencies = [
[[package]]
name = "clap"
version = "3.2.18"
version = "3.2.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15f2ea93df33549dbe2e8eecd1ca55269d63ae0b3ba1f55db030817d1c2867f"
checksum = "23b71c3ce99b7611011217b366d923f1d0a7e07a92bb2dbf1e84508c673ca3bd"
dependencies = [
"atty",
"bitflags",
@ -2809,10 +2809,11 @@ checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
[[package]]
name = "pest"
version = "2.1.3"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53"
checksum = "cb779fcf4bb850fbbb0edc96ff6cf34fd90c4b1a112ce042653280d9a7364048"
dependencies = [
"thiserror",
"ucd-trie",
]
@ -3446,7 +3447,7 @@ name = "roc_cli"
version = "0.1.0"
dependencies = [
"bumpalo",
"clap 3.2.18",
"clap 3.2.20",
"cli_utils",
"const_format",
"criterion",
@ -3591,7 +3592,7 @@ dependencies = [
name = "roc_docs_cli"
version = "0.0.1"
dependencies = [
"clap 3.2.18",
"clap 3.2.20",
"libc",
"roc_docs",
]
@ -3740,7 +3741,7 @@ name = "roc_glue"
version = "0.0.1"
dependencies = [
"bumpalo",
"clap 3.2.18",
"clap 3.2.20",
"cli_utils",
"dircpy",
"fnv",
@ -3759,6 +3760,7 @@ dependencies = [
"roc_std",
"roc_target",
"roc_test_utils",
"roc_tracing",
"roc_types",
"strum",
"strum_macros",
@ -3807,7 +3809,7 @@ version = "0.0.1"
dependencies = [
"bincode",
"bumpalo",
"clap 3.2.18",
"clap 3.2.20",
"iced-x86",
"mach_object",
"memmap2 0.5.7",
@ -4365,9 +4367,9 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.139"
version = "1.0.144"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0171ebb889e45aa68b44aee0859b3eede84c6f5f5c228e6f140c0b2a0a46cad6"
checksum = "0f747710de3dcd43b88c9168773254e809d8ddbdf9653b84e2554ab219f17860"
dependencies = [
"serde_derive",
]
@ -4405,9 +4407,9 @@ dependencies = [
[[package]]
name = "serde_derive"
version = "1.0.139"
version = "1.0.144"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc1d3230c1de7932af58ad8ffbe1d784bd55efd5a9d84ac24f69c72d83543dfb"
checksum = "94ed3a816fb1d101812f83e789f888322c34e291f894f19590dc310963e87a00"
dependencies = [
"proc-macro2",
"quote",

View file

@ -60,7 +60,7 @@ roc_editor = { path = "../editor", optional = true }
roc_linker = { path = "../linker" }
roc_repl_cli = { path = "../repl_cli", optional = true }
roc_tracing = { path = "../tracing" }
clap = { version = "3.2.18", default-features = false, features = ["std", "color", "suggestions"] }
clap = { version = "3.2.20", default-features = false, features = ["std", "color", "suggestions"] }
const_format = { version = "0.2.23", features = ["const_generics"] }
bumpalo = { version = "3.8.0", features = ["collections"] }
mimalloc = { version = "0.1.26", default-features = false }

View file

@ -10,7 +10,7 @@ description = "Our own markup language for Roc code. Used by the editor and the
roc_ast = { path = "../ast" }
roc_module = { path = "../compiler/module" }
roc_utils = { path = "../utils" }
serde = { version = "1.0.130", features = ["derive"] }
serde = { version = "1.0.144", features = ["derive"] }
palette = "0.6.1"
snafu = { version = "0.7.1", features = ["backtraces"] }
bumpalo = { version = "3.8.0", features = ["collections"] }

View file

@ -33,6 +33,9 @@ interface Str
toU8,
toI8,
toScalars,
replaceEach,
replaceFirst,
replaceLast,
splitFirst,
splitLast,
walkUtf8WithIndex,
@ -276,6 +279,65 @@ countUtf8Bytes : Str -> Nat
## string slice that does not do bounds checking or utf-8 verification
substringUnsafe : Str, Nat, Nat -> Str
## Returns the string with each occurrence of a substring replaced with a replacement.
## If the substring is not found, returns `Err NotFound`.
##
## Str.replaceEach "foo/bar/baz" "/" "_" == Ok "foo_bar_baz"
replaceEach : Str, Str, Str -> Result Str [NotFound]*
replaceEach = \haystack, needle, flower ->
when splitFirst haystack needle is
Ok { before, after } ->
# We found at least one needle, so start the buffer off with
# `before` followed by the first replacement flower.
Str.reserve "" (Str.countUtf8Bytes haystack)
|> Str.concat before
|> Str.concat flower
|> replaceEachHelp after needle flower
|> Ok
Err err -> Err err
replaceEachHelp : Str, Str, Str, Str -> Str
replaceEachHelp = \buf, haystack, needle, flower ->
when splitFirst haystack needle is
Ok { before, after } ->
buf
|> Str.concat before
|> Str.concat flower
|> replaceEachHelp after needle flower
Err NotFound -> Str.concat buf haystack
expect Str.replaceEach "abXdeXghi" "X" "_" == Ok "ab_de_ghi"
## Returns the string with the first occurrence of a substring replaced with a replacement.
## If the substring is not found, returns `Err NotFound`.
##
## Str.replaceFirst "foo/bar/baz" "/" "_" == Ok "foo_bar/baz"
replaceFirst : Str, Str, Str -> Result Str [NotFound]*
replaceFirst = \haystack, needle, flower ->
when splitFirst haystack needle is
Ok { before, after } ->
Ok "\(before)\(flower)\(after)"
Err err -> Err err
expect Str.replaceFirst "abXdeXghi" "X" "_" == Ok "ab_deXghi"
## Returns the string with the last occurrence of a substring replaced with a replacement.
## If the substring is not found, returns `Err NotFound`.
##
## Str.replaceLast "foo/bar/baz" "/" "_" == Ok "foo/bar_baz"
replaceLast : Str, Str, Str -> Result Str [NotFound]*
replaceLast = \haystack, needle, flower ->
when splitLast haystack needle is
Ok { before, after } ->
Ok "\(before)\(flower)\(after)"
Err err -> Err err
expect Str.replaceLast "abXdeXghi" "X" "_" == Ok "abXde_ghi"
## Returns the string before the first occurrence of a delimiter, as well as the
## rest of the string after that occurrence. If the delimiter is not found, returns `Err`.
##

View file

@ -1243,6 +1243,9 @@ define_builtins! {
47 STR_TO_NUM: "strToNum"
48 STR_FROM_UTF8_RANGE_LOWLEVEL: "fromUtf8RangeLowlevel"
49 STR_CAPACITY: "capacity"
50 STR_REPLACE_EACH: "replaceEach"
51 STR_REPLACE_FIRST: "replaceFirst"
52 STR_REPLACE_LAST: "replaceLast"
}
6 LIST: "List" => {
0 LIST_LIST: "List" imported // the List.List type alias

View file

@ -29,8 +29,8 @@ pub fn generate_docs_html(filenames: Vec<PathBuf>) {
// TODO: get info from a package module; this is all hardcoded for now.
let mut package = roc_load::docs::Documentation {
name: "roc/builtins".to_string(),
version: "1.0.0".to_string(),
name: "documentation".to_string(),
version: "".to_string(),
docs: "Package introduction or README.".to_string(),
modules: loaded_modules,
};
@ -239,7 +239,7 @@ fn render_module_documentation(
}
}
type_annotation_to_html(0, &mut content, type_ann);
type_annotation_to_html(0, &mut content, type_ann, false);
buf.push_str(
html_to_string(
@ -477,60 +477,65 @@ fn new_line(buf: &mut String) {
}
// html is written to buf
fn type_annotation_to_html(indent_level: usize, buf: &mut String, type_ann: &TypeAnnotation) {
fn type_annotation_to_html(
indent_level: usize,
buf: &mut String,
type_ann: &TypeAnnotation,
needs_parens: bool,
) {
let is_multiline = should_be_multiline(type_ann);
match type_ann {
TypeAnnotation::TagUnion { tags, extension } => {
let tags_len = tags.len();
if tags.is_empty() {
buf.push_str("[]");
} else {
let tags_len = tags.len();
let tag_union_indent = indent_level + 1;
if is_multiline {
new_line(buf);
indent(buf, tag_union_indent);
}
buf.push('[');
if is_multiline {
new_line(buf);
}
let next_indent_level = tag_union_indent + 1;
for (index, tag) in tags.iter().enumerate() {
if is_multiline {
indent(buf, next_indent_level);
} else {
buf.push(' ');
}
buf.push_str(tag.name.as_str());
for type_value in &tag.values {
buf.push(' ');
type_annotation_to_html(next_indent_level, buf, type_value);
}
let tag_union_indent = indent_level + 1;
if is_multiline {
if index < (tags_len - 1) {
buf.push(',');
}
new_line(buf);
indent(buf, tag_union_indent);
}
buf.push('[');
if is_multiline {
new_line(buf);
}
let next_indent_level = tag_union_indent + 1;
for (index, tag) in tags.iter().enumerate() {
if is_multiline {
indent(buf, next_indent_level);
}
buf.push_str(tag.name.as_str());
for type_value in &tag.values {
buf.push(' ');
type_annotation_to_html(next_indent_level, buf, type_value, true);
}
if is_multiline {
if index < (tags_len - 1) {
buf.push(',');
}
new_line(buf);
}
}
if is_multiline {
indent(buf, tag_union_indent);
}
buf.push(']');
}
if is_multiline {
indent(buf, tag_union_indent);
} else {
buf.push(' ');
}
buf.push(']');
type_annotation_to_html(indent_level, buf, extension);
type_annotation_to_html(indent_level, buf, extension, true);
}
TypeAnnotation::BoundVariable(var_name) => {
buf.push_str(var_name);
@ -539,82 +544,91 @@ fn type_annotation_to_html(indent_level: usize, buf: &mut String, type_ann: &Typ
if parts.is_empty() {
buf.push_str(name);
} else {
buf.push('(');
if needs_parens {
buf.push('(');
}
buf.push_str(name);
for part in parts {
buf.push(' ');
type_annotation_to_html(indent_level, buf, part);
type_annotation_to_html(indent_level, buf, part, true);
}
if needs_parens {
buf.push(')');
}
buf.push(')');
}
}
TypeAnnotation::Record { fields, extension } => {
let fields_len = fields.len();
if fields.is_empty() {
buf.push_str("{}");
} else {
let fields_len = fields.len();
let record_indent = indent_level + 1;
let record_indent = indent_level + 1;
if is_multiline {
new_line(buf);
indent(buf, record_indent);
}
buf.push('{');
if is_multiline {
new_line(buf);
}
let next_indent_level = record_indent + 1;
for (index, field) in fields.iter().enumerate() {
if is_multiline {
indent(buf, next_indent_level);
new_line(buf);
indent(buf, record_indent);
}
buf.push('{');
if is_multiline {
new_line(buf);
}
let next_indent_level = record_indent + 1;
for (index, field) in fields.iter().enumerate() {
if is_multiline {
indent(buf, next_indent_level);
} else {
buf.push(' ');
}
let fields_name = match field {
RecordField::RecordField { name, .. } => name,
RecordField::OptionalField { name, .. } => name,
RecordField::LabelOnly { name } => name,
};
buf.push_str(fields_name.as_str());
match field {
RecordField::RecordField {
type_annotation, ..
} => {
buf.push_str(" : ");
type_annotation_to_html(next_indent_level, buf, type_annotation, false);
}
RecordField::OptionalField {
type_annotation, ..
} => {
buf.push_str(" ? ");
type_annotation_to_html(next_indent_level, buf, type_annotation, false);
}
RecordField::LabelOnly { .. } => {}
}
if is_multiline {
if index < (fields_len - 1) {
buf.push(',');
}
new_line(buf);
}
}
if is_multiline {
indent(buf, record_indent);
} else {
buf.push(' ');
}
let fields_name = match field {
RecordField::RecordField { name, .. } => name,
RecordField::OptionalField { name, .. } => name,
RecordField::LabelOnly { name } => name,
};
buf.push_str(fields_name.as_str());
match field {
RecordField::RecordField {
type_annotation, ..
} => {
buf.push_str(" : ");
type_annotation_to_html(next_indent_level, buf, type_annotation);
}
RecordField::OptionalField {
type_annotation, ..
} => {
buf.push_str(" ? ");
type_annotation_to_html(next_indent_level, buf, type_annotation);
}
RecordField::LabelOnly { .. } => {}
}
if is_multiline {
if index < (fields_len - 1) {
buf.push(',');
}
new_line(buf);
}
buf.push('}');
}
if is_multiline {
indent(buf, record_indent);
} else {
buf.push(' ');
}
buf.push('}');
type_annotation_to_html(indent_level, buf, extension);
type_annotation_to_html(indent_level, buf, extension, true);
}
TypeAnnotation::Function { args, output } => {
let mut peekable_args = args.iter().peekable();
@ -626,7 +640,7 @@ fn type_annotation_to_html(indent_level: usize, buf: &mut String, type_ann: &Typ
indent(buf, indent_level + 1);
}
type_annotation_to_html(indent_level, buf, arg);
type_annotation_to_html(indent_level, buf, arg, false);
if peekable_args.peek().is_some() {
buf.push_str(", ");
@ -646,7 +660,7 @@ fn type_annotation_to_html(indent_level: usize, buf: &mut String, type_ann: &Typ
next_indent_level += 1;
}
type_annotation_to_html(next_indent_level, buf, output);
type_annotation_to_html(next_indent_level, buf, output, false);
}
TypeAnnotation::Ability { members: _ } => {
// TODO(abilities): fill me in

View file

@ -18,7 +18,7 @@ bench = false
[dependencies]
roc_docs = { path = "../docs" }
clap = { version = "3.2.18", default-features = false, features = ["std", "color", "suggestions", "derive"] }
clap = { version = "3.2.20", default-features = false, features = ["std", "color", "suggestions", "derive"] }
[target.'cfg(windows)'.dependencies]
libc = "0.2.132"

View file

@ -47,14 +47,14 @@ futures = "0.3.24"
cgmath = "0.18.0"
snafu = { version = "0.7.1", features = ["backtraces"] }
colored = "2.0.0"
pest = "2.1.3"
pest = "2.3.1"
pest_derive = "2.1.0"
copypasta = "0.8.1"
palette = "0.6.1"
confy = { git = 'https://github.com/rust-cli/confy', features = [
"yaml_conf"
], default-features = false }
serde = { version = "1.0.130", features = ["derive"] }
serde = { version = "1.0.144", features = ["derive"] }
nonempty = "0.8.0"
fs_extra = "1.2.0"
rodio = { version = "0.15.0", optional = true } # to play sounds

View file

@ -18,9 +18,10 @@ roc_module = { path = "../compiler/module" }
roc_collections = { path = "../compiler/collections" }
roc_target = { path = "../compiler/roc_target" }
roc_error_macros = { path = "../error_macros" }
roc_tracing = { path = "../tracing" }
bumpalo = { version = "3.8.0", features = ["collections"] }
target-lexicon = "0.12.3"
clap = { version = "3.2.18", default-features = false, features = ["std", "color", "suggestions", "derive"] }
clap = { version = "3.2.20", default-features = false, features = ["std", "color", "suggestions", "derive"] }
strum = "0.24.0"
strum_macros = "0.24"
indexmap = "1.8.1"

View file

@ -65,24 +65,45 @@ impl Types {
}
pub fn is_equivalent(&self, a: &RocType, b: &RocType) -> bool {
self.is_equivalent_help(RocTypeOrPending::Type(a), RocTypeOrPending::Type(b))
}
fn is_equivalent_help(&self, a: RocTypeOrPending, b: RocTypeOrPending) -> bool {
use RocType::*;
let (a, b) = match (a, b) {
(RocTypeOrPending::Type(a), RocTypeOrPending::Type(b)) => (a, b),
(RocTypeOrPending::Pending, RocTypeOrPending::Pending) => return true,
_ => return false,
};
match (a, b) {
(RocStr, RocStr) | (Bool, Bool) | (EmptyTagUnion, EmptyTagUnion) | (Unit, Unit) => true,
(RocResult(ok_a, err_a), RocResult(ok_b, err_b)) => {
self.is_equivalent(self.get_type(*ok_a), self.get_type(*ok_b))
&& self.is_equivalent(self.get_type(*err_a), self.get_type(*err_b))
self.is_equivalent_help(
self.get_type_or_pending(*ok_a),
self.get_type_or_pending(*ok_b),
) && self.is_equivalent_help(
self.get_type_or_pending(*err_a),
self.get_type_or_pending(*err_b),
)
}
(Num(num_a), Num(num_b)) => num_a == num_b,
(RocList(elem_a), RocList(elem_b))
| (RocSet(elem_a), RocSet(elem_b))
| (RocBox(elem_a), RocBox(elem_b))
| (RecursivePointer(elem_a), RecursivePointer(elem_b)) => {
self.is_equivalent(self.get_type(*elem_a), self.get_type(*elem_b))
}
| (RecursivePointer(elem_a), RecursivePointer(elem_b)) => self.is_equivalent_help(
self.get_type_or_pending(*elem_a),
self.get_type_or_pending(*elem_b),
),
(RocDict(key_a, val_a), RocDict(key_b, val_b)) => {
self.is_equivalent(self.get_type(*key_a), self.get_type(*key_b))
&& self.is_equivalent(self.get_type(*val_a), self.get_type(*val_b))
self.is_equivalent_help(
self.get_type_or_pending(*key_a),
self.get_type_or_pending(*key_b),
) && self.is_equivalent_help(
self.get_type_or_pending(*val_a),
self.get_type_or_pending(*val_b),
)
}
(TagUnion(union_a), TagUnion(union_b)) => {
use RocTagUnion::*;
@ -113,8 +134,10 @@ impl Types {
},
) => {
tag_name_a == tag_name_b
&& self
.is_equivalent(self.get_type(*payload_a), self.get_type(*payload_b))
&& self.is_equivalent_help(
self.get_type_or_pending(*payload_a),
self.get_type_or_pending(*payload_b),
)
}
(Enumeration { tags: tags_a, .. }, Enumeration { tags: tags_b, .. }) => {
tags_a == tags_b
@ -157,9 +180,9 @@ impl Types {
|((name_a, opt_id_a), (name_b, opt_id_b))| {
name_a == name_b
&& match (opt_id_a, opt_id_b) {
(Some(id_a), Some(id_b)) => self.is_equivalent(
self.get_type(*id_a),
self.get_type(*id_b),
(Some(id_a), Some(id_b)) => self.is_equivalent_help(
self.get_type_or_pending(*id_a),
self.get_type_or_pending(*id_b),
),
(None, None) => true,
(None, Some(_)) | (Some(_), None) => false,
@ -179,9 +202,9 @@ impl Types {
|((name_a, opt_id_a), (name_b, opt_id_b))| {
name_a == name_b
&& match (opt_id_a, opt_id_b) {
(Some(id_a), Some(id_b)) => self.is_equivalent(
self.get_type(*id_a),
self.get_type(*id_b),
(Some(id_a), Some(id_b)) => self.is_equivalent_help(
self.get_type_or_pending(*id_a),
self.get_type_or_pending(*id_b),
),
(None, None) => true,
(None, Some(_)) | (Some(_), None) => false,
@ -241,7 +264,10 @@ impl Types {
.zip(fields_b.iter())
.all(|((name_a, id_a), (name_b, id_b))| {
name_a == name_b
&& self.is_equivalent(self.get_type(*id_a), self.get_type(*id_b))
&& self.is_equivalent_help(
self.get_type_or_pending(*id_a),
self.get_type_or_pending(*id_b),
)
})
} else {
false
@ -261,7 +287,10 @@ impl Types {
.zip(fields_b.iter())
.all(|((name_a, id_a), (name_b, id_b))| {
name_a == name_b
&& self.is_equivalent(self.get_type(*id_a), self.get_type(*id_b))
&& self.is_equivalent_help(
self.get_type_or_pending(*id_a),
self.get_type_or_pending(*id_b),
)
})
} else {
false
@ -283,10 +312,16 @@ impl Types {
// with the same type could have completely different implementations!
if name_a == name_b
&& args_a.len() == args_b.len()
&& self.is_equivalent(self.get_type(*ret_a), self.get_type(*ret_b))
&& self.is_equivalent_help(
self.get_type_or_pending(*ret_a),
self.get_type_or_pending(*ret_b),
)
{
args_a.iter().zip(args_b.iter()).all(|(id_a, id_b)| {
self.is_equivalent(self.get_type(*id_a), self.get_type(*id_b))
self.is_equivalent_help(
self.get_type_or_pending(*id_a),
self.get_type_or_pending(*id_b),
)
})
} else {
false
@ -359,6 +394,12 @@ impl Types {
typ: RocType,
layout: Layout<'a>,
) -> TypeId {
for (id, existing_type) in self.types.iter().enumerate() {
if self.is_equivalent(&typ, existing_type) {
return TypeId(id);
}
}
let id = TypeId(self.types.len());
assert!(id.0 <= TypeId::MAX.0);
@ -379,7 +420,15 @@ impl Types {
pub fn get_type(&self, id: TypeId) -> &RocType {
match self.types.get(id.0) {
Some(typ) => typ,
None => unreachable!(),
None => unreachable!("{:?}", id),
}
}
fn get_type_or_pending(&self, id: TypeId) -> RocTypeOrPending {
match self.types.get(id.0) {
Some(typ) => RocTypeOrPending::Type(typ),
None if id == TypeId::PENDING => RocTypeOrPending::Pending,
None => unreachable!("{:?}", id),
}
}
@ -442,6 +491,12 @@ impl Types {
}
}
enum RocTypeOrPending<'a> {
Type(&'a RocType),
/// A pending recursive pointer
Pending,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum RocType {
RocStr,
@ -653,6 +708,8 @@ impl<'a> Env<'a> {
}
fn add_type(&mut self, var: Variable, types: &mut Types) -> TypeId {
roc_tracing::debug!(content=?roc_types::subs::SubsFmtContent(self.subs.get_content_without_compacting(var), self.subs), "adding type");
let layout = self
.layout_cache
.from_var(self.arena, var, self.subs)

View file

@ -0,0 +1,5 @@
interface Dep1 exposes [DepStr1, string] imports []
DepStr1 := [ S Str ]
string = \s -> @DepStr1 (S s)

View file

@ -0,0 +1,5 @@
interface Dep2 exposes [DepStr2, string] imports []
DepStr2 := [ R Str ]
string = \s -> @DepStr2 (R s)

View file

@ -0,0 +1,6 @@
app "app"
packages { pf: "platform.roc" }
imports [pf.Dep1, pf.Dep2]
provides [main] to pf
main = {s1: Dep1.string "hello", s2: Dep2.string "world"}

View file

@ -0,0 +1,11 @@
platform "test-platform"
requires {} { main : _ }
exposes []
packages {}
imports [Dep1, Dep2]
provides [mainForHost]
Combined : {s1: Dep1.DepStr1, s2: Dep2.DepStr2}
mainForHost : Combined
mainForHost = main

View file

@ -0,0 +1,99 @@
mod test_glue;
use indoc::indoc;
use test_glue::Combined;
extern "C" {
#[link_name = "roc__mainForHost_1_exposed_generic"]
fn roc_main(_: *mut Combined);
}
#[no_mangle]
pub extern "C" fn rust_main() -> i32 {
use std::cmp::Ordering;
use std::collections::hash_set::HashSet;
let tag_union = unsafe {
let mut ret: core::mem::MaybeUninit<Combined> = core::mem::MaybeUninit::uninit();
roc_main(ret.as_mut_ptr());
ret.assume_init()
};
// Verify that it has all the expected traits.
assert!(tag_union == tag_union); // PartialEq
assert!(tag_union.clone() == tag_union.clone()); // Clone
assert!(tag_union.partial_cmp(&tag_union) == Some(Ordering::Equal)); // PartialOrd
assert!(tag_union.cmp(&tag_union) == Ordering::Equal); // Ord
print!(
indoc!(
r#"
combined was: {:?}
"#
),
tag_union,
); // Debug
let mut set = HashSet::new();
set.insert(tag_union.clone()); // Eq, Hash
set.insert(tag_union);
assert_eq!(set.len(), 1);
// Exit code
0
}
// Externs required by roc_std and by the Roc app
use core::ffi::c_void;
use std::ffi::CStr;
use std::os::raw::c_char;
#[no_mangle]
pub unsafe extern "C" fn roc_alloc(size: usize, _alignment: u32) -> *mut c_void {
return libc::malloc(size);
}
#[no_mangle]
pub unsafe extern "C" fn roc_realloc(
c_ptr: *mut c_void,
new_size: usize,
_old_size: usize,
_alignment: u32,
) -> *mut c_void {
return libc::realloc(c_ptr, new_size);
}
#[no_mangle]
pub unsafe extern "C" fn roc_dealloc(c_ptr: *mut c_void, _alignment: u32) {
return libc::free(c_ptr);
}
#[no_mangle]
pub unsafe extern "C" fn roc_panic(c_ptr: *mut c_void, tag_id: u32) {
match tag_id {
0 => {
let slice = CStr::from_ptr(c_ptr as *const c_char);
let string = slice.to_str().unwrap();
eprintln!("Roc hit a panic: {}", string);
std::process::exit(1);
}
_ => todo!(),
}
}
#[no_mangle]
pub unsafe extern "C" fn roc_memcpy(dst: *mut c_void, src: *mut c_void, n: usize) -> *mut c_void {
libc::memcpy(dst, src, n)
}
#[no_mangle]
pub unsafe extern "C" fn roc_memset(dst: *mut c_void, c: i32, n: usize) -> *mut c_void {
libc::memset(dst, c, n)
}

View file

@ -124,6 +124,9 @@ mod glue_cli_run {
list_recursive_union:"list-recursive-union" => indoc!(r#"
rbt was: Rbt { default: Job::Job(R1 { command: Command::Command(R2 { args: [], tool: Tool::SystemTool("test") }), inputFiles: ["foo"], job: [] }) }
"#),
multiple_modules:"multiple-modules" => indoc!(r#"
combined was: Combined { s1: DepStr1::S("hello"), s2: DepStr2::R("world") }
"#),
}
fn check_for_tests(all_fixtures: &mut roc_collections::VecSet<String>) {

View file

@ -17,12 +17,12 @@ roc_build = { path = "../compiler/build" }
roc_collections = { path = "../compiler/collections" }
roc_error_macros = { path = "../error_macros" }
bumpalo = { version = "3.8.0", features = ["collections"] }
clap = { version = "3.2.18", default-features = false, features = ["std", "color", "suggestions"] }
clap = { version = "3.2.20", default-features = false, features = ["std", "color", "suggestions"] }
iced-x86 = { version = "1.15.0", default-features = false, features = ["std", "decoder", "op_code_info", "instr_info"] }
memmap2 = "0.5.7"
object = { version = "0.29.0", features = ["read", "write"] }
mach_object = "0.1"
serde = { version = "1.0.130", features = ["derive"] }
serde = { version = "1.0.144", features = ["derive"] }
bincode = "1.3.3"
target-lexicon = "0.12.3"
tempfile = "3.2.0"

View file

@ -5,3 +5,4 @@ effects
form
tui
http-get
file-io

View file

@ -0,0 +1,25 @@
interface Dir
exposes [ReadErr, DeleteErr, DirEntry, deleteEmptyDir, deleteRecursive, list]
imports [Effect, Task.{ Task }, InternalTask, Path.{ Path }, InternalPath, InternalDir]
ReadErr : InternalDir.ReadErr
DeleteErr : InternalDir.DeleteErr
DirEntry : InternalDir.DirEntry
## Lists the files and directories inside the directory.
list : Path -> Task (List Path) [DirReadErr Path ReadErr]* [Read [File]*]*
list = \path ->
effect = Effect.map (Effect.dirList (InternalPath.toBytes path)) \result ->
when result is
Ok entries -> Ok (List.map entries InternalPath.fromOsBytes)
Err err -> Err (DirReadErr path err)
InternalTask.fromEffect effect
## Deletes a directory if it's empty.
deleteEmptyDir : Path -> Task {} [DirDeleteErr Path DeleteErr]* [Write [File]*]*
## Recursively deletes the directory as well as all files and directories inside it.
deleteRecursive : Path -> Task {} [DirDeleteErr Path DeleteErr]* [Write [File]*]*

View file

@ -6,15 +6,18 @@ hosted Effect
always,
forever,
loop,
dirList,
cwd,
stdoutLine,
stderrLine,
stdinLine,
sendRequest,
fileReadBytes,
fileDelete,
fileWriteUtf8,
fileWriteBytes,
]
imports [InternalHttp.{ Request, Response }, InternalFile]
imports [InternalHttp.{ Request, Response }, InternalFile, InternalDir]
generates Effect with [after, map, always, forever, loop]
stdoutLine : Str -> Effect {}
@ -23,6 +26,10 @@ stdinLine : Effect Str
fileWriteBytes : List U8, List U8 -> Effect (Result {} InternalFile.WriteErr)
fileWriteUtf8 : List U8, Str -> Effect (Result {} InternalFile.WriteErr)
fileDelete : List U8 -> Effect (Result {} InternalFile.WriteErr)
fileReadBytes : List U8 -> Effect (Result (List U8) InternalFile.ReadErr)
dirList : List U8 -> Effect (Result (List (List U8)) InternalDir.ReadErr)
cwd : Effect (List U8)
sendRequest : Box Request -> Effect Response

View file

@ -0,0 +1,15 @@
interface Env
exposes [cwd]
imports [Task.{ Task }, Path.{ Path }, InternalPath, Effect, InternalTask]
## Reads the [current working directory](https://en.wikipedia.org/wiki/Working_directory)
## from the environment.
cwd : Task Path [CwdUnavailable]* [Env]*
cwd =
effect = Effect.map Effect.cwd \bytes ->
if List.isEmpty bytes then
Err CwdUnavailable
else
Ok (InternalPath.fromArbitraryBytes bytes)
InternalTask.fromEffect effect

View file

@ -1,11 +1,13 @@
interface File
exposes [ReadErr, WriteErr, write, writeUtf8, writeBytes, readUtf8, readBytes]
imports [Effect, Task.{ Task }, InternalTask, InternalFile, Path.{ Path }, InternalPath]
exposes [ReadErr, WriteErr, write, writeUtf8, writeBytes, readUtf8, readBytes, delete]
imports [Task.{ Task }, InternalTask, InternalFile, Path.{ Path }, InternalPath, Effect.{ Effect }]
ReadErr : InternalFile.ReadErr
WriteErr : InternalFile.WriteErr
## Encodes a value using the given `EncodingFormat` and writes it to a file.
##
## For example, suppose you have a [JSON](https://en.wikipedia.org/wiki/JSON)
## `EncodingFormat` named `Json.toCompactUtf8`. Then you can use that format
## to write some encodable data to a file as JSON, like so:
@ -31,7 +33,7 @@ write = \path, val, fmt ->
# TODO handle encoding errors here, once they exist
writeBytes path bytes
## Write bytes to a file.
## Writes bytes to a file.
##
## # Writes the bytes 1, 2, 3 to the file `myfile.dat`.
## File.writeBytes (Path.fromStr "myfile.dat") [1, 2, 3]
@ -41,12 +43,9 @@ write = \path, val, fmt ->
## To format data before writing it to a file, you can use [File.write] instead.
writeBytes : Path, List U8 -> Task {} [FileWriteErr Path WriteErr]* [Write [File]*]*
writeBytes = \path, bytes ->
InternalPath.toBytes path
|> Effect.fileWriteBytes bytes
|> InternalTask.fromEffect
|> Task.mapFail \err -> FileWriteErr path err
toWriteTask path \pathBytes -> Effect.fileWriteBytes pathBytes bytes
## Write a [Str] to a file, encoded as [UTF-8](https://en.wikipedia.org/wiki/UTF-8).
## Writes a [Str] to a file, encoded as [UTF-8](https://en.wikipedia.org/wiki/UTF-8).
##
## # Writes "Hello!" encoded as UTF-8 to the file `myfile.txt`.
## File.writeUtf8 (Path.fromStr "myfile.txt") "Hello!"
@ -56,12 +55,29 @@ writeBytes = \path, bytes ->
## To write unformatted bytes to a file, you can use [File.writeBytes] instead.
writeUtf8 : Path, Str -> Task {} [FileWriteErr Path WriteErr]* [Write [File]*]*
writeUtf8 = \path, str ->
InternalPath.toBytes path
|> Effect.fileWriteUtf8 str
|> InternalTask.fromEffect
|> Task.mapFail \err -> FileWriteErr path err
toWriteTask path \bytes -> Effect.fileWriteUtf8 bytes str
## Read all the bytes in a file.
## Deletes a file from the filesystem.
##
## # Deletes the file named
## File.delete (Path.fromStr "myfile.dat") [1, 2, 3]
##
## Note that this does not securely erase the file's contents from disk; instead, the operating
## system marks the space it was occupying as safe to write over in the future. Also, the operating
## system may not immediately mark the space as free; for example, on Windows it will wait until
## the last file handle to it is closed, and on UNIX, it will not remove it until the last
## [hard link](https://en.wikipedia.org/wiki/Hard_link) to it has been deleted.
##
## This performs a [`DeleteFile`](https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-deletefile)
## on Windows and [`unlink`](https://en.wikipedia.org/wiki/Unlink_(Unix)) on UNIX systems.
##
## On Windows, this will fail when attempting to delete a readonly file; the file's
## readonly permission must be disabled before it can be successfully deleted.
delete : Path -> Task {} [FileWriteErr Path WriteErr]* [Write [File]*]*
delete = \path ->
toWriteTask path \bytes -> Effect.fileDelete bytes
## Reads all the bytes in a file.
##
## # Read all the bytes in `myfile.txt`.
## File.readBytes (Path.fromStr "myfile.txt")
@ -71,12 +87,9 @@ writeUtf8 = \path, str ->
## To read and decode data from a file, you can use `File.read` instead.
readBytes : Path -> Task (List U8) [FileReadErr Path ReadErr]* [Read [File]*]*
readBytes = \path ->
InternalPath.toBytes path
|> Effect.fileReadBytes
|> InternalTask.fromEffect
|> Task.mapFail \err -> FileReadErr path err
toReadTask path \bytes -> Effect.fileReadBytes bytes
## Read a [Str] from a file containing [UTF-8](https://en.wikipedia.org/wiki/UTF-8)-encoded text.
## Reads a [Str] from a file containing [UTF-8](https://en.wikipedia.org/wiki/UTF-8)-encoded text.
##
## # Reads UTF-8 encoded text into a `Str` from the file `myfile.txt`.
## File.readUtf8 (Path.fromStr "myfile.txt")
@ -119,3 +132,16 @@ readUtf8 = \path ->
# Err decodingErr -> Err (FileReadDecodeErr decodingErr)
# Err readErr -> Err (FileReadErr readErr)
# InternalTask.fromEffect effect
toWriteTask : Path, (List U8 -> Effect (Result ok err)) -> Task ok [FileWriteErr Path err]* [Write [File]*]*
toWriteTask = \path, toEffect ->
InternalPath.toBytes path
|> toEffect
|> InternalTask.fromEffect
|> Task.mapFail \err -> FileWriteErr path err
toReadTask : Path, (List U8 -> Effect (Result ok err)) -> Task ok [FileReadErr Path err]* [Read [File]*]*
toReadTask = \path, toEffect ->
InternalPath.toBytes path
|> toEffect
|> InternalTask.fromEffect
|> Task.mapFail \err -> FileReadErr path err

View file

@ -0,0 +1,32 @@
interface FileMetadata
exposes [FileMetadata, bytes, type, isReadonly, mode]
imports []
# Design note: this is an opaque type rather than a type alias so that
# we can add new operating system info if new OS releases introduce them,
# as a backwards-compatible change.
FileMetadata := {
bytes : U64,
type : [File, Dir, Symlink],
isReadonly : Bool,
mode : [Unix U32, NonUnix],
}
bytes : FileMetadata -> U64
bytes = \@FileMetadata info -> info.bytes
isReadonly : FileMetadata -> Bool
isReadonly = \@FileMetadata info -> info.isReadonly
type : FileMetadata -> [File, Dir, Symlink]
type = \@FileMetadata info -> info.type
mode : FileMetadata -> [Unix U32, NonUnix]
mode = \@FileMetadata info -> info.mode
# TODO need to create a Time module and return something like Time.Utc here.
# lastModified : FileMetadata -> Utc
# TODO need to create a Time module and return something like Time.Utc here.
# lastAccessed : FileMetadata -> Utc
# TODO need to create a Time module and return something like Time.Utc here.
# created : FileMetadata -> Utc

View file

@ -0,0 +1,41 @@
interface InternalDir
exposes [ReadErr, DeleteErr, DirEntry]
imports [FileMetadata.{ FileMetadata }, Path.{ Path }]
DirEntry : {
path : Path,
type : [File, Dir, Symlink],
metadata : FileMetadata,
}
ReadErr : [
NotFound,
Interrupted,
InvalidFilename,
PermissionDenied,
TooManySymlinks, # aka FilesystemLoop
TooManyHardlinks,
TimedOut,
StaleNetworkFileHandle,
NotADirectory,
OutOfMemory,
Unsupported,
Unrecognized I32 Str,
]
DeleteErr : [
NotFound,
Interrupted,
InvalidFilename,
PermissionDenied,
TooManySymlinks, # aka FilesystemLoop
TooManyHardlinks,
TimedOut,
StaleNetworkFileHandle,
NotADirectory,
ReadOnlyFilesystem,
DirectoryNotEmpty,
OutOfMemory,
Unsupported,
Unrecognized I32 Str,
]

View file

@ -5,6 +5,8 @@ interface InternalPath
wrap,
unwrap,
toBytes,
fromArbitraryBytes,
fromOsBytes,
]
imports []
@ -61,3 +63,11 @@ toBytes = \@InternalPath path ->
FromOperatingSystem bytes -> bytes
ArbitraryBytes bytes -> bytes
FromStr str -> Str.toUtf8 str
fromArbitraryBytes : List U8 -> InternalPath
fromArbitraryBytes = \bytes ->
@InternalPath (ArbitraryBytes bytes)
fromOsBytes : List U8 -> InternalPath
fromOsBytes = \bytes ->
@InternalPath (FromOperatingSystem bytes)

View file

@ -6,6 +6,7 @@ interface Path
WindowsRoot,
# toComponents,
# walkComponents,
display,
fromStr,
fromBytes,
withExtension,
@ -82,13 +83,13 @@ fromBytes = \bytes ->
## have been encoded with the same charset as the operating system's curent locale (which
## typically does not change after it is set during installation of the OS), so
## this should convert a [Path] to a valid string as long as the path was created
## with the given [Charset]. (Use [Env.charset] to get the current system charset.)
## with the given `Charset`. (Use `Env.charset` to get the current system charset.)
##
## For a conversion to [Str] that is lossy but does not return a [Result], see
## [displayUtf8].
## [display].
# toInner : Path -> [Str Str, Bytes (List U8)]
## Assumes a path is encoded as [UTF-8](https://en.wikipedia.org/wiki/UTF-8),
## and converts it to a string using [Str.displayUtf8].
## and converts it to a string using `Str.display`.
##
## This conversion is lossy because the path may contain invalid UTF-8 bytes. If that happens,
## any invalid bytes will be replaced with the [Unicode replacement character](https://unicode.org/glossary/#replacement_character)
@ -103,17 +104,21 @@ fromBytes = \bytes ->
## Converting paths to strings can be an unreliable operation, because operating systems
## don't record the paths' encodings. This means it's possible for the path to have been
## encoded with a different character set than UTF-8 even if UTF-8 is the system default,
## which means when [displayUtf8] converts them to a string, the string may include gibberish.
## which means when [display] converts them to a string, the string may include gibberish.
## [Here is an example.](https://unix.stackexchange.com/questions/667652/can-a-file-path-be-invalid-utf-8/667863#667863)
##
## If you happen to know the [Charset] that was used to encode the path, you can use
## [toStrUsingCharset] instead of [displayUtf8].
# displayUtf8 : Path -> Str
# displayUtf8 = \path ->
# when InternalPath.unwrap path is
# FromStr str -> str
# FromOperatingSystem bytes | ArbitraryBytes bytes ->
# Str.displayUtf8 bytes
## If you happen to know the `Charset` that was used to encode the path, you can use
## `toStrUsingCharset` instead of [display].
display : Path -> Str
display = \path ->
when InternalPath.unwrap path is
FromStr str -> str
FromOperatingSystem bytes | ArbitraryBytes bytes ->
when Str.fromUtf8 bytes is
Ok str -> str
# TODO: this should use the builtin Str.display to display invalid UTF-8 chars in just the right spots, but that does not exist yet!
Err _ -> "<22>"
# isEq : Path, Path -> Bool
# isEq = \p1, p2 ->
# when InternalPath.unwrap p1 is

View file

@ -216,11 +216,76 @@ pub fn os_str_from_list(bytes: &RocList<u8>) -> &OsStr {
}
#[no_mangle]
pub extern "C" fn roc_fx_fileReadBytes(path: &RocList<u8>) -> RocResult<RocList<u8>, ReadErr> {
let path = path_from_roc_path(path);
println!("TODO read bytes from {:?}", path);
pub extern "C" fn roc_fx_fileReadBytes(roc_path: &RocList<u8>) -> RocResult<RocList<u8>, ReadErr> {
use std::io::Read;
RocResult::ok(RocList::empty())
let mut bytes = Vec::new();
match File::open(path_from_roc_path(roc_path)) {
Ok(mut file) => match file.read_to_end(&mut bytes) {
Ok(_bytes_read) => RocResult::ok(RocList::from(bytes.as_slice())),
Err(_) => {
todo!("Report a file write error");
}
},
Err(_) => {
todo!("Report a file open error");
}
}
}
#[no_mangle]
pub extern "C" fn roc_fx_fileDelete(roc_path: &RocList<u8>) -> RocResult<(), ReadErr> {
match std::fs::remove_file(path_from_roc_path(roc_path)) {
Ok(()) => RocResult::ok(()),
Err(_) => {
todo!("Report a file write error");
}
}
}
#[no_mangle]
pub extern "C" fn roc_fx_cwd() -> RocList<u8> {
// TODO instead, call getcwd on UNIX and GetCurrentDirectory on Windows
match std::env::current_dir() {
Ok(path_buf) => os_str_to_roc_path(path_buf.into_os_string().as_os_str()),
Err(_) => {
// Default to empty path
RocList::empty()
}
}
}
#[no_mangle]
pub extern "C" fn roc_fx_dirList(
// TODO: this RocResult should use Dir.WriteErr - but right now it's File.WriteErr
// because glue doesn't have Dir.WriteErr yet.
roc_path: &RocList<u8>,
) -> RocResult<RocList<RocList<u8>>, WriteErr> {
println!("Dir.list...");
match std::fs::read_dir(path_from_roc_path(roc_path)) {
Ok(dir_entries) => RocResult::ok(
dir_entries
.map(|opt_dir_entry| match opt_dir_entry {
Ok(entry) => os_str_to_roc_path(entry.path().into_os_string().as_os_str()),
Err(_) => {
todo!("handle dir_entry path didn't resolve")
}
})
.collect::<RocList<RocList<u8>>>(),
),
Err(_) => {
todo!("handle Dir.list error");
}
}
}
#[cfg(target_family = "unix")]
/// TODO convert from EncodeWide to RocPath on Windows
fn os_str_to_roc_path(os_str: &OsStr) -> RocList<u8> {
use std::os::unix::ffi::OsStrExt;
RocList::from(os_str.as_bytes())
}
#[no_mangle]

View file

@ -1,17 +1,30 @@
app "echo"
app "file-io"
packages { pf: "cli-platform/main.roc" }
imports [pf.Stdout, pf.Stderr, pf.Task, pf.File, pf.Path]
imports [pf.Stdout, pf.Stderr, pf.Task, pf.File, pf.Path, pf.Env, pf.Dir]
provides [main] to pf
main : Task.Task {} [] [Write [File, Stdout, Stderr]]
main : Task.Task {} [] [Write [File, Stdout, Stderr], Read [File], Env]
main =
path = Path.fromStr "out.txt"
task =
cwd <- Env.cwd |> Task.await
cwdStr = Path.display cwd
_ <- Stdout.line "cwd: \(cwdStr)" |> Task.await
dirEntries <- Dir.list cwd |> Task.await
contentsStr = Str.joinWith (List.map dirEntries Path.display) "\n "
_ <- Stdout.line "Directory contents:\n \(contentsStr)\n" |> Task.await
_ <- Stdout.line "Writing a string to out.txt" |> Task.await
File.writeUtf8 (Path.fromStr "out.txt") "a string!\n"
_ <- File.writeUtf8 path "a string!" |> Task.await
contents <- File.readUtf8 path |> Task.await
Stdout.line "I read the file back. Its contents: \"\(contents)\""
Task.attempt task \result ->
when result is
Err (FileWriteErr _ PermissionDenied) -> Stderr.line "Err: PermissionDenied"
Err (FileWriteErr _ Unsupported) -> Stderr.line "Err: Unsupported"
Err (FileWriteErr _ (Unrecognized _ other)) -> Stderr.line "Err: \(other)"
_ -> Stdout.line "Successfully wrote a string to out.txt"
Err (FileReadErr _ _) -> Stderr.line "Error reading file"
Err _ -> Stderr.line "Uh oh, there was an error!"
Ok _ -> Stdout.line "Successfully wrote a string to out.txt"