mirror of
https://github.com/roc-lang/roc.git
synced 2025-10-02 00:01:16 +00:00
Merge branch 'trunk' of github.com:rtfeldman/roc into shadowing-report
This commit is contained in:
commit
6e00e6f4a2
14 changed files with 654 additions and 27 deletions
|
@ -688,7 +688,7 @@ fn canonicalize_when_branch<'a>(
|
||||||
env: &mut Env<'a>,
|
env: &mut Env<'a>,
|
||||||
var_store: &VarStore,
|
var_store: &VarStore,
|
||||||
scope: &mut Scope,
|
scope: &mut Scope,
|
||||||
region: Region,
|
_region: Region,
|
||||||
branch: &'a ast::WhenBranch<'a>,
|
branch: &'a ast::WhenBranch<'a>,
|
||||||
output: &mut Output,
|
output: &mut Output,
|
||||||
) -> (WhenBranch, References) {
|
) -> (WhenBranch, References) {
|
||||||
|
@ -721,7 +721,7 @@ fn canonicalize_when_branch<'a>(
|
||||||
None => None,
|
None => None,
|
||||||
Some(loc_expr) => {
|
Some(loc_expr) => {
|
||||||
let (can_guard, guard_branch_output) =
|
let (can_guard, guard_branch_output) =
|
||||||
canonicalize_expr(env, var_store, &mut scope, region, &loc_expr.value);
|
canonicalize_expr(env, var_store, &mut scope, loc_expr.region, &loc_expr.value);
|
||||||
|
|
||||||
branch_output.union(guard_branch_output);
|
branch_output.union(guard_branch_output);
|
||||||
Some(can_guard)
|
Some(can_guard)
|
||||||
|
|
|
@ -181,9 +181,10 @@ pub fn constrain_expr(
|
||||||
region,
|
region,
|
||||||
);
|
);
|
||||||
|
|
||||||
cons.push(con);
|
// ensure constraints are solved in this order, gives better errors
|
||||||
cons.push(fields_con);
|
cons.insert(0, fields_con);
|
||||||
cons.push(record_con);
|
cons.insert(1, con);
|
||||||
|
cons.insert(2, record_con);
|
||||||
|
|
||||||
exists(vars, And(cons))
|
exists(vars, And(cons))
|
||||||
}
|
}
|
||||||
|
@ -202,9 +203,12 @@ pub fn constrain_expr(
|
||||||
let list_elem_type = Type::Variable(*elem_var);
|
let list_elem_type = Type::Variable(*elem_var);
|
||||||
let mut constraints = Vec::with_capacity(1 + loc_elems.len());
|
let mut constraints = Vec::with_capacity(1 + loc_elems.len());
|
||||||
|
|
||||||
for loc_elem in loc_elems {
|
for (index, loc_elem) in loc_elems.iter().enumerate() {
|
||||||
let elem_expected =
|
let elem_expected = ForReason(
|
||||||
ForReason(Reason::ElemInList, list_elem_type.clone(), region);
|
Reason::ElemInList { index },
|
||||||
|
list_elem_type.clone(),
|
||||||
|
loc_elem.region,
|
||||||
|
);
|
||||||
let constraint =
|
let constraint =
|
||||||
constrain_expr(env, loc_elem.region, &loc_elem.value, elem_expected);
|
constrain_expr(env, loc_elem.region, &loc_elem.value, elem_expected);
|
||||||
|
|
||||||
|
@ -539,7 +543,11 @@ pub fn constrain_expr(
|
||||||
cond_type.clone(),
|
cond_type.clone(),
|
||||||
region,
|
region,
|
||||||
),
|
),
|
||||||
ForReason(Reason::WhenBranch { index }, branch_type.clone(), region),
|
ForReason(
|
||||||
|
Reason::WhenBranch { index },
|
||||||
|
branch_type.clone(),
|
||||||
|
when_branch.value.region,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
branch_cons.push(branch_con);
|
branch_cons.push(branch_con);
|
||||||
|
|
|
@ -596,9 +596,12 @@ pub fn constrain_expr(
|
||||||
let entry_type = Type::Variable(*elem_var);
|
let entry_type = Type::Variable(*elem_var);
|
||||||
let mut constraints = Vec::with_capacity(1 + loc_elems.len());
|
let mut constraints = Vec::with_capacity(1 + loc_elems.len());
|
||||||
|
|
||||||
for loc_elem in loc_elems.iter() {
|
for (index, loc_elem) in loc_elems.iter().enumerate() {
|
||||||
let elem_expected =
|
let elem_expected = Expected::ForReason(
|
||||||
Expected::ForReason(Reason::ElemInList, entry_type.clone(), region);
|
Reason::ElemInList { index },
|
||||||
|
entry_type.clone(),
|
||||||
|
region,
|
||||||
|
);
|
||||||
let constraint = constrain_expr(
|
let constraint = constrain_expr(
|
||||||
env,
|
env,
|
||||||
var_store,
|
var_store,
|
||||||
|
|
|
@ -14,6 +14,7 @@ use ven_pretty::{BoxAllocator, DocAllocator, DocBuilder, Render, RenderAnnotated
|
||||||
|
|
||||||
/// A textual report.
|
/// A textual report.
|
||||||
pub struct Report {
|
pub struct Report {
|
||||||
|
pub title: String,
|
||||||
pub filename: PathBuf,
|
pub filename: PathBuf,
|
||||||
pub text: ReportText,
|
pub text: ReportText,
|
||||||
}
|
}
|
||||||
|
@ -128,6 +129,7 @@ pub fn can_problem(filename: PathBuf, problem: Problem) -> Report {
|
||||||
};
|
};
|
||||||
|
|
||||||
Report {
|
Report {
|
||||||
|
title: "SYNTAX PROBLEM".to_string(),
|
||||||
filename,
|
filename,
|
||||||
text: Concat(texts),
|
text: Concat(texts),
|
||||||
}
|
}
|
||||||
|
@ -327,11 +329,11 @@ where
|
||||||
Url => {
|
Url => {
|
||||||
self.write_str("<")?;
|
self.write_str("<")?;
|
||||||
}
|
}
|
||||||
GlobalTag | PrivateTag | RecordField | Keyword => {
|
GlobalTag | PrivateTag | Keyword => {
|
||||||
self.write_str("`")?;
|
self.write_str("`")?;
|
||||||
}
|
}
|
||||||
CodeBlock | PlainText | LineNumber | Error | GutterBar | TypeVariable | Alias
|
CodeBlock | PlainText | LineNumber | Error | GutterBar | TypeVariable | Alias
|
||||||
| Module | Structure | Symbol | BinOp => {}
|
| RecordField | Module | Structure | Symbol | BinOp => {}
|
||||||
}
|
}
|
||||||
self.style_stack.push(*annotation);
|
self.style_stack.push(*annotation);
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -349,11 +351,11 @@ where
|
||||||
Url => {
|
Url => {
|
||||||
self.write_str(">")?;
|
self.write_str(">")?;
|
||||||
}
|
}
|
||||||
GlobalTag | PrivateTag | RecordField | Keyword => {
|
GlobalTag | PrivateTag | Keyword => {
|
||||||
self.write_str("`")?;
|
self.write_str("`")?;
|
||||||
}
|
}
|
||||||
CodeBlock | PlainText | LineNumber | Error | GutterBar | TypeVariable | Alias
|
CodeBlock | PlainText | LineNumber | Error | GutterBar | TypeVariable | Alias
|
||||||
| Module | Structure | Symbol | BinOp => {}
|
| RecordField | Module | Structure | Symbol | BinOp => {}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -74,6 +74,7 @@ fn report_mismatch(
|
||||||
];
|
];
|
||||||
|
|
||||||
Report {
|
Report {
|
||||||
|
title: "TYPE MISMATCH".to_string(),
|
||||||
filename,
|
filename,
|
||||||
text: Concat(lines),
|
text: Concat(lines),
|
||||||
}
|
}
|
||||||
|
@ -104,6 +105,7 @@ fn report_bad_type(
|
||||||
];
|
];
|
||||||
|
|
||||||
Report {
|
Report {
|
||||||
|
title: "TYPE MISMATCH".to_string(),
|
||||||
filename,
|
filename,
|
||||||
text: Concat(lines),
|
text: Concat(lines),
|
||||||
}
|
}
|
||||||
|
@ -119,8 +121,8 @@ fn to_expr_report(
|
||||||
use ReportText::*;
|
use ReportText::*;
|
||||||
|
|
||||||
match expected {
|
match expected {
|
||||||
Expected::NoExpectation(_expected_type) => todo!(),
|
Expected::NoExpectation(expected_type) => todo!("hit no expectation with type {:?}", expected_type),
|
||||||
Expected::FromAnnotation(_name, _arity, _sub_context, _expected_type) => todo!(),
|
Expected::FromAnnotation(_name, _arity, _sub_context, _expected_type) => todo!("hit from annotation {:?} {:?}",_sub_context, _expected_type ),
|
||||||
Expected::ForReason(reason, expected_type, region) => match reason {
|
Expected::ForReason(reason, expected_type, region) => match reason {
|
||||||
Reason::IfCondition => {
|
Reason::IfCondition => {
|
||||||
let problem = Concat(vec![
|
let problem = Concat(vec![
|
||||||
|
@ -159,6 +161,36 @@ fn to_expr_report(
|
||||||
// they don't know. ("Wait, what's truthiness?")
|
// they don't know. ("Wait, what's truthiness?")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
Reason::WhenGuard => {
|
||||||
|
let problem = Concat(vec![
|
||||||
|
plain_text("This "),
|
||||||
|
keyword_text("if"),
|
||||||
|
plain_text(" guard condition needs to be a "),
|
||||||
|
ReportText::Type(Content::Alias(Symbol::BOOL_BOOL, vec![], Variable::BOOL)),
|
||||||
|
plain_text("."),
|
||||||
|
]);
|
||||||
|
report_bad_type(
|
||||||
|
filename,
|
||||||
|
&category,
|
||||||
|
found,
|
||||||
|
expected_type,
|
||||||
|
region,
|
||||||
|
Some(expr_region),
|
||||||
|
problem,
|
||||||
|
plain_text("Right now it’s"),
|
||||||
|
Concat(vec![
|
||||||
|
plain_text("but I need every "),
|
||||||
|
keyword_text("if"),
|
||||||
|
plain_text(" guard condition to evaluate to a "),
|
||||||
|
ReportText::Type(Content::Alias(Symbol::BOOL_BOOL, vec![], Variable::BOOL)),
|
||||||
|
plain_text("—either "),
|
||||||
|
global_tag_text("True"),
|
||||||
|
plain_text(" or "),
|
||||||
|
global_tag_text("False"),
|
||||||
|
plain_text("."),
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
}
|
||||||
Reason::IfBranch {
|
Reason::IfBranch {
|
||||||
index,
|
index,
|
||||||
total_branches,
|
total_branches,
|
||||||
|
@ -211,11 +243,96 @@ fn to_expr_report(
|
||||||
)),
|
)),
|
||||||
plain_text(&format!("The {} branch is", ith)),
|
plain_text(&format!("The {} branch is", ith)),
|
||||||
plain_text("but all the previous branches have the type"),
|
plain_text("but all the previous branches have the type"),
|
||||||
plain_text("instead."),
|
Concat(vec![
|
||||||
|
plain_text("instead. I need all branches in an "),
|
||||||
|
keyword_text("if"),
|
||||||
|
plain_text(" to have the same type!"),
|
||||||
|
]),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
_ => todo!(),
|
Reason::WhenBranch { index } => {
|
||||||
|
// NOTE: is 0-based
|
||||||
|
|
||||||
|
let ith = int_to_ordinal(index + 1);
|
||||||
|
|
||||||
|
report_mismatch(
|
||||||
|
filename,
|
||||||
|
&category,
|
||||||
|
found,
|
||||||
|
expected_type,
|
||||||
|
region,
|
||||||
|
Some(expr_region),
|
||||||
|
Concat(vec![
|
||||||
|
plain_text(&format!("The {} branch of this ", ith)),
|
||||||
|
keyword_text("when"),
|
||||||
|
plain_text(" does not match all the previous branches"),
|
||||||
|
]),
|
||||||
|
plain_text(&format!("The {} branch is", ith)),
|
||||||
|
plain_text("but all the previous branches have type"),
|
||||||
|
Concat(vec![
|
||||||
|
plain_text("instead. I need all branches of a "),
|
||||||
|
keyword_text("when"),
|
||||||
|
plain_text(" to have the same type!"),
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Reason::ElemInList { index } => {
|
||||||
|
// NOTE: is 0-based
|
||||||
|
|
||||||
|
let ith = int_to_ordinal(index + 1);
|
||||||
|
|
||||||
|
report_mismatch(
|
||||||
|
filename,
|
||||||
|
&category,
|
||||||
|
found,
|
||||||
|
expected_type,
|
||||||
|
region,
|
||||||
|
Some(expr_region),
|
||||||
|
plain_text(&format!(
|
||||||
|
"The {} element of this list does not match all the previous elements",
|
||||||
|
ith
|
||||||
|
)),
|
||||||
|
plain_text(&format!("The {} element is", ith)),
|
||||||
|
plain_text("but all the previous elements in the list have type"),
|
||||||
|
plain_text("instead. I need all elements of a list to have the same type!"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Reason::RecordUpdateValue(field) => report_mismatch(
|
||||||
|
filename,
|
||||||
|
&category,
|
||||||
|
found,
|
||||||
|
expected_type,
|
||||||
|
region,
|
||||||
|
Some(expr_region),
|
||||||
|
Concat(vec![
|
||||||
|
plain_text("I cannot update the "),
|
||||||
|
record_field_text(field.as_str()),
|
||||||
|
plain_text(" field like this"),
|
||||||
|
]),
|
||||||
|
Concat(vec![
|
||||||
|
plain_text("You are trying to update "),
|
||||||
|
record_field_text(field.as_str()),
|
||||||
|
plain_text(" to be"),
|
||||||
|
]),
|
||||||
|
plain_text("But it should be"),
|
||||||
|
plain_text("instead. Record update syntax does not allow you to change the type of fields. You can achieve that with record literal syntax."),
|
||||||
|
),
|
||||||
|
other => {
|
||||||
|
// AnonymousFnArg { arg_index: u8 },
|
||||||
|
// NamedFnArg(String /* function name */, u8 /* arg index */),
|
||||||
|
// AnonymousFnCall { arity: u8 },
|
||||||
|
// NamedFnCall(String /* function name */, u8 /* arity */),
|
||||||
|
// BinOpArg(BinOp, ArgSide),
|
||||||
|
// BinOpRet(BinOp),
|
||||||
|
// FloatLiteral,
|
||||||
|
// IntLiteral,
|
||||||
|
// NumLiteral,
|
||||||
|
// InterpolatedStringVar,
|
||||||
|
// RecordUpdateValue(Lowercase),
|
||||||
|
// RecordUpdateKeys(Symbol, SendMap<Lowercase, Type>),
|
||||||
|
todo!("I don't have a message yet for reason {:?}", other)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -378,6 +495,7 @@ fn to_circular_report(
|
||||||
];
|
];
|
||||||
|
|
||||||
Report {
|
Report {
|
||||||
|
title: "TYPE MISMATCH".to_string(),
|
||||||
filename,
|
filename,
|
||||||
text: Concat(lines),
|
text: Concat(lines),
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,6 +37,7 @@ mod test_reporting {
|
||||||
// use roc_problem::can;
|
// use roc_problem::can;
|
||||||
fn to_simple_report(text: ReportText) -> Report {
|
fn to_simple_report(text: ReportText) -> Report {
|
||||||
Report {
|
Report {
|
||||||
|
title: "SYNTAX PROBLEM".to_string(),
|
||||||
text: text,
|
text: text,
|
||||||
filename: filename_from_string(r"\code\proj\Main.roc"),
|
filename: filename_from_string(r"\code\proj\Main.roc"),
|
||||||
}
|
}
|
||||||
|
@ -826,6 +827,33 @@ mod test_reporting {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn when_if_guard() {
|
||||||
|
report_problem_as(
|
||||||
|
indoc!(
|
||||||
|
r#"
|
||||||
|
when 1 is
|
||||||
|
2 if 1 -> 0x0
|
||||||
|
_ -> 0x1
|
||||||
|
"#
|
||||||
|
),
|
||||||
|
indoc!(
|
||||||
|
r#"
|
||||||
|
This `if` guard condition needs to be a Bool.
|
||||||
|
|
||||||
|
2 ┆ 2 if 1 -> 0x0
|
||||||
|
┆ ^
|
||||||
|
|
||||||
|
Right now it’s a number of type
|
||||||
|
|
||||||
|
Num a
|
||||||
|
|
||||||
|
but I need every `if` guard condition to evaluate to a Bool—either `True` or `False`.
|
||||||
|
"#
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn if_2_branch_mismatch() {
|
fn if_2_branch_mismatch() {
|
||||||
report_problem_as(
|
report_problem_as(
|
||||||
|
@ -885,6 +913,160 @@ mod test_reporting {
|
||||||
// )
|
// )
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn when_branch_mismatch() {
|
||||||
|
report_problem_as(
|
||||||
|
indoc!(
|
||||||
|
r#"
|
||||||
|
when 1 is
|
||||||
|
2 -> "foo"
|
||||||
|
3 -> {}
|
||||||
|
"#
|
||||||
|
),
|
||||||
|
indoc!(
|
||||||
|
r#"
|
||||||
|
The 2nd branch of this `when` does not match all the previous branches
|
||||||
|
|
||||||
|
3 ┆ 3 -> {}
|
||||||
|
┆ ^^
|
||||||
|
|
||||||
|
The 2nd branch is a record of type
|
||||||
|
|
||||||
|
{}
|
||||||
|
|
||||||
|
but all the previous branches have type
|
||||||
|
|
||||||
|
Str
|
||||||
|
|
||||||
|
instead. I need all branches of a `when` to have the same type!
|
||||||
|
"#
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn elem_in_list() {
|
||||||
|
report_problem_as(
|
||||||
|
indoc!(
|
||||||
|
r#"
|
||||||
|
[ 1, 3, "foo" ]
|
||||||
|
"#
|
||||||
|
),
|
||||||
|
indoc!(
|
||||||
|
r#"
|
||||||
|
The 3rd element of this list does not match all the previous elements
|
||||||
|
|
||||||
|
1 ┆ [ 1, 3, "foo" ]
|
||||||
|
┆ ^^^^^
|
||||||
|
|
||||||
|
The 3rd element is a string of type
|
||||||
|
|
||||||
|
Str
|
||||||
|
|
||||||
|
but all the previous elements in the list have type
|
||||||
|
|
||||||
|
Num a
|
||||||
|
|
||||||
|
instead. I need all elements of a list to have the same type!
|
||||||
|
"#
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn record_update_value() {
|
||||||
|
report_problem_as(
|
||||||
|
indoc!(
|
||||||
|
r#"
|
||||||
|
x : { foo : {} }
|
||||||
|
x = { foo: {} }
|
||||||
|
|
||||||
|
{ x & foo: "bar" }
|
||||||
|
"#
|
||||||
|
),
|
||||||
|
indoc!(
|
||||||
|
r#"
|
||||||
|
I cannot update the .foo field like this
|
||||||
|
|
||||||
|
4 ┆ { x & foo: "bar" }
|
||||||
|
┆ ^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
You are trying to update .foo to be a string of type
|
||||||
|
|
||||||
|
Str
|
||||||
|
|
||||||
|
But it should be
|
||||||
|
|
||||||
|
{}
|
||||||
|
|
||||||
|
instead. Record update syntax does not allow you to change the type of fields. You can achieve that with record literal syntax.
|
||||||
|
"#
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// needs a bit more infrastructure re. diffing records
|
||||||
|
// #[test]
|
||||||
|
// fn record_update_keys() {
|
||||||
|
// report_problem_as(
|
||||||
|
// indoc!(
|
||||||
|
// r#"
|
||||||
|
// x : { foo : {} }
|
||||||
|
// x = { foo: {} }
|
||||||
|
//
|
||||||
|
// { x & baz: "bar" }
|
||||||
|
// "#
|
||||||
|
// ),
|
||||||
|
// indoc!(
|
||||||
|
// r#"
|
||||||
|
// The `x` record does not have a `baz` field
|
||||||
|
//
|
||||||
|
// 4 ┆ { x & baz: "bar" }
|
||||||
|
// ┆ ^^^
|
||||||
|
//
|
||||||
|
// This is usually a typo. Here are the `x` fields that are most similar
|
||||||
|
//
|
||||||
|
// { foo : {}
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// So maybe `baz` should be `foo`?
|
||||||
|
// "#
|
||||||
|
// ),
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
|
||||||
|
// #[test]
|
||||||
|
// fn num_literal() {
|
||||||
|
// report_problem_as(
|
||||||
|
// indoc!(
|
||||||
|
// r#"
|
||||||
|
// x : Str
|
||||||
|
// x = 4
|
||||||
|
//
|
||||||
|
// x
|
||||||
|
// "#
|
||||||
|
// ),
|
||||||
|
// indoc!(
|
||||||
|
// r#"
|
||||||
|
// Something is off with the body of the `x` definition
|
||||||
|
//
|
||||||
|
// 4 ┆ x = 4
|
||||||
|
// ┆ ^
|
||||||
|
//
|
||||||
|
// The body is a number of type
|
||||||
|
//
|
||||||
|
// Num a
|
||||||
|
//
|
||||||
|
// But the type annotation on `x` says that it should be
|
||||||
|
//
|
||||||
|
// Str
|
||||||
|
//
|
||||||
|
// instead.
|
||||||
|
// "#
|
||||||
|
// ),
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn circular_type() {
|
fn circular_type() {
|
||||||
report_problem_as(
|
report_problem_as(
|
||||||
|
|
|
@ -628,7 +628,7 @@ pub enum Reason {
|
||||||
WhenGuard,
|
WhenGuard,
|
||||||
IfCondition,
|
IfCondition,
|
||||||
IfBranch { index: usize, total_branches: usize },
|
IfBranch { index: usize, total_branches: usize },
|
||||||
ElemInList,
|
ElemInList { index: usize },
|
||||||
RecordUpdateValue(Lowercase),
|
RecordUpdateValue(Lowercase),
|
||||||
RecordUpdateKeys(Symbol, SendMap<Lowercase, Type>),
|
RecordUpdateKeys(Symbol, SendMap<Lowercase, Type>),
|
||||||
}
|
}
|
||||||
|
|
Binary file not shown.
|
@ -1,7 +1,7 @@
|
||||||
use std::ffi::CStr;
|
use std::ffi::CStr;
|
||||||
use std::os::raw::c_char;
|
use std::os::raw::c_char;
|
||||||
|
|
||||||
#[link(name = "hello_from_roc")]
|
#[link(name = "app_main")]
|
||||||
extern "C" {
|
extern "C" {
|
||||||
#[link_name = "$Test.main"]
|
#[link_name = "$Test.main"]
|
||||||
fn str_from_roc() -> *const c_char;
|
fn str_from_roc() -> *const c_char;
|
||||||
|
|
3
examples/quicksort/.gitignore
vendored
Normal file
3
examples/quicksort/.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
qs
|
||||||
|
*.o
|
||||||
|
*.so
|
234
examples/quicksort/README.md
Normal file
234
examples/quicksort/README.md
Normal file
|
@ -0,0 +1,234 @@
|
||||||
|
# Quicksort
|
||||||
|
|
||||||
|
Right now, there is only one way to build Roc programs: the Rube Goldberg Build Process.
|
||||||
|
(In the future, it will be nicer. At the moment, the nicer build system exists only
|
||||||
|
in our imaginations...so Rube Goldberg it is!)
|
||||||
|
|
||||||
|
*NOTE:* On macOS or Linux, you can run `sudo ./build.sh` from this directory instead of following these instructions.
|
||||||
|
|
||||||
|
## Ingredients
|
||||||
|
|
||||||
|
1. A host. For this example, our host is implemented in the file `host.rs`.
|
||||||
|
2. A Roc function. For this example, we'll use a function which returns a list of integers.
|
||||||
|
3. Having `gcc` installed. This will not be necessary in the future, but the Rube Goldberg build process needs it.
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
1. `cd` into `examples/hello-world/`
|
||||||
|
2. Run `cargo run hello.roc` to compile the Roc source code into a `hello.o` file.
|
||||||
|
3. Run `gcc -shared hello.o -o libroc_qs_main.so` to generate `libroc_qs_main.so`. (This filename must begin with `lib` and end in `.so` or else `host.rs` won't be able to find it!)
|
||||||
|
4. Move `libroc_qs_main.so` onto the system library path, e.g. with `sudo mv libroc_qs_main.so /usr/local/lib/` on macOS, or `sudo mv libroc_qs_main.so /usr/local/lib /usr/lib` on Linux.
|
||||||
|
5. Run `rustc host.rs -o qs` to generate the `qs` executable.
|
||||||
|
6. Run `./qs` to see the output!
|
||||||
|
|
||||||
|
To run in release mode instead, do:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo run --release hello.roc
|
||||||
|
```
|
||||||
|
|
||||||
|
## Design Notes
|
||||||
|
|
||||||
|
This demonstrates the basic design of hosts: Roc code gets compiled into a pure
|
||||||
|
function (in this case, a thunk that always returns `"Hello, World!"`) and
|
||||||
|
then the host calls that function. Fundamentally, that's the whole idea! The host
|
||||||
|
might not even have a `main` - it could be a library, a plugin, anything.
|
||||||
|
Everything else is built on this basic "hosts calling linked pure functions" design.
|
||||||
|
|
||||||
|
For example, things get more interesting when the compiled Roc function returns
|
||||||
|
a `Task` - that is, a tagged union data structure containing function pointers
|
||||||
|
to callback closures. This lets the Roc pure function describe arbitrary
|
||||||
|
chainable effects, which the host can interpret to perform I/O as requested by
|
||||||
|
the Roc program. (The tagged union `Task` would have a variant for each supported
|
||||||
|
I/O operation.)
|
||||||
|
|
||||||
|
In this trivial example, it's very easy to line up the API between the host and
|
||||||
|
the Roc program. In a more involved host, this would be much trickier - especially
|
||||||
|
if the API were changing frequently during development.
|
||||||
|
|
||||||
|
The idea there is to have a first-class concept of "glue code" which host authors
|
||||||
|
can write (it would be plain Roc code, but with some extra keywords that aren't
|
||||||
|
available in normal modules - kinda like `port module` in Elm), and which
|
||||||
|
describe both the Roc-host/C boundary as well as the Roc-host/Roc-app boundary.
|
||||||
|
Roc application authors only care about the Roc-host/Roc-app portion, and the
|
||||||
|
host author only cares about the Roc-host/C bounary when implementing the host.
|
||||||
|
|
||||||
|
Using this glue code, the Roc compiler can generate C header files describing the
|
||||||
|
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)
|
||||||
|
generates correct Rust FFI bindings from C headers.
|
||||||
|
|
||||||
|
The whole "calling gcc and rustc" part of the current build process is obviously
|
||||||
|
not something Roc application authors should ever need to do. Rather, the idea
|
||||||
|
would be to have the host precompiled into an object file (eliminating the
|
||||||
|
need for Roc authors to run `rustc` in this example) and for the Roc compiler
|
||||||
|
to not only generate the object file for the Roc file, but also to link it with
|
||||||
|
the host object file to produce an executable (eliminating the need for `gcc`)
|
||||||
|
such that Roc application authors can concern themselves exclusively with Roc code
|
||||||
|
and need only the Roc compiler to build and to execute it.
|
||||||
|
|
||||||
|
Of course, none of those niceties exist yet. But we'll get there!
|
||||||
|
|
||||||
|
## The test that builds things
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let src = indoc!(
|
||||||
|
r#"
|
||||||
|
"Hello, World!"
|
||||||
|
"#
|
||||||
|
);
|
||||||
|
|
||||||
|
// Build the expr
|
||||||
|
let arena = Bump::new();
|
||||||
|
let (loc_expr, _output, _problems, subs, var, constraint, home, interns) = uniq_expr(src);
|
||||||
|
|
||||||
|
let mut unify_problems = Vec::new();
|
||||||
|
let (content, mut subs) = infer_expr(subs, &mut unify_problems, &constraint, var);
|
||||||
|
|
||||||
|
let context = Context::create();
|
||||||
|
let module = context.create_module("app");
|
||||||
|
let builder = context.create_builder();
|
||||||
|
let fpm = PassManager::create(&module);
|
||||||
|
|
||||||
|
roc_gen::llvm::build::add_passes(&fpm);
|
||||||
|
|
||||||
|
fpm.initialize();
|
||||||
|
|
||||||
|
// Compute main_fn_type before moving subs to Env
|
||||||
|
let layout = Layout::from_content(&arena, content, &subs, crate::helpers::eval::POINTER_SIZE)
|
||||||
|
.unwrap_or_else(|err| panic!("Code gen error in test: could not convert to layout. Err was {:?} and Subs were {:?}", err, subs));
|
||||||
|
|
||||||
|
let execution_engine = module
|
||||||
|
.create_jit_execution_engine(OptimizationLevel::None)
|
||||||
|
.expect("Error creating JIT execution engine for test");
|
||||||
|
|
||||||
|
let ptr_bytes = execution_engine
|
||||||
|
.get_target_data()
|
||||||
|
.get_pointer_byte_size(None);
|
||||||
|
let main_fn_type =
|
||||||
|
basic_type_from_layout(&arena, &context, &layout, ptr_bytes).fn_type(&[], false);
|
||||||
|
let main_fn_name = "$Test.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,
|
||||||
|
module: arena.alloc(module),
|
||||||
|
ptr_bytes,
|
||||||
|
};
|
||||||
|
let mut procs = Procs::default();
|
||||||
|
let mut ident_ids = env.interns.all_ident_ids.remove(&home).unwrap();
|
||||||
|
|
||||||
|
// Populate Procs and get the low-level Expr from the canonical Expr
|
||||||
|
let main_body = Expr::new(
|
||||||
|
&arena,
|
||||||
|
&mut subs,
|
||||||
|
loc_expr.value,
|
||||||
|
&mut procs,
|
||||||
|
home,
|
||||||
|
&mut ident_ids,
|
||||||
|
crate::helpers::eval::POINTER_SIZE,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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());
|
||||||
|
|
||||||
|
// 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, opt_proc) in procs.as_map().into_iter() {
|
||||||
|
if let Some(proc) = opt_proc {
|
||||||
|
let (fn_val, arg_basic_types) = build_proc_header(&env, symbol, &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, proc, &procs, 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 main_fn = env.module.add_function(main_fn_name, main_fn_type, None);
|
||||||
|
|
||||||
|
main_fn.set_call_conventions(crate::helpers::eval::MAIN_CALLING_CONVENTION);
|
||||||
|
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,
|
||||||
|
&ImMap::default(),
|
||||||
|
main_fn,
|
||||||
|
&main_body,
|
||||||
|
&mut Procs::default(),
|
||||||
|
);
|
||||||
|
|
||||||
|
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!("Errors defining module: {:?}", errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uncomment this to see the module's optimized LLVM instruction output:
|
||||||
|
// env.module.print_to_stderr();
|
||||||
|
|
||||||
|
// Emit
|
||||||
|
Target::initialize_x86(&InitializationConfig::default());
|
||||||
|
|
||||||
|
let opt = OptimizationLevel::Default;
|
||||||
|
let reloc = RelocMode::Default;
|
||||||
|
let model = CodeModel::Default;
|
||||||
|
let target = Target::from_name("x86-64").unwrap();
|
||||||
|
let target_machine = target
|
||||||
|
.create_target_machine(
|
||||||
|
&TargetTriple::create("x86_64-pc-linux-gnu"),
|
||||||
|
"x86-64",
|
||||||
|
"+avx2",
|
||||||
|
opt,
|
||||||
|
reloc,
|
||||||
|
model,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let path = Path::new("../../hello.o");
|
||||||
|
|
||||||
|
assert!(target_machine
|
||||||
|
.write_to_file(&env.module, FileType::Object, &path)
|
||||||
|
.is_ok());
|
||||||
|
|
||||||
|
let path = Path::new("../../hello.asm");
|
||||||
|
|
||||||
|
assert!(target_machine
|
||||||
|
.write_to_file(&env.module, FileType::Assembly, &path)
|
||||||
|
.is_ok());
|
||||||
|
```
|
18
examples/quicksort/build.sh
Executable file
18
examples/quicksort/build.sh
Executable file
|
@ -0,0 +1,18 @@
|
||||||
|
#!/bin/bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
cargo run qs.roc
|
||||||
|
gcc -shared qs.o -o libroc_qs_main.so
|
||||||
|
|
||||||
|
# Move it to a different place depending on Linux vs macOS
|
||||||
|
unameVal="$(uname -s)"
|
||||||
|
|
||||||
|
case "${unameVal}" in
|
||||||
|
Linux*) sudo mv libroc_qs_main.so /usr/lib/;;
|
||||||
|
Darwin*) sudo mv libroc_qs_main.so /usr/local/lib/;;
|
||||||
|
*) echo "build.sh does not support this operating system!" exit 1;
|
||||||
|
esac
|
||||||
|
|
||||||
|
rustc host.rs -o qs
|
||||||
|
|
||||||
|
./qs
|
12
examples/quicksort/host.rs
Normal file
12
examples/quicksort/host.rs
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
#[link(name = "roc_qs_main")]
|
||||||
|
extern "C" {
|
||||||
|
#[allow(improper_ctypes)]
|
||||||
|
#[link_name = "$Test.main"]
|
||||||
|
fn list_from_roc() -> Box<[i64]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn main() {
|
||||||
|
let list = unsafe { list_from_roc() };
|
||||||
|
|
||||||
|
println!("Roc quicksort says: {:?}", list);
|
||||||
|
}
|
47
examples/quicksort/qs.roc
Normal file
47
examples/quicksort/qs.roc
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
quicksort : List (Num a), Int, Int -> List (Num a)
|
||||||
|
quicksort = \list, low, high ->
|
||||||
|
when partition low high list is
|
||||||
|
Pair partitionIndex partitioned ->
|
||||||
|
partitioned
|
||||||
|
|> quicksort low (partitionIndex - 1)
|
||||||
|
|> quicksort (partitionIndex + 1) high
|
||||||
|
|
||||||
|
|
||||||
|
swap : Int, Int, List a -> List a
|
||||||
|
swap = \i, j, list ->
|
||||||
|
when Pair (List.get list i) (List.get list j) is
|
||||||
|
Pair (Ok atI) (Ok atJ) ->
|
||||||
|
list
|
||||||
|
|> List.set i atJ
|
||||||
|
|> List.set j atI
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
[]
|
||||||
|
|
||||||
|
|
||||||
|
partition : Int, Int, List (Num a) -> [ Pair Int (List (Num a)) ]
|
||||||
|
partition = \low, high, initialList ->
|
||||||
|
when List.get initialList high is
|
||||||
|
Ok pivot ->
|
||||||
|
go = \i, j, list ->
|
||||||
|
if j < high then
|
||||||
|
when List.get list j is
|
||||||
|
Ok value ->
|
||||||
|
if value <= pivot then
|
||||||
|
go (i + 1) (j + 1) (swap (i + 1) j list)
|
||||||
|
else
|
||||||
|
go i (j + 1) list
|
||||||
|
|
||||||
|
Err _ ->
|
||||||
|
Pair i list
|
||||||
|
else
|
||||||
|
Pair i list
|
||||||
|
|
||||||
|
when go (low - 1) low initialList is
|
||||||
|
Pair newI newList ->
|
||||||
|
Pair (newI + 1) (swap (newI + 1) high newList)
|
||||||
|
|
||||||
|
Err _ ->
|
||||||
|
Pair (low - 1) initialList
|
||||||
|
|
||||||
|
quicksort [ 7, 4, 9 ]
|
Loading…
Add table
Add a link
Reference in a new issue