Merge branch 'trunk' into update-builtins-readme

This commit is contained in:
Chadtech 2020-07-22 20:44:55 -04:00 committed by GitHub
commit 3e48ddfb59
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 1442 additions and 718 deletions

145
Cargo.lock generated
View file

@ -207,6 +207,35 @@ dependencies = [
"unicode-width", "unicode-width",
] ]
[[package]]
name = "clap"
version = "3.0.0-beta.1"
source = "git+https://github.com/rtfeldman/clap#e1d83a78804a271b053d4d21f69b67f7eb01ad01"
dependencies = [
"atty",
"bitflags",
"clap_derive",
"indexmap",
"lazy_static",
"strsim",
"termcolor",
"textwrap",
"unicode-width",
"vec_map",
]
[[package]]
name = "clap_derive"
version = "3.0.0-beta.1"
source = "git+https://github.com/rtfeldman/clap#e1d83a78804a271b053d4d21f69b67f7eb01ad01"
dependencies = [
"heck",
"proc-macro-error",
"proc-macro2 1.0.10",
"quote 1.0.3",
"syn 1.0.17",
]
[[package]] [[package]]
name = "cloudabi" name = "cloudabi"
version = "0.0.3" version = "0.0.3"
@ -329,7 +358,7 @@ checksum = "1fc755679c12bda8e5523a71e4d654b6bf2e14bd838dfc48cde6559a05caf7d1"
dependencies = [ dependencies = [
"atty", "atty",
"cast", "cast",
"clap", "clap 2.33.0",
"criterion-plot", "criterion-plot",
"csv", "csv",
"itertools", "itertools",
@ -856,6 +885,24 @@ dependencies = [
"xi-unicode", "xi-unicode",
] ]
[[package]]
name = "hashbrown"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34f595585f103464d8d2f6e9864682d74c1601fed5e07d62b1c9058dba8246fb"
dependencies = [
"autocfg 1.0.0",
]
[[package]]
name = "heck"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205"
dependencies = [
"unicode-segmentation",
]
[[package]] [[package]]
name = "hermit-abi" name = "hermit-abi"
version = "0.1.10" version = "0.1.10"
@ -911,6 +958,16 @@ dependencies = [
"version_check", "version_check",
] ]
[[package]]
name = "indexmap"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b88cd59ee5f71fea89a62248fc8f387d44400cefe05ef548466d61ced9029a7"
dependencies = [
"autocfg 1.0.0",
"hashbrown",
]
[[package]] [[package]]
name = "indoc" name = "indoc"
version = "0.3.5" version = "0.3.5"
@ -1434,6 +1491,32 @@ dependencies = [
"difference", "difference",
] ]
[[package]]
name = "proc-macro-error"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18f33027081eba0a6d8aba6d1b1c3a3be58cbb12106341c2d5759fcd9b5277e7"
dependencies = [
"proc-macro-error-attr",
"proc-macro2 1.0.10",
"quote 1.0.3",
"syn 1.0.17",
"version_check",
]
[[package]]
name = "proc-macro-error-attr"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a5b4b77fdb63c1eca72173d68d24501c54ab1269409f6b672c85deb18af69de"
dependencies = [
"proc-macro2 1.0.10",
"quote 1.0.3",
"syn 1.0.17",
"syn-mid",
"version_check",
]
[[package]] [[package]]
name = "proc-macro-hack" name = "proc-macro-hack"
version = "0.5.15" version = "0.5.15"
@ -1757,6 +1840,43 @@ dependencies = [
"winapi 0.3.8", "winapi 0.3.8",
] ]
[[package]]
name = "roc-cli"
version = "0.1.0"
dependencies = [
"bumpalo",
"clap 3.0.0-beta.1",
"im",
"im-rc",
"indoc",
"inkwell",
"inlinable_string",
"maplit",
"pretty_assertions",
"quickcheck",
"quickcheck_macros",
"roc_build",
"roc_builtins",
"roc_can",
"roc_collections",
"roc_constrain",
"roc_editor",
"roc_gen",
"roc_load",
"roc_module",
"roc_mono",
"roc_parse",
"roc_problem",
"roc_region",
"roc_reporting",
"roc_solve",
"roc_types",
"roc_unify",
"roc_uniq",
"target-lexicon",
"tokio",
]
[[package]] [[package]]
name = "roc_build" name = "roc_build"
version = "0.1.0" version = "0.1.0"
@ -2361,6 +2481,12 @@ dependencies = [
"lock_api", "lock_api",
] ]
[[package]]
name = "strsim"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c"
[[package]] [[package]]
name = "syn" name = "syn"
version = "0.15.44" version = "0.15.44"
@ -2383,6 +2509,17 @@ dependencies = [
"unicode-xid 0.2.0", "unicode-xid 0.2.0",
] ]
[[package]]
name = "syn-mid"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7be3539f6c128a931cf19dcee741c1af532c7fd387baa739c03dd2e96479338a"
dependencies = [
"proc-macro2 1.0.10",
"quote 1.0.3",
"syn 1.0.17",
]
[[package]] [[package]]
name = "synstructure" name = "synstructure"
version = "0.12.4" version = "0.12.4"
@ -2499,6 +2636,12 @@ version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33" checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33"
[[package]]
name = "unicode-segmentation"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0"
[[package]] [[package]]
name = "unicode-width" name = "unicode-width"
version = "0.1.7" version = "0.1.7"

View file

@ -23,7 +23,8 @@ members = [
"vendor/ena", "vendor/ena",
"vendor/pathfinding", "vendor/pathfinding",
"vendor/pretty", "vendor/pretty",
"editor" "editor",
"cli"
] ]
# Optimizations based on https://deterministic.space/high-performance-rust.html # Optimizations based on https://deterministic.space/high-performance-rust.html

View file

@ -2,33 +2,17 @@
extern crate clap; extern crate clap;
use bumpalo::Bump; use bumpalo::Bump;
use inkwell::context::Context; use roc_build::program::gen;
use inkwell::module::Linkage;
use inkwell::passes::PassManager;
use inkwell::types::BasicType;
use inkwell::OptimizationLevel;
use roc_collections::all::ImMap;
use roc_collections::all::MutMap; use roc_collections::all::MutMap;
use roc_gen::layout_id::LayoutIds; use roc_gen::llvm::build::OptLevel;
use roc_gen::llvm::build::{ use roc_load::file::LoadingProblem;
build_proc, build_proc_header, get_call_conventions, module_from_builtins, OptLevel,
};
use roc_gen::llvm::convert::basic_type_from_layout;
use roc_build::program::build;
use roc_load::file::{LoadedModule, LoadingProblem};
use roc_module::symbol::Symbol;
use roc_mono::expr::{Env, Expr, PartialProc, Procs};
use roc_mono::layout::Layout;
use std::time::SystemTime; use std::time::SystemTime;
use clap::{App, Arg, ArgMatches}; use clap::{App, Arg, ArgMatches};
use inkwell::targets::{
CodeModel, FileType, InitializationConfig, RelocMode, Target, TargetTriple,
};
use std::io::{self, ErrorKind}; use std::io::{self, ErrorKind};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process; use std::process;
use target_lexicon::{Architecture, Environment, OperatingSystem, Triple, Vendor}; use target_lexicon::Triple;
use tokio::process::Command; use tokio::process::Command;
use tokio::runtime::Builder; use tokio::runtime::Builder;
@ -118,7 +102,7 @@ pub fn build(matches: &ArgMatches, run_after_build: bool) -> io::Result<()> {
} else { } else {
OptLevel::Normal OptLevel::Normal
}; };
let path = Path::new(filename); let path = Path::new(filename).canonicalize().unwrap();
let src_dir = path.parent().unwrap().canonicalize().unwrap(); let src_dir = path.parent().unwrap().canonicalize().unwrap();
// Create the runtime // Create the runtime
@ -247,377 +231,3 @@ async fn build_file(
Ok(binary_path) Ok(binary_path)
} }
// TODO this should probably use more helper functions
#[allow(clippy::cognitive_complexity)]
fn gen(
arena: &Bump,
loaded: LoadedModule,
filename: PathBuf,
target: Triple,
dest_filename: &Path,
opt_level: OptLevel,
) {
use roc_reporting::report::{can_problem, type_problem, RocDocAllocator, DEFAULT_PALETTE};
let src = loaded.src;
let home = loaded.module_id;
let src_lines: Vec<&str> = src.split('\n').collect();
let palette = DEFAULT_PALETTE;
// Report parsing and canonicalization problems
let alloc = RocDocAllocator::new(&src_lines, home, &loaded.interns);
for problem in loaded.can_problems.into_iter() {
let report = can_problem(&alloc, filename.clone(), problem);
let mut buf = String::new();
report.render_color_terminal(&mut buf, &alloc, &palette);
println!("\n{}\n", buf);
}
for problem in loaded.type_problems.into_iter() {
let report = type_problem(&alloc, filename.clone(), problem);
let mut buf = String::new();
report.render_color_terminal(&mut buf, &alloc, &palette);
println!("\n{}\n", buf);
}
// Look up the types and expressions of the `provided` values
// TODO instead of hardcoding this to `main`, use the `provided` list and gen all of them.
let ident_ids = loaded.interns.all_ident_ids.get(&home).unwrap();
let main_ident_id = *ident_ids.get_id(&"main".into()).unwrap_or_else(|| {
todo!("TODO gracefully handle the case where `main` wasn't declared in the app")
});
let main_symbol = Symbol::new(home, main_ident_id);
let mut main_var = None;
let mut main_expr = None;
for (symbol, var) in loaded.exposed_vars_by_symbol {
if symbol == main_symbol {
main_var = Some(var);
break;
}
}
let mut decls_by_id = loaded.declarations_by_id;
let home_decls = decls_by_id
.remove(&loaded.module_id)
.expect("Root module ID not found in loaded declarations_by_id");
// We use a loop label here so we can break all the way out of a nested
// loop inside DeclareRec if we find the expr there.
//
// https://doc.rust-lang.org/1.30.0/book/first-edition/loops.html#loop-labels
'find_expr: for decl in home_decls.iter() {
use roc_can::def::Declaration::*;
match decl {
Declare(def) => {
if def.pattern_vars.contains_key(&main_symbol) {
main_expr = Some(def.loc_expr.clone());
break 'find_expr;
}
}
DeclareRec(defs) => {
for def in defs {
if def.pattern_vars.contains_key(&main_symbol) {
main_expr = Some(def.loc_expr.clone());
break 'find_expr;
}
}
}
InvalidCycle(_, _) => {}
}
}
let loc_expr = main_expr.unwrap_or_else(|| {
panic!("TODO gracefully handle the case where `main` was declared but not exposed")
});
let mut subs = loaded.solved.into_inner();
let content = match main_var {
Some(var) => subs.get_without_compacting(var).content,
None => todo!("TODO gracefully handle the case where `main` was declared but not exposed"),
};
// Generate the binary
let context = Context::create();
let module = module_from_builtins(&context, "app");
let builder = context.create_builder();
let fpm = PassManager::create(&module);
roc_gen::llvm::build::add_passes(&fpm, opt_level);
fpm.initialize();
// Compute main_fn_type before moving subs to Env
let ptr_bytes = target.pointer_width().unwrap().bytes() as u32;
let layout = Layout::new(&arena, content, &subs, ptr_bytes).unwrap_or_else(|err| {
panic!(
"Code gen error in CLI: could not convert to layout. Err was {:?}",
err
)
});
let main_fn_type =
basic_type_from_layout(&arena, &context, &layout, ptr_bytes).fn_type(&[], false);
let main_fn_name = "$main";
// Compile and add all the Procs before adding main
let mut env = roc_gen::llvm::build::Env {
arena: &arena,
builder: &builder,
context: &context,
interns: loaded.interns,
module: arena.alloc(module),
ptr_bytes,
};
let mut ident_ids = env.interns.all_ident_ids.remove(&home).unwrap();
let mut layout_ids = LayoutIds::default();
let mut procs = Procs::default();
let mut mono_problems = std::vec::Vec::new();
let mut mono_env = Env {
arena,
subs: &mut subs,
problems: &mut mono_problems,
home,
ident_ids: &mut ident_ids,
pointer_size: ptr_bytes,
jump_counter: arena.alloc(0),
};
// Add modules' decls to Procs
for (_, mut decls) in decls_by_id
.drain()
.chain(std::iter::once((loaded.module_id, home_decls)))
{
for decl in decls.drain(..) {
use roc_can::def::Declaration::*;
use roc_can::expr::Expr::*;
use roc_can::pattern::Pattern::*;
match decl {
Declare(def) => match def.loc_pattern.value {
Identifier(symbol) => {
match def.loc_expr.value {
Closure(annotation, _, _, loc_args, boxed_body) => {
let (loc_body, ret_var) = *boxed_body;
procs.insert_named(
&mut mono_env,
symbol,
annotation,
loc_args,
loc_body,
ret_var,
);
}
body => {
let proc = PartialProc {
annotation: def.expr_var,
// This is a 0-arity thunk, so it has no arguments.
patterns: bumpalo::collections::Vec::new_in(arena),
body,
};
procs.user_defined.insert(symbol, proc);
procs.module_thunks.insert(symbol);
}
};
}
other => {
todo!("TODO gracefully handle Declare({:?})", other);
}
},
DeclareRec(_defs) => {
todo!("TODO support DeclareRec");
}
InvalidCycle(_loc_idents, _regions) => {
todo!("TODO handle InvalidCycle");
}
}
}
}
// Populate Procs further and get the low-level Expr from the canonical Expr
let main_body = Expr::new(&mut mono_env, loc_expr.value, &mut procs);
// Put this module's ident_ids back in the interns, so we can use them in env.
env.interns.all_ident_ids.insert(home, ident_ids);
let mut headers = Vec::with_capacity(procs.len());
let (mut proc_map, runtime_errors) = procs.into_map();
assert_eq!(runtime_errors, roc_collections::all::MutSet::default());
// Add all the Proc headers to the module.
// We have to do this in a separate pass first,
// because their bodies may reference each other.
for (symbol, mut procs_by_layout) in proc_map.drain() {
for (layout, proc) in procs_by_layout.drain() {
let (fn_val, arg_basic_types) =
build_proc_header(&env, &mut layout_ids, symbol, &layout, &proc);
headers.push((proc, fn_val, arg_basic_types));
}
}
// Build each proc using its header info.
for (proc, fn_val, arg_basic_types) in headers {
// NOTE: This is here to be uncommented in case verification fails.
// (This approach means we don't have to defensively clone name here.)
//
// println!("\n\nBuilding and then verifying function {}\n\n", name);
build_proc(&env, &mut layout_ids, proc, fn_val, arg_basic_types);
if fn_val.verify(true) {
fpm.run_on(&fn_val);
} else {
// NOTE: If this fails, uncomment the above println to debug.
panic!(
"Non-main function failed LLVM verification. Uncomment the above println to debug!"
);
}
}
// Add main to the module.
let cc = get_call_conventions(target.default_calling_convention().unwrap());
let main_fn = env.module.add_function(main_fn_name, main_fn_type, None);
main_fn.set_call_conventions(cc);
main_fn.set_linkage(Linkage::External);
// Add main's body
let basic_block = context.append_basic_block(main_fn, "entry");
builder.position_at_end(basic_block);
let ret = roc_gen::llvm::build::build_expr(
&env,
&mut layout_ids,
&ImMap::default(),
main_fn,
&main_body,
);
builder.build_return(Some(&ret));
// Uncomment this to see the module's un-optimized LLVM instruction output:
// env.module.print_to_stderr();
if main_fn.verify(true) {
fpm.run_on(&main_fn);
} else {
panic!("Function {} failed LLVM verification.", main_fn_name);
}
// Verify the module
if let Err(errors) = env.module.verify() {
panic!("😱 LLVM errors when defining module: {:?}", errors);
}
// Uncomment this to see the module's optimized LLVM instruction output:
// env.module.print_to_stderr();
// Emit the .o file
// NOTE: arch_str is *not* the same as the beginning of the magic target triple
// string! For example, if it's "x86-64" here, the magic target triple string
// will begin with "x86_64" (with an underscore) instead.
let arch_str = match target.architecture {
Architecture::X86_64 => {
Target::initialize_x86(&InitializationConfig::default());
"x86-64"
}
Architecture::Arm(_) if cfg!(feature = "target-arm") => {
// NOTE: why not enable arm and wasm by default?
//
// We had some trouble getting them to link properly. This may be resolved in the
// future, or maybe it was just some weird configuration on one machine.
Target::initialize_arm(&InitializationConfig::default());
"arm"
}
Architecture::Wasm32 if cfg!(feature = "target-webassembly") => {
Target::initialize_webassembly(&InitializationConfig::default());
"wasm32"
}
_ => panic!(
"TODO gracefully handle unsupported target architecture: {:?}",
target.architecture
),
};
let opt = OptimizationLevel::Default;
let reloc = RelocMode::Default;
let model = CodeModel::Default;
// Best guide I've found on how to determine these magic strings:
//
// https://stackoverflow.com/questions/15036909/clang-how-to-list-supported-target-architectures
//
// Clang docs are particularly helpful: http://clang.llvm.org/docs/CrossCompilation.html
let target_triple_str = match target {
Triple {
architecture: Architecture::X86_64,
vendor: Vendor::Unknown,
operating_system: OperatingSystem::Linux,
..
} => "x86_64-unknown-linux-gnu",
Triple {
architecture: Architecture::X86_64,
vendor: Vendor::Pc,
operating_system: OperatingSystem::Linux,
..
} => "x86_64-pc-linux-gnu",
Triple {
architecture: Architecture::X86_64,
vendor: Vendor::Unknown,
operating_system: OperatingSystem::Darwin,
..
} => "x86_64-unknown-darwin10",
Triple {
architecture: Architecture::X86_64,
vendor: Vendor::Apple,
operating_system: OperatingSystem::Darwin,
..
} => "x86_64-apple-darwin10",
Triple {
architecture: Architecture::X86_64,
vendor: Vendor::Pc,
operating_system: OperatingSystem::Windows,
environment: Environment::Msvc,
..
} => "x86_64-pc-win32-gnu",
_ => panic!("TODO gracefully handle unsupported target: {:?}", target),
};
let target_machine = Target::from_name(arch_str)
.unwrap()
.create_target_machine(
&TargetTriple::create(target_triple_str),
arch_str,
"+avx2", // TODO this string was used uncritically from an example, and should be reexamined
opt,
reloc,
model,
)
.unwrap();
target_machine
.write_to_file(&env.module, FileType::Object, &dest_filename)
.expect("Writing .o file failed");
println!("\nSuccess!\n\n\t—> {}\n", dest_filename.display());
}

View file

@ -20,7 +20,7 @@ use roc_gen::llvm::convert::basic_type_from_layout;
use roc_module::ident::Ident; use roc_module::ident::Ident;
use roc_module::symbol::{IdentIds, Interns, ModuleId, ModuleIds, Symbol}; use roc_module::symbol::{IdentIds, Interns, ModuleId, ModuleIds, Symbol};
use roc_mono::expr::Procs; use roc_mono::expr::Procs;
use roc_mono::layout::Layout; use roc_mono::layout::{Layout, LayoutCache};
use roc_parse::ast::{self, Attempting}; use roc_parse::ast::{self, Attempting};
use roc_parse::blankspace::space0_before; use roc_parse::blankspace::space0_before;
use roc_parse::parser::{loc, Fail, FailReason, Parser, State}; use roc_parse::parser::{loc, Fail, FailReason, Parser, State};
@ -257,31 +257,47 @@ pub fn gen(src: &str, target: Triple, opt_level: OptLevel) -> Result<(String, St
pointer_size: ptr_bytes, pointer_size: ptr_bytes,
jump_counter: arena.alloc(0), jump_counter: arena.alloc(0),
}; };
let main_body = roc_mono::expr::Expr::new(&mut mono_env, loc_expr.value, &mut procs); let main_body = roc_mono::expr::Expr::new(&mut mono_env, loc_expr.value, &mut procs);
let mut headers = {
let num_headers = match &procs.pending_specializations {
Some(map) => map.len(),
None => 0,
};
// Put this module's ident_ids back in the interns, so we can use them in Env. Vec::with_capacity(num_headers)
env.interns.all_ident_ids.insert(home, ident_ids); };
let mut layout_cache = LayoutCache::default();
let mut headers = Vec::with_capacity(procs.len()); let mut procs = roc_mono::expr::specialize_all(&mut mono_env, procs, &mut layout_cache);
let (mut proc_map, runtime_errors) = procs.into_map();
assert_eq!( assert_eq!(
runtime_errors, procs.runtime_errors,
roc_collections::all::MutSet::default(), roc_collections::all::MutMap::default()
"TODO code gen runtime exception functions"
); );
// Put this module's ident_ids back in the interns, so we can use them in env.
// This must happen *after* building the headers, because otherwise there's
// a conflicting mutable borrow on ident_ids.
env.interns.all_ident_ids.insert(home, ident_ids);
// Add all the Proc headers to the module. // Add all the Proc headers to the module.
// We have to do this in a separate pass first, // We have to do this in a separate pass first,
// because their bodies may reference each other. // because their bodies may reference each other.
for (symbol, mut procs_by_layout) in proc_map.drain() { for ((symbol, layout), proc) in procs.specialized.drain() {
for (layout, proc) in procs_by_layout.drain() { use roc_mono::expr::InProgressProc::*;
match proc {
InProgress => {
panic!("A specialization was still marked InProgress after monomorphization had completed: {:?} with layout {:?}", symbol, layout);
}
Done(proc) => {
let (fn_val, arg_basic_types) = let (fn_val, arg_basic_types) =
build_proc_header(&env, &mut layout_ids, symbol, &layout, &proc); build_proc_header(&env, &mut layout_ids, symbol, &layout, &proc);
headers.push((proc, fn_val, arg_basic_types)); headers.push((proc, fn_val, arg_basic_types));
} }
} }
}
// Build each proc using its header info. // Build each proc using its header info.
for (proc, fn_val, arg_basic_types) in headers { for (proc, fn_val, arg_basic_types) in headers {

View file

@ -25,7 +25,7 @@ use target_lexicon::{Architecture, OperatingSystem, Triple, Vendor};
// TODO this should probably use more helper functions // TODO this should probably use more helper functions
// TODO make this polymorphic in the llvm functions so it can be reused for another backend. // TODO make this polymorphic in the llvm functions so it can be reused for another backend.
#[allow(clippy::cognitive_complexity)] #[allow(clippy::cognitive_complexity)]
pub fn build( pub fn gen(
arena: &Bump, arena: &Bump,
loaded: LoadedModule, loaded: LoadedModule,
filename: PathBuf, filename: PathBuf,

View file

@ -1,264 +0,0 @@
interface List
exposes [ List, map, fold ]
imports []
## Types
## A sequential list of values.
##
## >>> [ 1, 2, 3 ] # a list of numbers
##
## >>> [ "a", "b", "c" ] # a list of strings
##
## >>> [ [ 1.1 ], [], [ 2.2, 3.3 ] ] # a list of lists of floats
##
## The list [ 1, "a" ] gives an error, because each element in a list must have
## the same type. If you want to put a mix of #Int and #Str values into a list, try this:
##
## ```
## mixedList : List [ IntElem Int, StrElem Str ]*
## mixedList = [ IntElem 1, IntElem 2, StrElem "a", StrElem "b" ]
## ```
##
## The maximum size of a #List is limited by the amount of heap memory available
## to the current process. If there is not enough memory available, attempting to
## create the list could crash. (On Linux, where [overcommit](https://www.etalabs.net/overcommit.html)
## is normally enabled, not having enough memory could result in the list appearing
## to be created just fine, but then crashing later.)
##
## > The theoretical maximum length for a list created in Roc is
## > #Int.highestUlen divided by 2. Attempting to create a list bigger than that
## > in Roc code will always fail, although in practice it is likely to fail
## > at much smaller lengths due to insufficient memory being available.
##
## ## Performance notes
##
## Under the hood, a list is a record containing a `len : Ulen` field as well
## as a pointer to a flat list of bytes.
##
## This is not a [persistent data structure](https://en.wikipedia.org/wiki/Persistent_data_structure),
## so copying it is not cheap! The reason #List is designed this way is because:
##
## * Copying small lists is typically slightly faster than copying small persistent data structures. This is because, at small sizes, persistent data structures are usually thin wrappers around flat lists anyway. They don't start conferring copying advantages until crossing a certain minimum size threshold.
## Many list operations are no faster with persistent data structures. For example, even if it were a persistent data structure, #List.map, #List.fold, and #List.keepIf would all need to traverse every element in the list and build up the result from scratch.
## * Roc's compiler optimizes many list operations into in-place mutations behind the scenes, depending on how the list is being used. For example, #List.map, #List.keepIf, and #List.set can all be optimized to perform in-place mutations.
## * If possible, it is usually best for performance to use large lists in a way where the optimizer can turn them into in-place mutations. If this is not possible, a persistent data structure might be faster - but this is a rare enough scenario that it would not be good for the average Roc program's performance if this were the way #List worked by default. Instead, you can look outside Roc's standard modules for an implementation of a persistent data structure - likely built using #List under the hood!
List elem : @List elem
## Initialize
single : elem -> List elem
empty : List *
repeat : elem, Ulen -> List elem
range : Int a, Int a -> List (Int a)
## TODO I don't think we should have this after all. *Maybe* have an Ok.toList instead?
##
## When given an #Err, returns #[], and when given #Ok, returns a list containing
## only that value.
##
## >>> List.fromResult (Ok 5)
##
## >>> List.fromResult (Err "the Err's contents get discarded")
##
## This is useful when using `List.joinMap` with a function ## that returns a #Result.
##
## > (List.joinMap [ "cat", "", "bat", "" ] Str.first) |> List.map List.fromResult
fromResult : Result elem * -> List elem
## Transform
reverse : List elem -> List elem
sort : List elem, Sorter elem -> List elem
## Convert each element in the list to something new, by calling a conversion
## function on each of them. Then return a new list of the converted values.
##
## > List.map [ 1, 2, 3 ] (\num -> num + 1)
##
## > List.map [ "", "a", "bc" ] Str.isEmpty
##
## `map` functions like this are common in Roc, and they all work similarly.
## See for example #Result.map, #Set.map, and #Map.map.
map : List before, (before -> after) -> List after
## This works the same way as #List.map, except it also passes the index
## of the element to the conversion function.
indexedMap : List before, (before, Int -> after) -> List after
## Add a single element to the end of a list.
##
## >>> List.append [ 1, 2, 3 ] 4
##
## >>> [ 0, 1, 2 ]
## >>> |> List.append 3
append : List elem, elem -> List elem
## Add a single element to the beginning of a list.
##
## >>> List.prepend [ 1, 2, 3 ] 0
##
## >>> [ 2, 3, 4 ]
## >>> |> List.prepend 1
prepend : List elem, elem -> List elem
## Put two lists together.
##
## >>> List.concat [ 1, 2, 3 ] [ 4, 5 ]
##
## >>> [ 0, 1, 2 ]
## >>> |> List.concat [ 3, 4 ]
concat : List elem, List elem -> List elem
## Join the given lists together into one list.
##
## >>> List.join [ [ 1, 2, 3 ], [ 4, 5 ], [], [ 6, 7 ] ]
##
## >>> List.join [ [], [] ]
##
## >>> List.join []
join : List (List elem) -> List elem
joinMap : List before, (before -> List after) -> List after
## Like #List.join, but only keeps elements tagged with `Ok`. Elements
## tagged with `Err` are dropped.
##
## This can be useful after using an operation that returns a #Result
## on each element of a list, for example #List.first:
##
## >>> [ [ 1, 2, 3 ], [], [], [ 4, 5 ] ]
## >>> |> List.map List.first
## >>> |> List.joinOks
joinOks : List (Result elem *) -> List elem
## Iterates over the shortest of the given lists and returns a list of `Pair`
## tags, each wrapping one of the elements in that list, along with the elements
## in the same position in # the other lists.
##
## >>> List.zip [ "a1", "b1" "c1" ] [ "a2", "b2" ] [ "a3", "b3", "c3" ]
##
## Accepts up to 8 lists.
##
## > For a generalized version that returns whatever you like, instead of a `Pair`,
## > see `zipMap`.
zip :
List a, List b, -> List [ Pair a b ]*
List a, List b, List c, -> List [ Pair a b c ]*
List a, List b, List c, List d -> List [ Pair a b c d ]*
## Like `zip` but you can specify what to do with each element.
##
## More specifically, [repeat what zip's docs say here]
##
## >>> List.zipMap [ 1, 2, 3 ] [ 0, 5, 4 ] [ 2, 1 ] \num1 num2 num3 -> num1 + num2 - num3
##
## Accepts up to 8 lists.
zipMap :
List a, List b, (a, b) -> List c |
List a, List b, List c, (a, b, c) -> List d |
List a, List b, List c, List d, (a, b, c, d) -> List e
## Filter
## Run the given function on each element of a list, and return all the
## elements for which the function returned `True`.
##
## >>> List.keepIf [ 1, 2, 3, 4 ] (\num -> num > 2)
##
## ## Performance Notes
##
## #List.keepIf always returns a list that takes up exactly the same amount
## of memory as the original, even if its length decreases. This is becase it
## can't know in advance exactly how much space it will need, and if it guesses a
## length that's too low, it would have to re-allocate.
##
## (If you want to do an operation like this which reduces the memory footprint
## of the resulting list, you can do two passes over the lis with #List.fold - one
## to calculate the precise new size, and another to populate the new list.)
##
## If given a unique list, #List.keepIf will mutate it in place to assemble the appropriate list.
## If that happens, this function will not allocate any new memory on the heap.
## If all elements in the list end up being kept, Roc will return the original
## list unaltered.
##
keepIf : List elem, (elem -> [True, False]) -> List elem
## Run the given function on each element of a list, and return all the
## elements for which the function returned `False`.
##
## >>> List.dropIf [ 1, 2, 3, 4 ] (\num -> num > 2)
##
## ## Performance Notes
##
## #List.dropIf has the same performance characteristics as #List.keepIf.
## See its documentation for details on those characteristics!
dropIf : List elem, (elem -> [True, False]) -> List elem
## Takes the requested number of elements from the front of a list
## and returns them.
##
## >>> take 5 [ 1, 2, 3, 4, 5, 6, 7, 8 ]
##
## If there are fewer elements in the list than the requeted number,
## returns the entire list.
##
## >>> take 5 [ 1, 2 ]
take : List elem, Int -> List elem
## Drops the requested number of elements from the front of a list
## and returns the rest.
##
## >>> drop 5 [ 1, 2, 3, 4, 5, 6, 7, 8 ]
##
## If there are fewer elements in the list than the requested number to
## be dropped, returns an empty list.
##
## >>> drop 5 [ 1, 2 ]
drop : List elem, Int -> List elem
## Access
first : List elem -> [Ok elem, ListWasEmpty]*
last : List elem -> [Ok elem, ListWasEmpty]*
get : List elem, Ulen -> [Ok elem, OutOfBounds]*
max : List (Num a) -> [Ok (Num a), ListWasEmpty]*
min : List (Num a) -> [Ok (Num a), ListWasEmpty]*
## Modify
set : List elem, Ulen, elem -> List elem
## Deconstruct
split : List elem, Int -> { before: List elem, remainder: List elem }
walk : List elem, { start : state, step : (state, elem -> state) } -> state
walkBackwards : List elem, { start : state, step : (state, elem -> state) } -> state
## Check
## Returns the length of the list - the number of elements it contains.
##
## One #List can store up to 2,147,483,648 elements (just over 2 billion), which
## is exactly equal to the highest valid #I32 value. This means the #U32 this function
## returns can always be safely converted to an #I32 without losing any data.
len : List * -> Ulen
isEmpty : List * -> Bool
contains : List elem, elem -> Bool
all : List elem, (elem -> Bool) -> Bool
any : List elem, (elem -> Bool) -> Bool

View file

@ -0,0 +1,586 @@
interface List
exposes [ List, map, fold ]
imports []
## Types
## A sequential list of values.
## # >>> [ 1, 2, 3 ] # a list of numbers # # >>> [ "a", "b", "c" ] # a list of strings
##
## >>> [ [ 1.1 ], [], [ 2.2, 3.3 ] ] # a list of lists of floats
##
## The list [ 1, "a" ] gives an error, because each element in a list must have
## the same type. If you want to put a mix of #Int and #Str values into a list, try this:
##
## ```
## mixedList : List [ IntElem Int, StrElem Str ]*
## mixedList = [ IntElem 1, IntElem 2, StrElem "a", StrElem "b" ]
## ```
##
## The maximum size of a #List is limited by the amount of heap memory available
## to the current process. If there is not enough memory available, attempting to
## create the list could crash. (On Linux, where [overcommit](https://www.etalabs.net/overcommit.html)
## is normally enabled, not having enough memory could result in the list appearing
## to be created just fine, but then crashing later.)
##
## > The theoretical maximum length for a list created in Roc is
## > #Int.highestLen divided by 2. Attempting to create a list bigger than that
## > in Roc code will always fail, although in practice it is likely to fail
## > at much smaller lengths due to insufficient memory being available.
##
## ## Performance Details
##
## Under the hood, a list is a record containing a `len : Len` field as well
## as a pointer to a reference count and a flat array of bytes. Unique lists
## store a capacity #Len instead of a reference count.
##
## ## Shared Lists
##
## Shared lists are [reference counted](https://en.wikipedia.org/wiki/Reference_counting).
##
## Each time a given list gets referenced, its reference count ("refcount" for short)
## gets incremented. Each time a list goes out of scope, its refcount count gets
## decremented. Once a refcount, has been decremented more times than it has been
## incremented, we know nothing is referencing it anymore, and the list's memory
## will be immediately freed.
##
## Let's look at an example.
##
## ratings = [ 5, 4, 3 ]
##
## { foo: ratings, bar: ratings }
##
## The first line binds the name `ratings` to the list `[ 5, 4, 3 ]`. The list
## begins with a refcount of 1, because so far only `ratings` is referencing it.
##
## The second line alters this refcount. `{ foo: ratings` references
## the `ratings` list, which will result in its refcount getting incremented
## from 0 to 1. Similarly, `bar: ratings }` also references the `ratings` list,
## which will result in its refcount getting incremented from 1 to 2.
##
## Let's turn this example into a function.
##
## getRatings = \first ->
## ratings = [ first, 4, 3 ]
##
## { foo: ratings, bar: ratings }
##
## getRatings 5
##
## At the end of the `getRatings` function, when the record gets returned,
## the original `ratings =` binding has gone out of scope and is no longer
## accessible. (Trying to reference `ratings` outside the scope of the
## `getRatings` function would be an error!)
##
## Since `ratings` represented a way to reference the list, and that way is no
## longer accessible, the list's refcount gets decremented when `ratings` goes
## out of scope. It will decrease from 2 back down to 1.
##
## Putting these together, when we call `getRatings 5`, what we get back is
## a record with two fields, `foo`, and `bar`, each of which refers to the same
## list, and that list has a refcount of 1.
##
## Let's change the last line to be `(getRatings 5).bar` instead of `getRatings 5`:
##
## getRatings = \first ->
## ratings = [ first, 4, 3 ]
##
## { foo: ratings, bar: ratings }
##
## (getRatings 5).bar
##
## Now, when this expression returns, only the `bar` field of the record will
## be returned. This will mean that the `foo` field becomes inaccessible, causing
## the list's refcount to get decremented from 2 to 1. At this point, the list is back
## where it started: there is only 1 reference to it.
##
## Finally let's suppose the final line were changed to this:
##
## List.first (getRatings 5).bar
##
## This call to #List.first means that even the list in the `bar` field has become
## inaccessible. As such, this line will cause the list's refcount to get
## decremented all the way to 0. At that point, nothing is referencing the list
## anymore, and its memory will get freed.
##
## Things are different if this is a list of lists instead of a list of numbers.
## Let's look at a simpler example using #List.first - first with a list of numbers,
## and then with a list of lists, to see how they differ.
##
## Here's the example using a list of numbers.
##
## nums = [ 1, 2, 3, 4, 5, 6, 7 ]
##
## first = List.first nums
## last = List.last nums
##
## first
##
## It makes a list, calls #List.first and #List.last on it, and then returns `first`.
##
## Here's the equivalent code with a list of lists:
##
## lists = [ [ 1 ], [ 2, 3 ], [], [ 4, 5, 6, 7 ] ]
##
## first = List.first lists
## last = List.last lists
##
## first
##
## TODO explain how in the former example, when we go to free `nums` at the end,
## we can free it immediately because there are no other refcounts. However,
## in the case of `lists`, we have to iterate through the list and decrement
## the refcounts of each of its contained lists - because they, too, have
## refcounts! Importantly, beacuse the first element had its refcount incremented
## because the function returned `first`, that element will actually end up
## *not* getting freed at the end - but all the others will be.
##
## In the `lists` example, `lists = [ ... ]` also creates a list with an initial
## refcount of 1. Separately, it also creates several other lists - each with
## their own refcounts - to go inside that list. (The empty list at the end
## does not use heap memory, and thus has no refcount.)
##
## At the end, we once again call #List.first on the list, but this time
##
## * Copying small lists (64 elements or fewer) is typically slightly faster than copying small persistent data structures. This is because, at small sizes, persistent data structures tend to be thin wrappers around flat arrays anyway. They don't have any copying advantage until crossing a certain minimum size threshold.
## * Even when copying is faster, other list operations may still be slightly slower with persistent data structures. For example, even if it were a persistent data structure, #List.map, #List.fold, and #List.keepIf would all need to traverse every element in the list and build up the result from scratch. These operations are all
## * Roc's compiler optimizes many list operations into in-place mutations behind the scenes, depending on how the list is being used. For example, #List.map, #List.keepIf, and #List.set can all be optimized to perform in-place mutations.
## * If possible, it is usually best for performance to use large lists in a way where the optimizer can turn them into in-place mutations. If this is not possible, a persistent data structure might be faster - but this is a rare enough scenario that it would not be good for the average Roc program's performance if this were the way #List worked by default. Instead, you can look outside Roc's standard modules for an implementation of a persistent data structure - likely built using #List under the hood!
List elem : @List elem
## Initialize
## A list with a single element in it.
##
## This is useful in pipelines, like so:
##
## websites =
## Str.concat domain ".com"
## |> List.single
##
single : elem -> List elem
## An empty list.
empty : List *
## Returns a list with the given length, where every element is the given value.
##
##
repeat : elem, Len -> List elem
## Returns a list of all the integers between one and another,
## including both of the given numbers.
##
## >>> List.range 2 8
range : Int a, Int a -> List (Int a)
## Transform
## Returns the list with its elements reversed.
##
## >>> List.reverse [ 1, 2, 3 ]
reverse : List elem -> List elem
## Sorts a list using a function which specifies how two elements are ordered.
##
##
sort : List elem, (elem, elem -> [ Lt, Eq, Gt ]) -> List elem
## Convert each element in the list to something new, by calling a conversion
## function on each of them. Then return a new list of the converted values.
##
## > List.map [ 1, 2, 3 ] (\num -> num + 1)
##
## > List.map [ "", "a", "bc" ] Str.isEmpty
##
## `map` functions like this are common in Roc, and they all work similarly.
## See for example #Result.map, #Set.map, and #Map.map.
map : List before, (before -> after) -> List after
## This works like #List.map, except it also passes the index
## of the element to the conversion function.
mapWithIndex : List before, (before, Int -> after) -> List after
## This works like #List.map, except at any time you can return `Err` to
## cancel the entire operation immediately, and return that #Err.
mapOrCancel : List before, (before -> Result after err) -> Result (List after) err
## This works like #List.map, except only the transformed values that are
## wrapped in `Ok` are kept. Any that are wrapped in `Err` are dropped.
##
## >>> List.mapOks [ [ "a", "b" ], [], [], [ "c", "d", "e" ] ] List.last
##
## >>> fn = \str -> if Str.isEmpty str then Err StrWasEmpty else Ok (Str.len str)
## >>>
## >>> List.mapOks [ "", "a", "bc", "", "d", "ef", "" ]
mapOks : List before, (before -> Result after *) -> List after
## If all the elements in the list are #Ok, return a new list containing the
## contents of those #Ok tags. If any elements are #Err, return #Err.
allOks : List (Result ok err) -> Result (List ok) err
## Add a single element to the end of a list.
##
## >>> List.append [ 1, 2, 3 ] 4
##
## >>> [ 0, 1, 2 ]
## >>> |> List.append 3
append : List elem, elem -> List elem
## Add a single element to the beginning of a list.
##
## >>> List.prepend [ 1, 2, 3 ] 0
##
## >>> [ 2, 3, 4 ]
## >>> |> List.prepend 1
prepend : List elem, elem -> List elem
## Put two lists together.
##
## >>> List.concat [ 1, 2, 3 ] [ 4, 5 ]
##
## >>> [ 0, 1, 2 ]
## >>> |> List.concat [ 3, 4 ]
concat : List elem, List elem -> List elem
## Join the given lists together into one list.
##
## >>> List.join [ [ 1, 2, 3 ], [ 4, 5 ], [], [ 6, 7 ] ]
##
## >>> List.join [ [], [] ]
##
## >>> List.join []
join : List (List elem) -> List elem
## Like #List.map, except the transformation function wraps the return value
## in a list. At the end, all the lists get joined together into one list.
joinMap : List before, (before -> List after) -> List after
## Like #List.join, but only keeps elements tagged with `Ok`. Elements
## tagged with `Err` are dropped.
##
## This can be useful after using an operation that returns a #Result
## on each element of a list, for example #List.first:
##
## >>> [ [ 1, 2, 3 ], [], [], [ 4, 5 ] ]
## >>> |> List.map List.first
## >>> |> List.joinOks
joinOks : List (Result elem *) -> List elem
## Iterates over the shortest of the given lists and returns a list of `Pair`
## tags, each wrapping one of the elements in that list, along with the elements
## in the same index in # the other lists.
##
## >>> List.zip [ "a1", "b1" "c1" ] [ "a2", "b2" ] [ "a3", "b3", "c3" ]
##
## Accepts up to 8 lists.
##
## > For a generalized version that returns whatever you like, instead of a `Pair`,
## > see `zipMap`.
zip : List a, List b, -> List [ Pair a b ]*
## Like `zip` but you can specify what to do with each element.
##
## More specifically, [repeat what zip's docs say here]
##
## >>> List.zipMap [ 1, 2, 3 ] [ 0, 5, 4 ] [ 2, 1 ] \num1 num2 num3 -> num1 + num2 - num3
##
## Accepts up to 8 lists.
zipMap : List a, List b, (a, b) -> List c
## Filter
## Run the given function on each element of a list, and return all the
## elements for which the function returned `True`.
##
## >>> List.keepIf [ 1, 2, 3, 4 ] (\num -> num > 2)
##
## ## Performance Details
##
## #List.keepIf always returns a list that takes up exactly the same amount
## of memory as the original, even if its length decreases. This is becase it
## can't know in advance exactly how much space it will need, and if it guesses a
## length that's too low, it would have to re-allocate.
##
## (If you want to do an operation like this which reduces the memory footprint
## of the resulting list, you can do two passes over the lis with #List.fold - one
## to calculate the precise new size, and another to populate the new list.)
##
## If given a unique list, #List.keepIf will mutate it in place to assemble the appropriate list.
## If that happens, this function will not allocate any new memory on the heap.
## If all elements in the list end up being kept, Roc will return the original
## list unaltered.
##
keepIf : List elem, (elem -> Bool) -> List elem
## Run the given function on each element of a list, and return all the
## elements for which the function returned `False`.
##
## >>> List.dropIf [ 1, 2, 3, 4 ] (\num -> num > 2)
##
## ## Performance Details
##
## #List.dropIf has the same performance characteristics as #List.keepIf.
## See its documentation for details on those characteristics!
dropIf : List elem, (elem -> Bool) -> List elem
## Access
## Returns the first element in the list, or `ListWasEmpty` if it was empty.
first : List elem -> Result elem [ ListWasEmpty ]*
## Returns the last element in the list, or `ListWasEmpty` if it was empty.
last : List elem -> Result elem [ ListWasEmpty ]*
get : List elem, Len -> Result elem [ OutOfBounds ]*
max : List (Num a) -> Result (Num a) [ ListWasEmpty ]*
min : List (Num a) -> Result (Num a) [ ListWasEmpty ]*
## Modify
## Replaces the element at the given index with a replacement.
##
## >>> List.put [ "a", "b", "c" ] 1 "B"
##
## If the given index is outside the bounds of the list, returns the original
## list unmodified.
put : List elem, Len, elem -> List elem
## Adds a new element to the end of the list.
##
## >>> List.append [ "a", "b" ] "c"
##
## ## Performance Details
##
## When given a Unique list, this adds the new element in-place if possible.
## This is only possible if the list has enough capacity. Otherwise, it will
## have to *clone and grow*. See the section on [capacity](#capacity) in this
## module's documentation.
append : List elem, elem -> List elem
## Adds a new element to the beginning of the list.
##
## >>> List.prepend [ "b", "c" ] "a"
##
## ## Performance Details
##
## This always clones the entire list, even when given a Unique list. That means
## it runs about as fast as #List.addLast when both are given a Shared list.
##
## If you have a Unique list instead, #List.append will run much faster than
## #List.prepend except in the specific case where the list has no excess capacity,
## and needs to *clone and grow*. In that uncommon case, both #List.append and
## #List.prepend will run at about the same speed—since #List.prepend always
## has to clone and grow.
##
## | Unique list | Shared list |
##---------+--------------------------------+----------------+
## append | in-place given enough capacity | clone and grow |
## prepend | clone and grow | clone and grow |
prepend : List elem, elem -> List elem
## Remove the last element from the list.
##
## Returns both the removed element as well as the new list (with the removed
## element missing), or `Err ListWasEmpty` if the list was empty.
##
## Here's one way you can use this:
##
## when List.pop list is
## Ok { others, last } -> ...
## Err ListWasEmpty -> ...
##
## ## Performance Details
##
## Calling #List.pop on a Unique list runs extremely fast. It's essentially
## the same as a #List.last except it also returns the #List it was given,
## with its length decreased by 1.
##
## In contrast, calling #List.pop on a Shared list creates a new list, then
## copies over every element in the original list except the last one. This
## takes much longer.
dropLast : List elem -> Result { others : List elem, last : elem } [ ListWasEmpty ]*
##
## Here's one way you can use this:
##
## when List.pop list is
## Ok { others, last } -> ...
## Err ListWasEmpty -> ...
##
## ## Performance Details
##
## When calling either #List.dropFirst or #List.dropLast on a Unique list, #List.dropLast
## runs *much* faster. This is because for #List.dropLast, removing the last element
## in-place is as easy as reducing the length of the list by 1. In contrast,
## removing the first element from the list involves copying every other element
## in the list into the index before it - which is massively more costly.
##
## In the case of a Shared list,
##
## | Unique list | Shared list |
##-----------+----------------------------------+---------------------------------+
## dropFirst | #List.last + length change | #List.last + clone rest of list |
## dropLast | #List.last + clone rest of list | #List.last + clone rest of list |
dropFirst : List elem -> Result { first: elem, others : List elem } [ ListWasEmpty ]*
## Returns the given number of elements from the beginning of the list.
##
## >>> List.takeFirst 4 [ 1, 2, 3, 4, 5, 6, 7, 8 ]
##
## If there are fewer elements in the list than the requested number,
## returns the entire list.
##
## >>> List.takeFirst 5 [ 1, 2 ]
##
## To *remove* elements from the beginning of the list, use #List.takeLast.
##
## To remove elements from both the beginning and end of the list,
## use #List.sublist.
##
## To split the list into two lists, use #List.split.
##
## ## Performance Details
##
## When given a Unique list, this runs extremely fast. It sets the list's length
## to the given length value, and frees the leftover elements. This runs very
## slightly faster than #List.takeLast.
##
## In fact, `List.takeFirst 1 list` runs faster than `List.first list` when given
## a Unique list, because #List.first returns the first element as well -
## which introduces a conditional bounds check as well as a memory load.
takeFirst : List elem, Len -> List elem
## Returns the given number of elements from the end of the list.
##
## >>> List.takeLast 4 [ 1, 2, 3, 4, 5, 6, 7, 8 ]
##
## If there are fewer elements in the list than the requested number,
## returns the entire list.
##
## >>> List.takeLast 5 [ 1, 2 ]
##
## To *remove* elements from the end of the list, use #List.takeFirst.
##
## To remove elements from both the beginning and end of the list,
## use #List.sublist.
##
## To split the list into two lists, use #List.split.
##
## ## Performance Details
##
## When given a Unique list, this runs extremely fast. It moves the list's
## pointer to the index at the given length value, updates its length,
## and frees the leftover elements. This runs very nearly as fast as
## #List.takeFirst on a Unique list.
##
## In fact, `List.takeLast 1 list` runs faster than `List.first list` when given
## a Unique list, because #List.first returns the first element as well -
## which introduces a conditional bounds check as well as a memory load.
takeLast : List elem, Len -> List elem
## Deconstruct
## Splits the list into two lists, around the given index.
##
## The returned lists are labeled `before` and `others`. The `before` list will
## contain all the elements whose index in the original list was **less than**
## than the given index, # and the `others` list will be all the others. (This
## means if you give an index of 0, the `before` list will be empty and the
## `others` list will have the same elements as the original list.)
split : List elem, Len -> { before: List elem, others: List elem }
## Returns a subsection of the given list, beginning at the `start` index and
## including a total of `len` elements.
##
## If `start` is outside the bounds of the given list, returns the empty list.
##
## >>> List.sublist { start: 4, len: 0 } [ 1, 2, 3 ]
##
## If more elements are requested than exist in the list, returns as many as it can.
##
## >>> List.sublist { start: 2, len: 10 } [ 1, 2, 3, 4, 5 ]
##
## > If you want a sublist which goes all the way to the end of the list, no
## > matter how long the list is, #List.takeLast can do that more efficiently.
##
## Some languages have a function called **`slice`** which works similarly to this.
sublist : List elem, { start : Len, len : Len } -> List elem
## Build a value using each element in the list.
##
## Starting with a given `state` value, this walks through each element in the
## list from first to last, running a given `step` function on that element
## which updates the `state`. It returns the final `state` at the end.
##
## You can use it in a pipeline:
##
## [ 2, 4, 8 ]
## |> List.walk { start: 0, step: Num.add }
##
## This returns 14 because:
## * `state` starts at 0 (because of `start: 0`)
## * Each `step` runs `Num.add state elem`, and the return value becomes the new `state`.
##
## Here is a table of how `state` changes as #List.walk walks over the elements
## `[ 2, 4, 8 ]` using #Num.add as its `step` function to determine the next `state`.
##
## `state` | `elem` | `step state elem` (`Num.add state elem`)
## --------+--------+-----------------------------------------
## 0 | |
## 0 | 2 | 2
## 2 | 4 | 6
## 6 | 8 | 14
##
## So `state` goes through these changes:
## 1. `0` (because of `start: 0`)
## 2. `1` (because of `Num.add state elem` with `state` = 0 and `elem` = 1
##
## [ 1, 2, 3 ]
## |> List.walk { start: 0, step: Num.sub }
##
## This returns -6 because
##
## Note that in other languages, `walk` is sometimes called `reduce`,
## `fold`, `foldLeft`, or `foldl`.
walk : List elem, { start : state, step : (state, elem -> state) } -> state
## Note that in other languages, `walkBackwards` is sometimes called `reduceRight`,
## `fold`, `foldRight`, or `foldr`.
walkBackwards : List elem, { start : state, step : (state, elem -> state ]) } -> state
## Same as #List.walk, except you can stop walking early.
##
## ## Performance Details
##
## Compared to #List.walk, this can potentially visit fewer elements (which can
## improve performance) at the cost of making each step take longer.
## However, the added cost to each step is extremely small, and can easily
## be outweighed if it results in skipping even a small number of elements.
##
## As such, it is typically better for performance to use this over #List.walk
## if returning `Done` earlier than the last element is expected to be common.
walkUntil : List elem, { start : state, step : (state, elem -> [ Continue state, Done state ]) } -> state
# Same as #List.walkBackwards, except you can stop walking early.
walkBackwardsUntil : List elem, { start : state, step : (state, elem -> [ Continue state, Done state ]) } -> state
## Check
## Returns the length of the list - the number of elements it contains.
##
## One #List can store up to 2,147,483,648 elements (just over 2 billion), which
## is exactly equal to the highest valid #I32 value. This means the #U32 this function
## returns can always be safely converted to an #I32 without losing any data.
len : List * -> Len
isEmpty : List * -> Bool
contains : List elem, elem -> Bool
all : List elem, (elem -> Bool) -> Bool
any : List elem, (elem -> Bool) -> Bool

View file

@ -475,6 +475,23 @@ ceil : Float * -> Int *
floor : Float * -> Int * floor : Float * -> Int *
trunc : Float * -> Int * trunc : Float * -> Int *
## Convert an #Int to a #Len. If the given number doesn't fit in #Len, it will be truncated.
## Since #Len has a different maximum number depending on the system you're building
## for, this may give a different answer on different systems.
##
## For example, on a 32-bit sytem, #Num.maxLen will return the same answer as
## #Num.maxU32. This means that calling `Num.toLen 9_000_000_000` on a 32-bit
## system will return #Num.maxU32 instead of 9 billion, because 9 billion is
## higher than #Num.maxU32 and will not fit in a #Len on a 32-bit system.
##
## However, calling `Num.toLen 9_000_000_000` on a 64-bit system will return
## the #Len value of 9_000_000_000. This is because on a 64-bit system, #Len can
## hold up to #Num.maxU64, and 9_000_000_000 is lower than #Num.maxU64.
##
## To convert a #Float to a #Len, first call either #Num.round, #Num.ceil, or #Num.floor
## on it, then call this on the resulting #Int.
toLen : Int * -> Len
## Convert an #Int to an #I8. If the given number doesn't fit in #I8, it will be truncated. ## Convert an #Int to an #I8. If the given number doesn't fit in #I8, it will be truncated.
## ##
## To convert a #Float to an #I8, first call either #Num.round, #Num.ceil, or #Num.floor ## To convert a #Float to an #I8, first call either #Num.round, #Num.ceil, or #Num.floor
@ -484,6 +501,7 @@ toI16 : Int * -> I16
toI32 : Int * -> I32 toI32 : Int * -> I32
toI64 : Int * -> I64 toI64 : Int * -> I64
toI128 : Int * -> I128 toI128 : Int * -> I128
## Convert an #Int to an #U8. If the given number doesn't fit in #U8, it will be truncated. ## Convert an #Int to an #U8. If the given number doesn't fit in #U8, it will be truncated.
## Crashes if the given number is negative. ## Crashes if the given number is negative.
toU8 : Int * -> U8 toU8 : Int * -> U8

View file

@ -543,6 +543,15 @@ pub fn types() -> MutMap<Symbol, (SolvedType, Region)> {
), ),
); );
// append : List elem, List elem -> List elem
add_type(
Symbol::LIST_APPEND,
SolvedType::Func(
vec![list_type(flex(TVAR1)), list_type(flex(TVAR1))],
Box::new(list_type(flex(TVAR1))),
),
);
// len : List * -> Int // len : List * -> Int
add_type( add_type(
Symbol::LIST_LEN, Symbol::LIST_LEN,

View file

@ -638,6 +638,37 @@ pub fn types() -> MutMap<Symbol, (SolvedType, Region)> {
) )
}); });
// append : Attr * (List (Attr * a)), Attr * (List (Attr * a)) -> Attr * (List (Attr * a))
add_type(Symbol::LIST_APPEND, {
let_tvars! { a, star1, star2, star3 };
unique_function(
vec![
SolvedType::Apply(
Symbol::ATTR_ATTR,
vec![
flex(star1),
SolvedType::Apply(Symbol::LIST_LIST, vec![flex(a)]),
],
),
SolvedType::Apply(
Symbol::ATTR_ATTR,
vec![
flex(star2),
SolvedType::Apply(Symbol::LIST_LIST, vec![flex(a)]),
],
),
],
SolvedType::Apply(
Symbol::ATTR_ATTR,
vec![
boolean(star3),
SolvedType::Apply(Symbol::LIST_LIST, vec![flex(a)]),
],
),
)
});
// push : Attr * (List a) // push : Attr * (List a)
// , a // , a
// -> Attr * (List a) // -> Attr * (List a)

View file

@ -59,6 +59,7 @@ pub fn builtin_defs(var_store: &mut VarStore) -> MutMap<Symbol, Def> {
Symbol::LIST_SINGLE => list_single, Symbol::LIST_SINGLE => list_single,
Symbol::LIST_REPEAT => list_repeat, Symbol::LIST_REPEAT => list_repeat,
Symbol::LIST_REVERSE => list_reverse, Symbol::LIST_REVERSE => list_reverse,
Symbol::LIST_APPEND => list_append,
Symbol::NUM_ADD => num_add, Symbol::NUM_ADD => num_add,
Symbol::NUM_SUB => num_sub, Symbol::NUM_SUB => num_sub,
Symbol::NUM_MUL => num_mul, Symbol::NUM_MUL => num_mul,
@ -616,6 +617,28 @@ fn list_reverse(symbol: Symbol, var_store: &mut VarStore) -> Def {
) )
} }
/// List.append : List elem, List elem -> List elem
fn list_append(symbol: Symbol, var_store: &mut VarStore) -> Def {
let list_var = var_store.fresh();
let body = RunLowLevel {
op: LowLevel::ListAppend,
args: vec![
(list_var, Var(Symbol::ARG_1)),
(list_var, Var(Symbol::ARG_2)),
],
ret_var: list_var,
};
defn(
symbol,
vec![(list_var, Symbol::ARG_1), (list_var, Symbol::ARG_2)],
var_store,
body,
list_var,
)
}
/// List.repeat : elem, Int -> List elem /// List.repeat : elem, Int -> List elem
fn list_repeat(symbol: Symbol, var_store: &mut VarStore) -> Def { fn list_repeat(symbol: Symbol, var_store: &mut VarStore) -> Def {
let elem_var = var_store.fresh(); let elem_var = var_store.fresh();

View file

@ -1,3 +1,4 @@
use crate::builtins::builtin_defs;
use crate::def::{canonicalize_defs, sort_can_defs, Declaration}; use crate::def::{canonicalize_defs, sort_can_defs, Declaration};
use crate::env::Env; use crate::env::Env;
use crate::expr::Output; use crate::expr::Output;
@ -114,7 +115,7 @@ pub fn canonicalize_module_defs<'a>(
} }
} }
let (defs, _scope, output, symbols_introduced) = canonicalize_defs( let (mut defs, _scope, output, symbols_introduced) = canonicalize_defs(
&mut env, &mut env,
Output::default(), Output::default(),
var_store, var_store,
@ -152,6 +153,14 @@ pub fn canonicalize_module_defs<'a>(
references.insert(*symbol); references.insert(*symbol);
} }
// Add defs for any referenced builtins.
for (symbol, def) in builtin_defs(var_store) {
if output.references.lookups.contains(&symbol) || output.references.calls.contains(&symbol)
{
defs.can_defs_by_symbol.insert(symbol, def);
}
}
match sort_can_defs(&mut env, defs, Output::default()) { match sort_can_defs(&mut env, defs, Output::default()) {
(Ok(declarations), output) => { (Ok(declarations), output) => {
use crate::def::Declaration::*; use crate::def::Declaration::*;

View file

@ -1325,6 +1325,19 @@ pub fn load_list_len<'ctx>(
.into_int_value() .into_int_value()
} }
fn list_is_not_empty<'ctx>(
builder: &Builder<'ctx>,
ctx: &'ctx Context,
list_len: IntValue<'ctx>,
) -> IntValue<'ctx> {
builder.build_int_compare(
IntPredicate::UGT,
list_len,
ctx.i64_type().const_int(0, false),
"greaterthanzero",
)
}
fn load_list_ptr<'ctx>( fn load_list_ptr<'ctx>(
builder: &Builder<'ctx>, builder: &Builder<'ctx>,
wrapper_struct: StructValue<'ctx>, wrapper_struct: StructValue<'ctx>,
@ -1806,6 +1819,7 @@ fn run_low_level<'a, 'ctx, 'env>(
} }
} }
} }
ListAppend => list_append(env, layout_ids, scope, parent, args),
ListPush => { ListPush => {
// List.push List elem, elem -> List elem // List.push List elem, elem -> List elem
debug_assert_eq!(args.len(), 2); debug_assert_eq!(args.len(), 2);
@ -2052,6 +2066,446 @@ fn build_int_binop<'a, 'ctx, 'env>(
} }
} }
fn list_append<'a, 'ctx, 'env>(
env: &Env<'a, 'ctx, 'env>,
layout_ids: &mut LayoutIds<'a>,
scope: &Scope<'a, 'ctx>,
parent: FunctionValue<'ctx>,
args: &[(Expr<'a>, Layout<'a>)],
) -> BasicValueEnum<'ctx> {
// List.append : List elem, List elem -> List elem
debug_assert_eq!(args.len(), 2);
// This implementation is quite long, let me explain what is complicating it. Here are our
// contraints:
//
// constraint 1. lists might be empty because they have the layout `EmptyList`, or they might
// be empty because they have a `List` layout, but happen to be empty, such as in this code:
//
// list : List Int
// list =
// []
//
// So we have two sources of truth for emptiness.
//
// constraint 2. iterating over a non-empty list involves allocating memory for a index and
// a loop, and allocating memory is costly, so we dont want to even try to iterate over empty
// lists.
//
// Accounting for all the possibilities in the two constraints above gives us 9 code paths:
//
// first list EmptyList List(list) List(list)
// second list where list.length = 0 where list.length > 0
// ---------------------------------------------------------------------
// EmptyList | [] | [] | clone(1st_list) |
// ---------------------------------------------------------------------
// List(list) | [] | [] | clone(1st_list) |
// where list.length = 0 | | | |
// ---------------------------------------------------------------------
// List(list) | clone(2nd_list) | clone(2nd_list) | 2nd_list ++ 1st_list |
// where list.length > 0 | | | |
// ---------------------------------------------------------------------
//
let builder = env.builder;
let ctx = env.context;
let (first_list, first_list_layout) = &args[0];
let (second_list, second_list_layout) = &args[1];
let second_list_wrapper =
build_expr(env, layout_ids, scope, parent, second_list).into_struct_value();
let second_list_len = load_list_len(builder, second_list_wrapper);
match first_list_layout {
Layout::Builtin(Builtin::EmptyList) => {
match second_list_layout {
Layout::Builtin(Builtin::EmptyList) => empty_list(env),
Layout::Builtin(Builtin::List(elem_layout)) => {
// THIS IS A COPY AND PASTE
// All the code under the Layout::Builtin(Builtin::List()) match branch
// is the same as what is under `if_first_list_is_empty`. Re-using
// `if_first_list_is_empty` here however, creates memory problems.
// second_list_len > 0
// We do this check to avoid allocating memory. If the second input
// list is empty, then we can just return the first list cloned
let second_list_length_comparison =
list_is_not_empty(builder, ctx, second_list_len);
let build_second_list_then = || {
let elem_type =
basic_type_from_layout(env.arena, ctx, elem_layout, env.ptr_bytes);
let ptr_type = get_ptr_type(&elem_type, AddressSpace::Generic);
let (new_wrapper, _) = clone_nonempty_list(
env,
second_list_len,
load_list_ptr(builder, second_list_wrapper, ptr_type),
elem_layout,
);
BasicValueEnum::StructValue(new_wrapper)
};
let build_second_list_else = || empty_list(env);
build_basic_phi2(
env,
parent,
second_list_length_comparison,
build_second_list_then,
build_second_list_else,
BasicTypeEnum::StructType(collection(ctx, env.ptr_bytes)),
)
}
_ => {
unreachable!(
"Invalid List layout for second input list of List.append: {:?}",
second_list_layout
);
}
}
}
Layout::Builtin(Builtin::List(elem_layout)) => {
let first_list_wrapper =
build_expr(env, layout_ids, scope, parent, first_list).into_struct_value();
let first_list_len = load_list_len(builder, first_list_wrapper);
// first_list_len > 0
// We do this check to avoid allocating memory. If the first input
// list is empty, then we can just return the second list cloned
let first_list_length_comparison = list_is_not_empty(builder, ctx, first_list_len);
let if_first_list_is_not_empty = || {
let elem_type = basic_type_from_layout(env.arena, ctx, elem_layout, env.ptr_bytes);
let ptr_type = get_ptr_type(&elem_type, AddressSpace::Generic);
let elem_type = basic_type_from_layout(env.arena, ctx, elem_layout, env.ptr_bytes);
let if_second_list_is_empty = || {
let (new_wrapper, _) = clone_nonempty_list(
env,
first_list_len,
load_list_ptr(builder, first_list_wrapper, ptr_type),
elem_layout,
);
BasicValueEnum::StructValue(new_wrapper)
};
match second_list_layout {
Layout::Builtin(Builtin::EmptyList) => {
let (new_wrapper, _) = clone_nonempty_list(
env,
first_list_len,
load_list_ptr(builder, first_list_wrapper, ptr_type),
elem_layout,
);
BasicValueEnum::StructValue(new_wrapper)
}
Layout::Builtin(Builtin::List(_)) => {
// second_list_len > 0
// We do this check to avoid allocating memory. If the second input
// list is empty, then we can just return the first list cloned
let second_list_length_comparison =
list_is_not_empty(builder, ctx, second_list_len);
let if_second_list_is_not_empty = || {
let combined_list_len = builder.build_int_add(
first_list_len,
second_list_len,
"add_list_lengths",
);
let combined_list_ptr = env
.builder
.build_array_malloc(
elem_type,
combined_list_len,
"create_combined_list_ptr",
)
.unwrap();
let index_name = "#index";
let index_alloca = builder.build_alloca(ctx.i64_type(), index_name);
// The index variable begins at 0 and increments
// on each iteration of the loop
builder.build_store(index_alloca, ctx.i64_type().const_int(0, false));
// FIRST LOOP
{
let first_loop_bb =
ctx.append_basic_block(parent, "first_list_append_loop");
builder.build_unconditional_branch(first_loop_bb);
builder.position_at_end(first_loop_bb);
let curr_first_loop_index = builder
.build_load(index_alloca, index_name)
.into_int_value();
let first_list_ptr =
load_list_ptr(builder, first_list_wrapper, ptr_type);
// The pointer to the element in the first list
let first_list_elem_ptr = unsafe {
builder.build_in_bounds_gep(
first_list_ptr,
&[curr_first_loop_index],
"load_index",
)
};
// The pointer to the element in the combined list
let combined_list_elem_ptr = unsafe {
builder.build_in_bounds_gep(
combined_list_ptr,
&[curr_first_loop_index],
"load_index_combined_list",
)
};
let first_list_elem =
builder.build_load(first_list_elem_ptr, "get_elem");
// Mutate the new array in-place to change the element.
builder.build_store(combined_list_elem_ptr, first_list_elem);
// #index = #index + 1
let next_first_loop_index = builder.build_int_add(
curr_first_loop_index,
ctx.i64_type().const_int(1, false),
"nextindex",
);
builder.build_store(index_alloca, next_first_loop_index);
// #index < first_list_len
let first_loop_end_cond = builder.build_int_compare(
IntPredicate::ULT,
next_first_loop_index,
first_list_len,
"loopcond",
);
let after_first_loop_bb =
ctx.append_basic_block(parent, "after_first_loop");
builder.build_conditional_branch(
first_loop_end_cond,
first_loop_bb,
after_first_loop_bb,
);
builder.position_at_end(after_first_loop_bb);
}
// Reset the index variable to 0
builder.build_store(index_alloca, ctx.i64_type().const_int(0, false));
// SECOND LOOP
{
let second_loop_bb =
ctx.append_basic_block(parent, "second_list_append_loop");
builder.build_unconditional_branch(second_loop_bb);
builder.position_at_end(second_loop_bb);
let curr_second_index = builder
.build_load(index_alloca, index_name)
.into_int_value();
let second_list_ptr =
load_list_ptr(builder, second_list_wrapper, ptr_type);
// The pointer to the element in the second list
let second_list_elem_ptr = unsafe {
builder.build_in_bounds_gep(
second_list_ptr,
&[curr_second_index],
"load_index",
)
};
// The pointer to the element in the combined list.
// Note that the pointer does not start at the index
// 0, it starts at the index of first_list_len. In that
// sense it is "offset".
let offset_combined_list_elem_ptr = unsafe {
builder.build_in_bounds_gep(
combined_list_ptr,
&[first_list_len],
"elem",
)
};
// The pointer to the element from the second list
// in the combined list
let combined_list_elem_ptr = unsafe {
builder.build_in_bounds_gep(
offset_combined_list_elem_ptr,
&[curr_second_index],
"load_index_combined_list",
)
};
let second_list_elem =
builder.build_load(second_list_elem_ptr, "get_elem");
// Mutate the new array in-place to change the element.
builder.build_store(combined_list_elem_ptr, second_list_elem);
// #index = #index + 1
let next_second_index = builder.build_int_add(
curr_second_index,
ctx.i64_type().const_int(1, false),
"increment_index",
);
builder.build_store(index_alloca, next_second_index);
// #index < second_list_len
let second_loop_end_cond = builder.build_int_compare(
IntPredicate::ULT,
next_second_index,
second_list_len,
"loopcond",
);
let after_second_loop_bb =
ctx.append_basic_block(parent, "after_second_loop");
builder.build_conditional_branch(
second_loop_end_cond,
second_loop_bb,
after_second_loop_bb,
);
builder.position_at_end(after_second_loop_bb);
let ptr_bytes = env.ptr_bytes;
let int_type = ptr_int(ctx, ptr_bytes);
let ptr_as_int = builder.build_ptr_to_int(
combined_list_ptr,
int_type,
"list_cast_ptr",
);
let struct_type = collection(ctx, ptr_bytes);
let mut struct_val;
// Store the pointer
struct_val = builder
.build_insert_value(
struct_type.get_undef(),
ptr_as_int,
Builtin::WRAPPER_PTR,
"insert_ptr",
)
.unwrap();
// Store the length
struct_val = builder
.build_insert_value(
struct_val,
combined_list_len,
Builtin::WRAPPER_LEN,
"insert_len",
)
.unwrap();
builder.build_bitcast(
struct_val.into_struct_value(),
collection(ctx, ptr_bytes),
"cast_collection",
)
}
};
build_basic_phi2(
env,
parent,
second_list_length_comparison,
if_second_list_is_not_empty,
if_second_list_is_empty,
BasicTypeEnum::StructType(collection(ctx, env.ptr_bytes)),
)
}
_ => {
unreachable!(
"Invalid List layout for second input list of List.append: {:?}",
second_list_layout
);
}
}
};
let if_first_list_is_empty = || {
match second_list_layout {
Layout::Builtin(Builtin::EmptyList) => empty_list(env),
Layout::Builtin(Builtin::List(elem_layout)) => {
// second_list_len > 0
// We do this check to avoid allocating memory. If the second input
// list is empty, then we can just return the first list cloned
let second_list_length_comparison =
list_is_not_empty(builder, ctx, second_list_len);
let build_second_list_then = || {
let elem_type =
basic_type_from_layout(env.arena, ctx, elem_layout, env.ptr_bytes);
let ptr_type = get_ptr_type(&elem_type, AddressSpace::Generic);
let (new_wrapper, _) = clone_nonempty_list(
env,
second_list_len,
load_list_ptr(builder, second_list_wrapper, ptr_type),
elem_layout,
);
BasicValueEnum::StructValue(new_wrapper)
};
let build_second_list_else = || empty_list(env);
build_basic_phi2(
env,
parent,
second_list_length_comparison,
build_second_list_then,
build_second_list_else,
BasicTypeEnum::StructType(collection(ctx, env.ptr_bytes)),
)
}
_ => {
unreachable!(
"Invalid List layout for second input list of List.append: {:?}",
second_list_layout
);
}
}
};
build_basic_phi2(
env,
parent,
first_list_length_comparison,
if_first_list_is_not_empty,
if_first_list_is_empty,
BasicTypeEnum::StructType(collection(ctx, env.ptr_bytes)),
)
}
_ => {
unreachable!(
"Invalid List layout for first list in List.append : {:?}",
first_list_layout
);
}
}
}
fn build_float_binop<'a, 'ctx, 'env>( fn build_float_binop<'a, 'ctx, 'env>(
env: &Env<'a, 'ctx, 'env>, env: &Env<'a, 'ctx, 'env>,
lhs: FloatValue<'ctx>, lhs: FloatValue<'ctx>,

View file

@ -125,6 +125,107 @@ mod gen_list {
assert_evals_to!("List.reverse []", &[], &'static [i64]); assert_evals_to!("List.reverse []", &[], &'static [i64]);
} }
#[test]
fn list_append() {
assert_evals_to!("List.append [] []", &[], &'static [i64]);
assert_evals_to!(
indoc!(
r#"
firstList : List Int
firstList =
[]
secondList : List Int
secondList =
[]
List.append firstList secondList
"#
),
&[],
&'static [i64]
);
assert_evals_to!("List.append [ 12, 13 ] []", &[12, 13], &'static [i64]);
assert_evals_to!(
"List.append [ 34, 43 ] [ 64, 55, 66 ]",
&[34, 43, 64, 55, 66],
&'static [i64]
);
assert_evals_to!("List.append [] [ 23, 24 ]", &[23, 24], &'static [i64]);
assert_evals_to!(
"List.append [ 1, 2 ] [ 3, 4 ]",
&[1, 2, 3, 4],
&'static [i64]
);
}
fn assert_append_worked(num_elems1: i64, num_elems2: i64) {
let vec1: Vec<i64> = (0..num_elems1)
.map(|i| 12345 % (i + num_elems1 + num_elems2 + 1))
.collect();
let vec2: Vec<i64> = (0..num_elems2)
.map(|i| 54321 % (i + num_elems1 + num_elems2 + 1))
.collect();
let slice_str1 = format!("{:?}", vec1);
let slice_str2 = format!("{:?}", vec2);
let mut expected = vec1;
expected.extend(vec2);
let expected_slice: &[i64] = expected.as_ref();
assert_evals_to!(
&format!("List.append {} {}", slice_str1, slice_str2),
expected_slice,
&'static [i64]
);
}
#[test]
fn list_append_empty_list() {
assert_append_worked(0, 0);
assert_append_worked(1, 0);
assert_append_worked(2, 0);
assert_append_worked(3, 0);
assert_append_worked(4, 0);
assert_append_worked(7, 0);
assert_append_worked(8, 0);
assert_append_worked(9, 0);
assert_append_worked(25, 0);
assert_append_worked(150, 0);
assert_append_worked(0, 1);
assert_append_worked(0, 2);
assert_append_worked(0, 3);
assert_append_worked(0, 4);
assert_append_worked(0, 7);
assert_append_worked(0, 8);
assert_append_worked(0, 9);
assert_append_worked(0, 25);
assert_append_worked(0, 150);
}
#[test]
fn list_append_nonempty_lists() {
assert_append_worked(1, 1);
assert_append_worked(1, 2);
assert_append_worked(1, 3);
assert_append_worked(2, 3);
assert_append_worked(2, 1);
assert_append_worked(2, 2);
assert_append_worked(3, 1);
assert_append_worked(3, 2);
assert_append_worked(2, 3);
assert_append_worked(3, 3);
assert_append_worked(4, 4);
assert_append_worked(150, 150);
assert_append_worked(129, 350);
assert_append_worked(350, 129);
}
#[test] #[test]
fn empty_list_len() { fn empty_list_len() {
assert_evals_to!("List.len []", 0, usize); assert_evals_to!("List.len []", 0, usize);

View file

@ -62,7 +62,7 @@ mod test_load {
subs: &mut Subs, subs: &mut Subs,
home: ModuleId, home: ModuleId,
def: &Def, def: &Def,
expected_types: &HashMap<&str, &str>, expected_types: &mut HashMap<&str, &str>,
) { ) {
for (symbol, expr_var) in &def.pattern_vars { for (symbol, expr_var) in &def.pattern_vars {
let content = subs.get(*expr_var).content; let content = subs.get(*expr_var).content;
@ -72,34 +72,30 @@ mod test_load {
let actual_str = content_to_string(content, subs, home, &interns); let actual_str = content_to_string(content, subs, home, &interns);
let fully_qualified = symbol.fully_qualified(&interns, home).to_string(); let fully_qualified = symbol.fully_qualified(&interns, home).to_string();
let expected_type = expected_types let expected_type = expected_types
.get(fully_qualified.as_str()) .remove(fully_qualified.as_str())
.unwrap_or_else(|| { .unwrap_or_else(|| {
panic!("Defs included an unexpected symbol: {:?}", fully_qualified) panic!("Defs included an unexpected symbol: {:?}", fully_qualified)
}); });
assert_eq!((&symbol, expected_type), (&symbol, &actual_str.as_str())); assert_eq!((&symbol, expected_type), (&symbol, actual_str.as_str()));
} }
} }
fn expect_types(mut loaded_module: LoadedModule, expected_types: HashMap<&str, &str>) { fn expect_types(mut loaded_module: LoadedModule, mut expected_types: HashMap<&str, &str>) {
let home = loaded_module.module_id; let home = loaded_module.module_id;
let mut subs = loaded_module.solved.into_inner(); let mut subs = loaded_module.solved.into_inner();
assert_eq!(loaded_module.can_problems, Vec::new()); assert_eq!(loaded_module.can_problems, Vec::new());
assert_eq!(loaded_module.type_problems, Vec::new()); assert_eq!(loaded_module.type_problems, Vec::new());
let mut num_decls = 0;
for decl in loaded_module.declarations_by_id.remove(&home).unwrap() { for decl in loaded_module.declarations_by_id.remove(&home).unwrap() {
num_decls += 1;
match decl { match decl {
Declare(def) => expect_def( Declare(def) => expect_def(
&loaded_module.interns, &loaded_module.interns,
&mut subs, &mut subs,
home, home,
&def, &def,
&expected_types, &mut expected_types,
), ),
DeclareRec(defs) => { DeclareRec(defs) => {
for def in defs { for def in defs {
@ -108,7 +104,7 @@ mod test_load {
&mut subs, &mut subs,
home, home,
&def, &def,
&expected_types, &mut expected_types,
); );
} }
} }
@ -119,7 +115,11 @@ mod test_load {
}; };
} }
assert_eq!(expected_types.len(), num_decls); assert_eq!(
expected_types,
HashMap::default(),
"Some expected types were not found in the defs"
);
} }
// TESTS // TESTS

View file

@ -57,7 +57,7 @@ mod test_uniq_load {
subs: &mut Subs, subs: &mut Subs,
home: ModuleId, home: ModuleId,
def: &Def, def: &Def,
expected_types: &HashMap<&str, &str>, expected_types: &mut HashMap<&str, &str>,
) { ) {
for (symbol, expr_var) in &def.pattern_vars { for (symbol, expr_var) in &def.pattern_vars {
let content = subs.get(*expr_var).content; let content = subs.get(*expr_var).content;
@ -67,34 +67,30 @@ mod test_uniq_load {
let actual_str = content_to_string(content, subs, home, &interns); let actual_str = content_to_string(content, subs, home, &interns);
let fully_qualified = symbol.fully_qualified(&interns, home).to_string(); let fully_qualified = symbol.fully_qualified(&interns, home).to_string();
let expected_type = expected_types let expected_type = expected_types
.get(fully_qualified.as_str()) .remove(fully_qualified.as_str())
.unwrap_or_else(|| { .unwrap_or_else(|| {
panic!("Defs included an unexpected symbol: {:?}", fully_qualified) panic!("Defs included an unexpected symbol: {:?}", fully_qualified)
}); });
assert_eq!((&symbol, expected_type), (&symbol, &actual_str.as_str())); assert_eq!((&symbol, expected_type), (&symbol, actual_str.as_str()));
} }
} }
fn expect_types(mut loaded_module: LoadedModule, expected_types: HashMap<&str, &str>) { fn expect_types(mut loaded_module: LoadedModule, mut expected_types: HashMap<&str, &str>) {
let home = loaded_module.module_id; let home = loaded_module.module_id;
let mut subs = loaded_module.solved.into_inner(); let mut subs = loaded_module.solved.into_inner();
assert_eq!(loaded_module.can_problems, Vec::new()); assert_eq!(loaded_module.can_problems, Vec::new());
assert_eq!(loaded_module.type_problems, Vec::new()); assert_eq!(loaded_module.type_problems, Vec::new());
let mut num_decls = 0;
for decl in loaded_module.declarations_by_id.remove(&home).unwrap() { for decl in loaded_module.declarations_by_id.remove(&home).unwrap() {
num_decls += 1;
match decl { match decl {
Declare(def) => expect_def( Declare(def) => expect_def(
&loaded_module.interns, &loaded_module.interns,
&mut subs, &mut subs,
home, home,
&def, &def,
&expected_types, &mut expected_types,
), ),
DeclareRec(defs) => { DeclareRec(defs) => {
for def in defs { for def in defs {
@ -103,7 +99,7 @@ mod test_uniq_load {
&mut subs, &mut subs,
home, home,
&def, &def,
&expected_types, &mut expected_types,
); );
} }
} }
@ -114,7 +110,11 @@ mod test_uniq_load {
}; };
} }
assert_eq!(expected_types.len(), num_decls); assert_eq!(
expected_types,
HashMap::default(),
"Some expected types were not found in the defs"
);
} }
// TESTS // TESTS

View file

@ -10,6 +10,7 @@ pub enum LowLevel {
ListSingle, ListSingle,
ListRepeat, ListRepeat,
ListReverse, ListReverse,
ListAppend,
ListPush, ListPush,
NumAdd, NumAdd,
NumSub, NumSub,

View file

@ -658,6 +658,7 @@ define_builtins! {
12 LIST_SINGLE: "single" 12 LIST_SINGLE: "single"
13 LIST_REPEAT: "repeat" 13 LIST_REPEAT: "repeat"
14 LIST_REVERSE: "reverse" 14 LIST_REVERSE: "reverse"
15 LIST_APPEND: "append"
} }
5 RESULT: "Result" => { 5 RESULT: "Result" => {
0 RESULT_RESULT: "Result" imported // the Result.Result type alias 0 RESULT_RESULT: "Result" imported // the Result.Result type alias

View file

@ -131,10 +131,8 @@ fn run_event_loop() -> Result<(), Box<dyn Error>> {
match ch { match ch {
'\u{8}' | '\u{7f}' => { '\u{8}' | '\u{7f}' => {
// In Linux, we get a '\u{8}' when you press backspace, // In Linux, we get a '\u{8}' when you press backspace,
// but in macOS we get '\u{7f}'. In both, we // but in macOS we get '\u{7f}'.
// get a Back keydown event. Therefore, we use the text_state.pop();
// Back keydown event and ignore these, resulting
// in a system that works in both Linux and macOS.
} }
'\u{e000}'..='\u{f8ff}' '\u{e000}'..='\u{f8ff}'
| '\u{f0000}'..='\u{ffffd}' | '\u{f0000}'..='\u{ffffd}'
@ -152,12 +150,7 @@ fn run_event_loop() -> Result<(), Box<dyn Error>> {
.. ..
} => { } => {
if let Some(virtual_keycode) = input.virtual_keycode { if let Some(virtual_keycode) = input.virtual_keycode {
handle_keydown( handle_keydown(input.state, virtual_keycode, keyboard_modifiers);
&mut text_state,
input.state,
virtual_keycode,
keyboard_modifiers,
);
} }
} }
winit::event::Event::WindowEvent { winit::event::Event::WindowEvent {
@ -228,7 +221,6 @@ fn run_event_loop() -> Result<(), Box<dyn Error>> {
} }
fn handle_keydown( fn handle_keydown(
text_state: &mut String,
elem_state: ElementState, elem_state: ElementState,
virtual_keycode: VirtualKeyCode, virtual_keycode: VirtualKeyCode,
_modifiers: ModifiersState, _modifiers: ModifiersState,
@ -240,12 +232,6 @@ fn handle_keydown(
} }
match virtual_keycode { match virtual_keycode {
Back => {
// Backspace deletes a character.
// In Linux, we get a Unicode character for backspace events
// (which is handled elsewhere), but on macOS we only get one of these.
text_state.pop();
}
Copy => { Copy => {
todo!("copy"); todo!("copy");
} }

View file

@ -1,6 +1,6 @@
# Hello, World! # Hello, World!
To run: To run, `cd` into this directory and run:
```bash ```bash
$ cargo run run Hello.roc $ cargo run run Hello.roc
@ -42,4 +42,3 @@ Using this glue code, the Roc compiler can generate C header files describing th
boundary. This not only gets us host compatibility with C compilers, but also boundary. This not only gets us host compatibility with C compilers, but also
Rust FFI for free, because [`rust-bindgen`](https://github.com/rust-lang/rust-bindgen) Rust FFI for free, because [`rust-bindgen`](https://github.com/rust-lang/rust-bindgen)
generates correct Rust FFI bindings from C headers. generates correct Rust FFI bindings from C headers.