Merge pull request #330 from rtfeldman/list-first

List.first for nonempty lists
This commit is contained in:
Richard Feldman 2020-04-26 10:09:15 -04:00 committed by GitHub
commit b40ca2d8f1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 409 additions and 30 deletions

View File

@ -371,9 +371,9 @@ pub fn types() -> MutMap<Symbol, (SolvedType, Region)> {
// List module
// get : List elem, Int -> Result elem [ IndexOutOfBounds ]*
// get : List elem, Int -> Result elem [ OutOfBounds ]*
let index_out_of_bounds = SolvedType::TagUnion(
vec![(TagName::Global("IndexOutOfBounds".into()), vec![])],
vec![(TagName::Global("OutOfBounds".into()), vec![])],
Box::new(SolvedType::Wildcard),
);
@ -386,7 +386,7 @@ pub fn types() -> MutMap<Symbol, (SolvedType, Region)> {
);
add_type(
Symbol::LIST_GET_UNSAFE, // TODO remove this once we can code gen Result
Symbol::LIST_GET_UNSAFE,
SolvedType::Func(
vec![list_type(flex(TVAR1)), int_type()],
Box::new(flex(TVAR1)),

View File

@ -450,9 +450,9 @@ pub fn types() -> MutMap<Symbol, (SolvedType, Region)> {
unique_function(vec![list_type(UVAR1, TVAR1)], int_type(UVAR2)),
);
// get : List a, Int -> Result a [ IndexOutOfBounds ]*
// get : List a, Int -> Result a [ OutOfBounds ]*
let index_out_of_bounds = SolvedType::TagUnion(
vec![(TagName::Global("IndexOutOfBounds".into()), vec![])],
vec![(TagName::Global("OutOfBounds".into()), vec![])],
Box::new(SolvedType::Wildcard),
);

View File

@ -0,0 +1,233 @@
use crate::def::Def;
use crate::expr::Expr;
use crate::expr::Recursive;
use roc_collections::all::SendMap;
use roc_module::ident::TagName;
use roc_module::symbol::Symbol;
use roc_parse::operator::CalledVia;
use roc_region::all::{Located, Region};
use roc_types::subs::{VarStore, Variable};
/// Some builtins cannot be constructed in code gen alone, and need to be defined
/// as separate Roc defs. For example, List.get has this type:
///
/// List.get : List elem, Int -> Result elem [ OutOfBounds ]*
///
/// Because this returns an open tag union for its Err type, it's not possible
/// for code gen to return a hardcoded value for OutOfBounds. For example,
/// if this Result unifies to [ Foo, OutOfBounds ] then OutOfBOunds will
/// get assigned the number 1 (because Foo got 0 alphabetically), whereas
/// if it unifies to [ OutOfBounds, Qux ] then OutOfBounds will get the number 0.
///
/// Getting these numbers right requires having List.get participate in the
/// normal type-checking and monomorphization processes. As such, this function
/// returns a normal def for List.get, which performs a bounds check and then
/// delegates to the compiler-internal List.getUnsafe function to do the actual
/// lookup (if the bounds check passed). That internal function is hardcoded in code gen,
/// which works fine because it doesn't involve any open tag unions.
pub fn builtin_defs(var_store: &VarStore) -> Vec<Def> {
vec![/*list_get(var_store),*/ list_first(var_store)]
}
/// List.get : List elem, Int -> Result elem [ OutOfBounds ]*
// fn list_get(var_store: &VarStore) -> Def {
// use crate::expr::Expr::*;
// use crate::pattern::Pattern::*;
// let args = vec![
// (
// var_store.fresh(),
// no_region(Identifier(Symbol::LIST_GET_ARG_LIST)),
// ),
// (
// var_store.fresh(),
// no_region(Identifier(Symbol::LIST_GET_ARG_INDEX)),
// ),
// ];
// // Perform a bounds check. If it passes, delegate to List.#getUnsafe
// let body = If {
// cond_var: var_store.fresh(),
// branch_var: var_store.fresh(),
// branches: vec![(
// // if-condition
// no_region(
// // index < List.len list
// call(
// Symbol::NUM_LT,
// vec![
// Var(Symbol::LIST_GET_ARG_INDEX),
// call(
// Symbol::LIST_LEN,
// vec![Var(Symbol::LIST_GET_ARG_LIST)],
// var_store,
// ),
// ],
// var_store,
// ),
// ),
// // then-branch
// no_region(
// // Ok
// tag(
// "Ok",
// vec![
// // List.getUnsafe list index
// Call(
// Box::new((
// var_store.fresh(),
// no_region(Var(Symbol::LIST_GET_UNSAFE)),
// var_store.fresh(),
// )),
// vec![
// (var_store.fresh(), no_region(Var(Symbol::LIST_GET_ARG_LIST))),
// (
// var_store.fresh(),
// no_region(Var(Symbol::LIST_GET_ARG_INDEX)),
// ),
// ],
// CalledVia::Space,
// ),
// ],
// var_store,
// ),
// ),
// )],
// final_else: Box::new(
// // else-branch
// no_region(
// // Err
// tag(
// "Err",
// vec![tag("OutOfBounds", Vec::new(), var_store)],
// var_store,
// ),
// ),
// ),
// };
// let expr = Closure(
// var_store.fresh(),
// Symbol::LIST_GET,
// Recursive::NotRecursive,
// args,
// Box::new((no_region(body), var_store.fresh())),
// );
// Def {
// loc_pattern: no_region(Identifier(Symbol::LIST_GET)),
// loc_expr: no_region(expr),
// expr_var: var_store.fresh(),
// pattern_vars: SendMap::default(),
// annotation: None,
// }
// }
/// List.first : List elem -> Result elem [ ListWasEmpty ]*
fn list_first(var_store: &VarStore) -> Def {
use crate::expr::Expr::*;
use crate::pattern::Pattern::*;
let args = vec![(
var_store.fresh(),
no_region(Identifier(Symbol::LIST_FIRST_ARG)),
)];
// Perform a bounds check. If it passes, delegate to List.getUnsafe.
let body = If {
// TODO Use "when" instead of "if" so that we can have False be the first branch.
// We want that for branch prediction; usually we expect the list to be nonempty.
cond_var: var_store.fresh(),
branch_var: var_store.fresh(),
branches: vec![(
// if-condition
no_region(
// List.isEmpty list
call(
Symbol::LIST_IS_EMPTY,
vec![Var(Symbol::LIST_FIRST_ARG)],
var_store,
),
),
// list was empty
no_region(
// Err ListWasEmpty
tag(
"Err",
vec![tag("ListWasEmpty", Vec::new(), var_store)],
var_store,
),
),
)],
final_else: Box::new(
// list was not empty
no_region(
// Ok (List.#getUnsafe list 0)
tag(
"Ok",
vec![
// List.#getUnsafe list 0
call(
Symbol::LIST_GET_UNSAFE,
vec![(Var(Symbol::LIST_FIRST_ARG)), (Int(var_store.fresh(), 0))],
var_store,
),
],
var_store,
),
),
),
};
let expr = Closure(
var_store.fresh(),
Symbol::LIST_FIRST,
Recursive::NotRecursive,
args,
Box::new((no_region(body), var_store.fresh())),
);
Def {
loc_pattern: no_region(Identifier(Symbol::LIST_FIRST)),
loc_expr: no_region(expr),
expr_var: var_store.fresh(),
pattern_vars: SendMap::default(),
annotation: None,
}
}
#[inline(always)]
fn no_region<T>(value: T) -> Located<T> {
Located {
region: Region::zero(),
value,
}
}
#[inline(always)]
fn tag(name: &'static str, args: Vec<Expr>, var_store: &VarStore) -> Expr {
Expr::Tag {
variant_var: var_store.fresh(),
ext_var: var_store.fresh(),
name: TagName::Global(name.into()),
arguments: args
.into_iter()
.map(|expr| (var_store.fresh(), no_region(expr)))
.collect::<Vec<(Variable, Located<Expr>)>>(),
}
}
#[inline(always)]
fn call(symbol: Symbol, args: Vec<Expr>, var_store: &VarStore) -> Expr {
Expr::Call(
Box::new((
var_store.fresh(),
no_region(Expr::Var(symbol)),
var_store.fresh(),
)),
args.into_iter()
.map(|expr| (var_store.fresh(), no_region(expr)))
.collect::<Vec<(Variable, Located<Expr>)>>(),
CalledVia::Space,
)
}

View File

@ -11,6 +11,7 @@
// re-enable this when working on performance optimizations than have it block PRs.
#![allow(clippy::large_enum_variant)]
pub mod annotation;
pub mod builtins;
pub mod constraint;
pub mod def;
pub mod env;

View File

@ -303,9 +303,35 @@ mod gen_builtins {
#[test]
fn head_int_list() {
assert_evals_to!("List.getUnsafe [ 12, 9, 6, 3 ] 0", 12, i64);
assert_evals_to!(
indoc!(
r#"
when List.first [ 12, 9, 6, 3 ] is
Ok val -> val
Err _ -> -1
"#
),
12,
i64
);
}
// TODO FIXME this should work, but doesn't!
// #[test]
// fn head_empty_list() {
// assert_evals_to!(
// indoc!(
// r#"
// when List.first [] is
// Ok val -> val
// Err _ -> -1
// "#
// ),
// -1,
// i64
// );
// }
#[test]
fn get_int_list() {
assert_evals_to!("List.getUnsafe [ 12, 9, 6 ] 1", 9, i64);

View File

@ -173,6 +173,42 @@ mod gen_primitives {
);
}
#[test]
fn when_two_element_tag_first() {
assert_evals_to!(
indoc!(
r#"
x : [A Int, B Int]
x = A 0x2
when x is
A v -> v
B v -> v
"#
),
2,
i64
);
}
#[test]
fn when_two_element_tag_second() {
assert_evals_to!(
indoc!(
r#"
x : [A Int, B Int]
x = B 0x3
when x is
A v -> v
B v -> v
"#
),
3,
i64
);
}
#[test]
fn gen_when_one_branch() {
assert_evals_to!(

View File

@ -4,11 +4,25 @@ macro_rules! assert_llvm_evals_to {
let target = target_lexicon::Triple::host();
let ptr_bytes = target.pointer_width().unwrap().bytes() as u32;
let arena = Bump::new();
let CanExprOut { loc_expr, var_store, var, constraint, home, interns, .. } = can_expr($src);
let CanExprOut { loc_expr, var_store, var, constraint, home, interns, problems, .. } = can_expr($src);
let errors = problems.into_iter().filter(|problem| {
use roc_problem::can::Problem::*;
// Ignore "unused" problems
match problem {
UnusedDef(_, _) | UnusedArgument(_, _, _) | UnusedImport(_, _) => false,
_ => true,
}
}).collect::<Vec<roc_problem::can::Problem>>();
assert_eq!(errors, Vec::new(), "Encountered errors: {:?}", errors);
let subs = Subs::new(var_store.into());
let mut unify_problems = Vec::new();
let (content, mut subs) = infer_expr(subs, &mut unify_problems, &constraint, var);
assert_eq!(unify_problems, Vec::new(), "Encountered type mismatches: {:?}", unify_problems);
let context = Context::create();
let module = roc_gen::llvm::build::module_from_builtins(&context, "app");
let builder = context.create_builder();
@ -142,7 +156,18 @@ macro_rules! assert_opt_evals_to {
let arena = Bump::new();
let target = target_lexicon::Triple::host();
let ptr_bytes = target.pointer_width().unwrap().bytes() as u32;
let (loc_expr, _output, _problems, subs, var, constraint, home, interns) = uniq_expr($src);
let (loc_expr, _output, problems, subs, var, constraint, home, interns) = uniq_expr($src);
let errors = problems.into_iter().filter(|problem| {
use roc_problem::can::Problem::*;
// Ignore "unused" problems
match problem {
UnusedDef(_, _) | UnusedArgument(_, _, _) | UnusedImport(_, _) => false,
_ => true,
}
}).collect::<Vec<roc_problem::can::Problem>>();
assert_eq!(errors, Vec::new(), "Encountered errors: {:?}", errors);
let mut unify_problems = Vec::new();
let (content, mut subs) = infer_expr(subs, &mut unify_problems, &constraint, var);
@ -278,7 +303,18 @@ macro_rules! assert_opt_evals_to {
macro_rules! emit_expr {
($src:expr, $expected:expr, $ty:ty, $transform:expr) => {
let arena = Bump::new();
let (loc_expr, _output, _problems, subs, var, constraint, home, interns) = uniq_expr($src);
let (loc_expr, _output, problems, subs, var, constraint, home, interns) = uniq_expr($src);
let errors = problems.into_iter().filter(|problem| {
use roc_problem::can::Problem::*;
// Ignore "unused" problems
match problem {
UnusedDef(_, _) | UnusedArgument(_, _, _) | UnusedImport(_, _) => false,
_ => true,
}
}).collect::<Vec<roc_problem::can::Problem>>();
assert_eq!(errors, Vec::new(), "Encountered errors: {:?}", errors);
let mut unify_problems = Vec::new();
let (content, mut subs) = infer_expr(subs, &mut unify_problems, &constraint, var);

View File

@ -207,7 +207,6 @@ pub struct CanExprOut {
pub constraint: Constraint,
}
#[allow(dead_code)]
pub fn can_expr_with(arena: &Bump, home: ModuleId, expr_str: &str) -> CanExprOut {
let loc_expr = parse_loc_with(&arena, expr_str).unwrap_or_else(|e| {
panic!(
@ -241,6 +240,29 @@ pub fn can_expr_with(arena: &Bump, home: ModuleId, expr_str: &str) -> CanExprOut
&loc_expr.value,
);
let mut with_builtins = loc_expr.value;
// Add builtin defs (e.g. List.get) directly to the canonical Expr,
// since we aren't using modules here.
let builtin_defs = roc_can::builtins::builtin_defs(&var_store);
for def in builtin_defs {
with_builtins = Expr::LetNonRec(
Box::new(def),
Box::new(Located {
region: Region::zero(),
value: with_builtins,
}),
var_store.fresh(),
SendMap::default(),
);
}
let loc_expr = Located {
region: loc_expr.region,
value: with_builtins,
};
let constraint = constrain_expr(
&roc_constrain::expr::Env {
rigids: ImMap::default(),

View File

@ -564,7 +564,7 @@ macro_rules! define_builtins {
};
}
// NOTE: Some of these builtins have a # at the beginning of their names.
// NOTE: Some of these builtins have a # in their names.
// This is because they are for compiler use only, and should not cause
// namespace conflicts with userspace!
define_builtins! {
@ -633,15 +633,19 @@ define_builtins! {
1 LIST_AT_LIST: "@List" // the List.@List private tag
2 LIST_IS_EMPTY: "isEmpty"
3 LIST_GET: "get"
4 LIST_SET: "set"
5 LIST_SET_IN_PLACE: "#setInPlace"
6 LIST_PUSH: "push"
7 LIST_MAP: "map"
8 LIST_LEN: "len"
9 LIST_FOLDL: "foldl"
10 LIST_FOLDR: "foldr"
11 LIST_GET_UNSAFE: "getUnsafe" // TODO remove once we can code gen Result
12 LIST_CONCAT: "concat"
4 LIST_GET_ARG_LIST: "get#list"
5 LIST_GET_ARG_INDEX: "get#index"
6 LIST_SET: "set"
7 LIST_SET_IN_PLACE: "#setInPlace"
8 LIST_PUSH: "push"
9 LIST_MAP: "map"
10 LIST_LEN: "len"
11 LIST_FOLDL: "foldl"
12 LIST_FOLDR: "foldr"
13 LIST_GET_UNSAFE: "getUnsafe"
14 LIST_CONCAT: "concat"
15 LIST_FIRST: "first"
16 LIST_FIRST_ARG: "first#list"
}
7 RESULT: "Result" => {
0 RESULT_RESULT: "Result" imported // the Result.Result type alias

View File

@ -60,7 +60,10 @@ impl<'a> Layout<'a> {
match content {
var @ FlexVar(_) | var @ RigidVar(_) => {
panic!("Layout::from_content encountered an unresolved {:?}", var);
panic!(
"Layout::from_content encountered an unresolved {:?} - subs was {:?}",
var, subs
);
}
Structure(flat_type) => layout_from_flat_type(arena, flat_type, subs, pointer_size),
@ -501,7 +504,10 @@ fn layout_from_num_content<'a>(content: Content) -> Result<Layout<'a>, ()> {
match content {
var @ FlexVar(_) | var @ RigidVar(_) => {
panic!("Layout::from_content encountered an unresolved {:?}", var);
panic!(
"Layout::from_num_content encountered an unresolved {:?}",
var
);
}
Structure(Apply(symbol, args)) => match symbol {
Symbol::INT_INTEGER => Ok(Layout::Builtin(Builtin::Int64)),

View File

@ -401,7 +401,7 @@ pub fn variable_usage(con: &Constraint) -> (SeenVariables, Vec<Variable>) {
variable_usage_help(con, &mut declared, &mut used);
// ..= because there is an extra undeclared variable that contains the type of the full expression
for i in 0..=Variable::RESERVED {
for i in 0..=Variable::NUM_RESERVED_VARS {
used.remove(unsafe { &Variable::unsafe_test_debug_variable(i as u32) });
}

View File

@ -2194,6 +2194,18 @@ mod test_solve {
);
}
#[test]
fn solve_list_get() {
infer_eq_without_problem(
indoc!(
r#"
List.get [ "a" ] 0
"#
),
"Result Str [ OutOfBounds ]*",
);
}
#[test]
fn type_more_general_than_signature() {
infer_eq_without_problem(
@ -2292,7 +2304,7 @@ mod test_solve {
List.get [ 10, 9, 8, 7 ] 1
"#
),
"Result (Num *) [ IndexOutOfBounds ]*",
"Result (Num *) [ OutOfBounds ]*",
);
}

View File

@ -1896,7 +1896,7 @@ mod test_uniq_solve {
|> List.get 2
"#
),
"Attr * (Result (Attr * (Num (Attr * *))) (Attr * [ IndexOutOfBounds ]*))",
"Attr * (Result (Attr * (Num (Attr * *))) (Attr * [ OutOfBounds ]*))",
);
}
@ -1984,7 +1984,7 @@ mod test_uniq_solve {
{ p, q }
"#
),
"Attr * (Attr * (List (Attr Shared a)) -> Attr * { p : (Attr * (Result (Attr Shared a) (Attr * [ IndexOutOfBounds ]*))), q : (Attr * (Result (Attr Shared a) (Attr * [ IndexOutOfBounds ]*))) })"
"Attr * (Attr * (List (Attr Shared a)) -> Attr * { p : (Attr * (Result (Attr Shared a) (Attr * [ OutOfBounds ]*))), q : (Attr * (Result (Attr Shared a) (Attr * [ OutOfBounds ]*))) })"
);
}
@ -2097,8 +2097,8 @@ mod test_uniq_solve {
reverse
"#
),
"Attr * (Attr * (List (Attr (a | b) c)) -> Attr (* | a | b) (List (Attr b c)))",
//"Attr * (Attr * (List (Attr (a | b) c)) -> Attr (* | a | b) (List (Attr a c)))",
// "Attr * (Attr * (List (Attr (a | b) c)) -> Attr (* | a | b) (List (Attr b c)))",
"Attr * (Attr * (List (Attr (a | b) c)) -> Attr (* | a | b) (List (Attr a c)))",
);
}

View File

@ -152,11 +152,14 @@ impl Variable {
pub const EMPTY_RECORD: Variable = Variable(1);
pub const EMPTY_TAG_UNION: Variable = Variable(2);
// Builtins
const BOOL_ENUM: Variable = Variable(3);
pub const BOOL: Variable = Variable(4);
pub const RESERVED: usize = 5;
pub const LIST_GET: Variable = Variable(5);
const FIRST_USER_SPACE_VAR: Variable = Variable(Self::RESERVED as u32);
pub const NUM_RESERVED_VARS: usize = 6;
const FIRST_USER_SPACE_VAR: Variable = Variable(Self::NUM_RESERVED_VARS as u32);
/// # Safety
///