moved all crates into seperate folder + related path fixes

This commit is contained in:
Anton-4 2022-07-01 17:37:43 +02:00
parent 12ef03bb86
commit eee85fa45d
No known key found for this signature in database
GPG key ID: C954D6E0F9C0ABFD
1063 changed files with 92 additions and 93 deletions

35
crates/ast/Cargo.toml Normal file
View file

@ -0,0 +1,35 @@
[package]
name = "roc_ast"
version = "0.1.0"
authors = ["The Roc Contributors"]
license = "UPL-1.0"
edition = "2021"
description = "AST as used by the editor and (soon) docs. In contrast to the compiler, these types do not keep track of a location in a file."
[dependencies]
roc_builtins = { path = "../compiler/builtins"}
roc_can = { path = "../compiler/can" }
roc_collections = { path = "../compiler/collections" }
roc_region = { path = "../compiler/region" }
roc_module = { path = "../compiler/module" }
roc_parse = { path = "../compiler/parse" }
roc_problem = { path = "../compiler/problem" }
roc_types = { path = "../compiler/types" }
roc_unify = { path = "../compiler/unify"}
roc_load = { path = "../compiler/load" }
roc_target = { path = "../compiler/roc_target" }
roc_error_macros = { path = "../error_macros" }
roc_reporting = { path = "../reporting" }
arrayvec = "0.7.2"
bumpalo = { version = "3.8.0", features = ["collections"] }
page_size = "0.4.2"
snafu = { version = "0.7.1", features = ["backtraces"] }
ven_graph = { path = "../vendor/pathfinding" }
libc = "0.2.106"
[dev-dependencies]
indoc = "1.0.3"
[target.'cfg(windows)'.dependencies]
winapi = { version = "0.3.9", features = ["memoryapi"]}

View file

@ -0,0 +1,73 @@
use roc_module::{ident::Ident, module_err::ModuleError};
use roc_parse::parser::SyntaxError;
use roc_region::all::{Loc, Region};
use snafu::{Backtrace, Snafu};
use crate::lang::core::ast::ASTNodeId;
#[derive(Debug, Snafu)]
#[snafu(visibility(pub))]
pub enum ASTError {
#[snafu(display(
"ASTNodeIdWithoutExprId: The expr_id_opt in ASTNode({:?}) was `None` but I was expecting `Some(ExprId)` .",
ast_node_id
))]
ASTNodeIdWithoutExprId {
ast_node_id: ASTNodeId,
backtrace: Backtrace,
},
#[snafu(display(
"UnexpectedASTNode: required a {} at this position, node was a {}.",
required_node_type,
encountered_node_type
))]
UnexpectedASTNode {
required_node_type: String,
encountered_node_type: String,
backtrace: Backtrace,
},
#[snafu(display(
"UnexpectedPattern2Variant: required a {} at this position, Pattern2 was a {}.",
required_pattern2,
encountered_pattern2,
))]
UnexpectedPattern2Variant {
required_pattern2: String,
encountered_pattern2: String,
backtrace: Backtrace,
},
#[snafu(display("IdentExistsError: {}", msg))]
IdentExistsError { msg: String },
WrapModuleError {
#[snafu(backtrace)]
source: ModuleError,
},
#[snafu(display("SyntaxError: {}", msg))]
SyntaxErrorNoBacktrace { msg: String },
}
pub type ASTResult<T, E = ASTError> = std::result::Result<T, E>;
impl From<ModuleError> for ASTError {
fn from(module_err: ModuleError) -> Self {
Self::WrapModuleError { source: module_err }
}
}
impl From<(Region, Loc<Ident>)> for ASTError {
fn from(ident_exists_err: (Region, Loc<Ident>)) -> Self {
Self::IdentExistsError {
msg: format!("{:?}", ident_exists_err),
}
}
}
impl<'a> From<SyntaxError<'a>> for ASTError {
fn from(syntax_err: SyntaxError) -> Self {
Self::SyntaxErrorNoBacktrace {
msg: format!("{:?}", syntax_err),
}
}
}

View file

@ -0,0 +1,316 @@
use roc_collections::all::MutMap;
use roc_problem::can::Problem;
use roc_region::all::{Loc, Region};
use roc_types::subs::Variable;
use crate::{
lang::{
core::{
def::def::References,
expr::{
expr2::{Expr2, ExprId, WhenBranch},
expr_to_expr2::expr_to_expr2,
output::Output,
record_field::RecordField,
},
pattern::to_pattern2,
},
env::Env,
scope::Scope,
},
mem_pool::{pool_str::PoolStr, pool_vec::PoolVec, shallow_clone::ShallowClone},
};
pub(crate) enum CanonicalizeRecordProblem {
#[allow(dead_code)]
InvalidOptionalValue {
field_name: PoolStr,
field_region: Region,
record_region: Region,
},
}
enum FieldVar {
VarAndExprId(Variable, ExprId),
OnlyVar(Variable),
}
pub(crate) fn canonicalize_fields<'a>(
env: &mut Env<'a>,
scope: &mut Scope,
fields: &'a [Loc<roc_parse::ast::AssignedField<'a, roc_parse::ast::Expr<'a>>>],
) -> Result<(PoolVec<RecordField>, Output), CanonicalizeRecordProblem> {
let mut can_fields: MutMap<&'a str, FieldVar> = MutMap::default();
let mut output = Output::default();
for loc_field in fields.iter() {
match canonicalize_field(env, scope, &loc_field.value) {
Ok(can_field) => {
match can_field {
CanonicalField::LabelAndValue {
label,
value_expr,
value_output,
var,
} => {
let expr_id = env.pool.add(value_expr);
let replaced =
can_fields.insert(label, FieldVar::VarAndExprId(var, expr_id));
if let Some(_old) = replaced {
// env.problems.push(Problem::DuplicateRecordFieldValue {
// field_name: label,
// field_region: loc_field.region,
// record_region: region,
// replaced_region: old.region,
// });
todo!()
}
output.references.union_mut(value_output.references);
}
CanonicalField::InvalidLabelOnly { label, var } => {
let replaced = can_fields.insert(label, FieldVar::OnlyVar(var));
if let Some(_old) = replaced {
todo!()
}
}
}
}
Err(CanonicalizeFieldProblem::InvalidOptionalValue {
field_name: _,
field_region: _,
}) => {
// env.problem(Problem::InvalidOptionalValue {
// field_name: field_name.clone(),
// field_region,
// record_region: region,
// });
// return Err(CanonicalizeRecordProblem::InvalidOptionalValue {
// field_name,
// field_region,
// record_region: region,
// });
todo!()
}
}
}
let pool_vec = PoolVec::with_capacity(can_fields.len() as u32, env.pool);
for (node_id, (string, field_var)) in pool_vec.iter_node_ids().zip(can_fields.into_iter()) {
let name = PoolStr::new(string, env.pool);
match field_var {
FieldVar::VarAndExprId(var, expr_id) => {
env.pool[node_id] = RecordField::LabeledValue(name, var, expr_id);
}
FieldVar::OnlyVar(var) => {
env.pool[node_id] = RecordField::InvalidLabelOnly(name, var);
} // TODO RecordField::LabelOnly
}
}
Ok((pool_vec, output))
}
#[allow(dead_code)]
enum CanonicalizeFieldProblem {
InvalidOptionalValue {
field_name: PoolStr,
field_region: Region,
},
}
// TODO: the `value_output: Output` field takes _a lot_ of space!
#[allow(clippy::large_enum_variant)]
enum CanonicalField<'a> {
LabelAndValue {
label: &'a str,
value_expr: Expr2,
value_output: Output,
var: Variable,
},
InvalidLabelOnly {
label: &'a str,
var: Variable,
}, // TODO make ValidLabelOnly
}
fn canonicalize_field<'a>(
env: &mut Env<'a>,
scope: &mut Scope,
field: &'a roc_parse::ast::AssignedField<'a, roc_parse::ast::Expr<'a>>,
) -> Result<CanonicalField<'a>, CanonicalizeFieldProblem> {
use roc_parse::ast::AssignedField::*;
match field {
// Both a label and a value, e.g. `{ name: "blah" }`
RequiredValue(label, _, loc_expr) => {
let field_var = env.var_store.fresh();
let (loc_can_expr, output) =
expr_to_expr2(env, scope, &loc_expr.value, loc_expr.region);
match loc_can_expr {
Expr2::RuntimeError() => Ok(CanonicalField::InvalidLabelOnly {
label: label.value,
var: field_var,
}),
_ => Ok(CanonicalField::LabelAndValue {
label: label.value,
value_expr: loc_can_expr,
value_output: output,
var: field_var,
}),
}
}
OptionalValue(label, _, loc_expr) => Err(CanonicalizeFieldProblem::InvalidOptionalValue {
field_name: PoolStr::new(label.value, env.pool),
field_region: Region::span_across(&label.region, &loc_expr.region),
}),
// A label with no value, e.g. `{ name }` (this is sugar for { name: name })
LabelOnly(label) => {
let field_var = env.var_store.fresh();
// TODO return ValidLabel if label points to in scope variable
Ok(CanonicalField::InvalidLabelOnly {
label: label.value,
var: field_var,
})
}
SpaceBefore(sub_field, _) | SpaceAfter(sub_field, _) => {
canonicalize_field(env, scope, sub_field)
}
Malformed(_string) => {
panic!("TODO canonicalize malformed record field");
}
}
}
#[inline(always)]
pub(crate) fn canonicalize_when_branch<'a>(
env: &mut Env<'a>,
scope: &mut Scope,
branch: &'a roc_parse::ast::WhenBranch<'a>,
output: &mut Output,
) -> (WhenBranch, References) {
let patterns = PoolVec::with_capacity(branch.patterns.len() as u32, env.pool);
let original_scope = scope;
let mut scope = original_scope.shallow_clone();
// TODO report symbols not bound in all patterns
for (node_id, loc_pattern) in patterns.iter_node_ids().zip(branch.patterns.iter()) {
let (new_output, can_pattern) = to_pattern2(
env,
&mut scope,
roc_parse::pattern::PatternType::WhenBranch,
&loc_pattern.value,
loc_pattern.region,
);
output.union(new_output);
env.set_region(node_id, loc_pattern.region);
env.pool[node_id] = can_pattern;
}
let (value, mut branch_output) =
expr_to_expr2(env, &mut scope, &branch.value.value, branch.value.region);
let value_id = env.pool.add(value);
env.set_region(value_id, branch.value.region);
let guard = match &branch.guard {
None => None,
Some(loc_expr) => {
let (can_guard, guard_branch_output) =
expr_to_expr2(env, &mut scope, &loc_expr.value, loc_expr.region);
let expr_id = env.pool.add(can_guard);
env.set_region(expr_id, loc_expr.region);
branch_output.union(guard_branch_output);
Some(expr_id)
}
};
// Now that we've collected all the references for this branch, check to see if
// any of the new idents it defined were unused. If any were, report it.
for (symbol, region) in scope.symbols() {
let symbol = symbol;
if !output.references.has_lookup(symbol)
&& !branch_output.references.has_lookup(symbol)
&& !original_scope.contains_symbol(symbol)
{
env.problem(Problem::UnusedDef(symbol, region));
}
}
let references = branch_output.references.clone();
output.union(branch_output);
(
WhenBranch {
patterns,
body: value_id,
guard,
},
references,
)
}
pub(crate) fn canonicalize_lookup(
env: &mut Env<'_>,
scope: &mut Scope,
module_name: &str,
ident: &str,
region: Region,
) -> (Expr2, Output) {
use Expr2::*;
let mut output = Output::default();
let can_expr = if module_name.is_empty() {
// Since module_name was empty, this is an unqualified var.
// Look it up in scope!
match scope.lookup(&(*ident).into(), region) {
Ok(symbol) => {
output.references.lookups.insert(symbol);
Var(symbol)
}
Err(problem) => {
env.problem(Problem::RuntimeError(problem));
RuntimeError()
}
}
} else {
// Since module_name was nonempty, this is a qualified var.
// Look it up in the env!
match env.qualified_lookup(module_name, ident, region) {
Ok(symbol) => {
output.references.lookups.insert(symbol);
Var(symbol)
}
Err(problem) => {
// Either the module wasn't imported, or
// it was imported but it doesn't expose this ident.
env.problem(Problem::RuntimeError(problem));
RuntimeError()
}
}
};
// If it's valid, this ident should be in scope already.
(can_expr, output)
}

View file

@ -0,0 +1,2 @@
pub mod canonicalize;
pub mod module;

View file

@ -0,0 +1,42 @@
#![allow(clippy::all)]
#![allow(dead_code)]
#![allow(unused_imports)]
#![allow(unused_variables)]
use bumpalo::Bump;
use roc_can::operator::desugar_def;
use roc_collections::all::{default_hasher, ImMap, ImSet, MutMap, MutSet, SendMap};
use roc_module::ident::Ident;
use roc_module::ident::Lowercase;
use roc_module::symbol::IdentIdsByModule;
use roc_module::symbol::{IdentIds, ModuleId, ModuleIds, Symbol};
use roc_parse::ast;
use roc_parse::pattern::PatternType;
use roc_problem::can::{Problem, RuntimeError};
use roc_region::all::{Loc, Region};
use roc_types::subs::{VarStore, Variable};
use crate::lang::core::def::def::canonicalize_defs;
use crate::lang::core::def::def::Def;
use crate::lang::core::def::def::{sort_can_defs, Declaration};
use crate::lang::core::expr::expr2::Expr2;
use crate::lang::core::expr::output::Output;
use crate::lang::core::pattern::Pattern2;
use crate::lang::core::types::Alias;
use crate::lang::core::val_def::ValueDef;
use crate::lang::env::Env;
use crate::lang::scope::Scope;
use crate::mem_pool::pool::NodeId;
use crate::mem_pool::pool::Pool;
use crate::mem_pool::pool_vec::PoolVec;
use crate::mem_pool::shallow_clone::ShallowClone;
pub struct ModuleOutput {
pub aliases: MutMap<Symbol, NodeId<Alias>>,
pub rigid_variables: MutMap<Variable, Lowercase>,
pub declarations: Vec<Declaration>,
pub exposed_imports: MutMap<Symbol, Variable>,
pub lookups: Vec<(Symbol, Variable, Region)>,
pub problems: Vec<Problem>,
pub ident_ids: IdentIds,
pub references: MutSet<Symbol>,
}

2813
crates/ast/src/constrain.rs Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,63 @@
use crate::{
ast_error::{ASTNodeIdWithoutExprIdSnafu, ASTResult},
mem_pool::pool::Pool,
};
use super::{
def::def2::{def2_to_string, DefId},
expr::{expr2::ExprId, expr2_to_string::expr2_to_string},
header::AppHeader,
};
#[derive(Debug)]
pub struct AST {
pub header: AppHeader,
pub def_ids: Vec<DefId>,
}
impl AST {
pub fn insert_def_at_index(&mut self, new_def_id: DefId, index: usize) {
self.def_ids.insert(index, new_def_id);
}
// TODO print in tree shape, similar to linux tree command
pub fn ast_to_string(&self, pool: &Pool) -> String {
let mut full_ast_string = String::new();
for def_id in self.def_ids.iter() {
full_ast_string.push_str(&def2_to_string(*def_id, pool));
full_ast_string.push_str("\n\n");
}
full_ast_string
}
}
#[derive(Debug, PartialEq, Copy, Clone)]
pub enum ASTNodeId {
ADefId(DefId),
AExprId(ExprId),
}
impl ASTNodeId {
pub fn to_expr_id(&self) -> ASTResult<ExprId> {
match self {
ASTNodeId::AExprId(expr_id) => Ok(*expr_id),
_ => ASTNodeIdWithoutExprIdSnafu { ast_node_id: *self }.fail()?,
}
}
pub fn to_def_id(&self) -> ASTResult<DefId> {
match self {
ASTNodeId::ADefId(def_id) => Ok(*def_id),
_ => ASTNodeIdWithoutExprIdSnafu { ast_node_id: *self }.fail()?,
}
}
}
pub fn ast_node_to_string(node_id: ASTNodeId, pool: &Pool) -> String {
match node_id {
ASTNodeId::ADefId(def_id) => def2_to_string(def_id, pool),
ASTNodeId::AExprId(expr_id) => expr2_to_string(expr_id, pool),
}
}

View file

@ -0,0 +1,70 @@
use roc_types::subs::VarStore;
use crate::{
lang::core::{def::def::Def, expr::expr2::Expr2},
mem_pool::{pool::Pool, pool_vec::PoolVec},
};
use super::def::def::Declaration;
pub(crate) fn decl_to_let(
pool: &mut Pool,
var_store: &mut VarStore,
decl: Declaration,
ret: Expr2,
) -> Expr2 {
match decl {
Declaration::Declare(def) => match def {
Def::AnnotationOnly { .. } => todo!(),
Def::Value(value_def) => {
let def_id = pool.add(value_def);
let body_id = pool.add(ret);
Expr2::LetValue {
def_id,
body_id,
body_var: var_store.fresh(),
}
}
Def::Function(function_def) => {
let def_id = pool.add(function_def);
let body_id = pool.add(ret);
Expr2::LetFunction {
def_id,
body_id,
body_var: var_store.fresh(),
}
}
},
Declaration::DeclareRec(defs) => {
let mut function_defs = vec![];
for def in defs {
match def {
Def::AnnotationOnly { .. } => todo!(),
Def::Function(function_def) => function_defs.push(function_def),
Def::Value(_) => unreachable!(),
}
}
let body_id = pool.add(ret);
Expr2::LetRec {
defs: PoolVec::new(function_defs.into_iter(), pool),
body_var: var_store.fresh(),
body_id,
}
}
Declaration::InvalidCycle(_entries, _) => {
// TODO: replace with something from Expr2
// Expr::RuntimeError(RuntimeError::CircularDef(entries))
todo!()
}
Declaration::Builtin(_) => {
// Builtins should only be added to top-level decls, not to let-exprs!
unreachable!()
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,58 @@
use roc_module::symbol::IdentId;
use crate::{
lang::core::expr::{expr2::Expr2, expr2_to_string::expr2_to_string},
mem_pool::pool::{NodeId, Pool},
};
// A top level definition, not inside a function. For example: `main = "Hello, world!"`
#[derive(Debug)]
pub enum Def2 {
// ValueDef example: `main = "Hello, world!"`. identifier -> `main`, expr -> "Hello, world!"
ValueDef {
identifier_id: IdentId,
expr_id: NodeId<Expr2>,
},
Blank,
CommentsBefore {
comments: String,
def_id: DefId,
},
CommentsAfter {
comments: String,
def_id: DefId,
},
}
pub type DefId = NodeId<Def2>;
pub fn def2_to_string(node_id: DefId, pool: &Pool) -> String {
let mut full_string = String::new();
let def2 = pool.get(node_id);
match def2 {
Def2::ValueDef {
identifier_id,
expr_id,
} => {
full_string.push_str(&format!(
"Def2::ValueDef(identifier_id: >>{:?}), expr_id: >>{:?})",
identifier_id,
expr2_to_string(*expr_id, pool)
));
}
Def2::Blank => {
full_string.push_str("Def2::Blank");
}
Def2::CommentsBefore {
comments,
def_id: _,
} => full_string.push_str(comments),
Def2::CommentsAfter {
comments,
def_id: _,
} => full_string.push_str(comments),
}
full_string
}

View file

@ -0,0 +1,193 @@
use bumpalo::Bump;
use roc_parse::{ast::CommentOrNewline, parser::SyntaxError};
use roc_region::all::Region;
use crate::lang::{core::expr::expr_to_expr2::loc_expr_to_expr2, env::Env, scope::Scope};
use super::def2::Def2;
fn spaces_to_comments(spaces: &[CommentOrNewline]) -> Option<String> {
if !spaces.is_empty() && !all_newlines(spaces) {
let mut all_comments_str = String::new();
for comment in spaces.iter().filter(|c_or_nl| !c_or_nl.is_newline()) {
all_comments_str.push_str(&comment.to_string_repr());
}
Some(all_comments_str)
} else {
None
}
}
pub fn toplevel_defs_to_defs2<'a>(
arena: &'a Bump,
env: &mut Env<'a>,
scope: &mut Scope,
parsed_defs: roc_parse::ast::Defs<'a>,
region: Region,
) -> Vec<Def2> {
let mut result = Vec::with_capacity(parsed_defs.tags.len());
for (index, def) in parsed_defs.defs().enumerate() {
let mut def = match def {
Err(roc_parse::ast::ValueDef::Body(&loc_pattern, &loc_expr)) => {
let expr2 = loc_expr_to_expr2(arena, loc_expr, env, scope, region).0;
let expr_id = env.pool.add(expr2);
use roc_parse::ast::Pattern::*;
match loc_pattern.value {
Identifier(id_str) => {
let identifier_id = env.ident_ids.get_or_insert(id_str);
// TODO support with annotation
Def2::ValueDef {
identifier_id,
expr_id,
}
}
other => {
unimplemented!(
"I don't yet know how to convert the pattern {:?} into an expr2",
other
)
}
}
}
other => {
unimplemented!(
"I don't know how to make an expr2 from this def yet: {:?}",
other
)
}
};
let spaces_before = &parsed_defs.spaces[parsed_defs.space_before[index].indices()];
let spaces_after = &parsed_defs.spaces[parsed_defs.space_after[index].indices()];
if let Some(comments) = spaces_to_comments(spaces_before) {
let inner_def_id = env.pool.add(def);
def = Def2::CommentsBefore {
comments,
def_id: inner_def_id,
};
}
if let Some(comments) = spaces_to_comments(spaces_after) {
let inner_def_id = env.pool.add(def);
def = Def2::CommentsAfter {
comments,
def_id: inner_def_id,
};
}
result.push(def)
}
result
}
pub fn def_to_def2<'a>(
arena: &'a Bump,
env: &mut Env<'a>,
scope: &mut Scope,
parsed_def: &'a roc_parse::ast::Def<'a>,
region: Region,
) -> Def2 {
use roc_parse::ast::Def::*;
//dbg!(parsed_def);
match parsed_def {
SpaceBefore(inner_def, comments) => {
// filter comments
if !comments.is_empty() && !all_newlines(comments) {
let inner_def = def_to_def2(arena, env, scope, inner_def, region);
let inner_def_id = env.pool.add(inner_def);
let mut all_comments_str = String::new();
for comment in comments.iter().filter(|c_or_nl| !c_or_nl.is_newline()) {
all_comments_str.push_str(&comment.to_string_repr());
}
Def2::CommentsBefore {
comments: all_comments_str,
def_id: inner_def_id,
}
} else {
def_to_def2(arena, env, scope, inner_def, region)
}
}
SpaceAfter(inner_def, comments) => {
// filter comments
if !comments.is_empty() && !all_newlines(comments) {
let inner_def = def_to_def2(arena, env, scope, inner_def, region);
let inner_def_id = env.pool.add(inner_def);
let mut all_comments_str = String::new();
for comment in comments.iter().filter(|c_or_nl| !c_or_nl.is_newline()) {
all_comments_str.push_str(&comment.to_string_repr());
}
Def2::CommentsAfter {
def_id: inner_def_id,
comments: all_comments_str,
}
} else {
def_to_def2(arena, env, scope, inner_def, region)
}
}
Value(roc_parse::ast::ValueDef::Body(&loc_pattern, &loc_expr)) => {
let expr2 = loc_expr_to_expr2(arena, loc_expr, env, scope, region).0;
let expr_id = env.pool.add(expr2);
use roc_parse::ast::Pattern::*;
match loc_pattern.value {
Identifier(id_str) => {
let identifier_id = env.ident_ids.get_or_insert(id_str);
// TODO support with annotation
Def2::ValueDef {
identifier_id,
expr_id,
}
}
other => {
unimplemented!(
"I don't yet know how to convert the pattern {:?} into an expr2",
other
)
}
}
}
other => {
unimplemented!(
"I don't know how to make an expr2 from this def yet: {:?}",
other
)
}
}
}
fn all_newlines(comments: &[CommentOrNewline]) -> bool {
comments
.iter()
.all(|com_or_newline| com_or_newline.is_newline())
}
pub fn str_to_def2<'a>(
arena: &'a Bump,
input: &'a str,
env: &mut Env<'a>,
scope: &mut Scope,
region: Region,
) -> Result<Vec<Def2>, SyntaxError<'a>> {
match roc_parse::test_helpers::parse_defs_with(arena, input.trim()) {
Ok(defs) => Ok(toplevel_defs_to_defs2(arena, env, scope, defs, region)),
Err(fail) => Err(fail),
}
}

View file

@ -0,0 +1,3 @@
pub mod def;
pub mod def2;
pub mod def_to_def2;

View file

@ -0,0 +1,228 @@
use arrayvec::ArrayString;
use roc_types::subs::Variable;
use crate::{
lang::core::{fun_def::FunctionDef, pattern::Pattern2, val_def::ValueDef},
mem_pool::{pool::NodeId, pool_str::PoolStr, pool_vec::PoolVec},
};
use roc_can::expr::Recursive;
use roc_module::called_via::CalledVia;
use roc_module::low_level::LowLevel;
use roc_module::symbol::Symbol;
use super::record_field::RecordField;
pub const ARR_STRING_CAPACITY: usize = 24;
pub type ArrString = ArrayString<ARR_STRING_CAPACITY>;
// TODO make the inner types private?
pub type ExprId = NodeId<Expr2>;
/// An Expr that fits in 32B.
/// It has a 1B discriminant and variants which hold payloads of at most 31B.
#[derive(Debug)]
pub enum Expr2 {
/// A negative number literal without a dot
SmallInt {
number: IntVal, // 16B
var: Variable, // 4B
style: IntStyle, // 1B
text: PoolStr, // 8B
},
// TODO(rvcas): rename this eventually
/// A large (over 64-bit) negative number literal without a dot.
/// This variant can't use IntVal because if IntVal stored 128-bit
/// integers, it would be 32B on its own because of alignment.
I128 {
number: i128, // 16B
var: Variable, // 4B
style: IntStyle, // 1B
text: PoolStr, // 8B
},
// TODO(rvcas): rename this eventually
/// A large (over 64-bit) nonnegative number literal without a dot
/// This variant can't use IntVal because if IntVal stored 128-bit
/// integers, it would be 32B on its own because of alignment.
U128 {
number: u128, // 16B
var: Variable, // 4B
style: IntStyle, // 1B
text: PoolStr, // 8B
},
/// A floating-point literal (with a dot)
Float {
number: FloatVal, // 16B
var: Variable, // 4B
text: PoolStr, // 8B
},
/// string literals of length up to 30B
SmallStr(ArrString), // 31B
/// string literals of length 31B or more
Str(PoolStr), // 8B
// Lookups
Var(Symbol), // 8B
InvalidLookup(PoolStr), // 8B
List {
elem_var: Variable, // 4B
elems: PoolVec<ExprId>, // 8B
},
If {
cond_var: Variable, // 4B
expr_var: Variable, // 4B
branches: PoolVec<(ExprId, ExprId)>, // 8B
final_else: ExprId, // 4B
},
When {
cond_var: Variable, // 4B
expr_var: Variable, // 4B
branches: PoolVec<WhenBranch>, // 8B
cond: ExprId, // 4B
},
LetRec {
defs: PoolVec<FunctionDef>, // 8B
body_var: Variable, // 8B
body_id: ExprId, // 4B
},
LetFunction {
def_id: NodeId<FunctionDef>, // 4B
body_var: Variable, // 8B
body_id: ExprId, // 4B
},
LetValue {
def_id: NodeId<ValueDef>, // 4B
body_id: ExprId, // 4B
body_var: Variable, // 4B
},
Call {
args: PoolVec<(Variable, ExprId)>, // 8B
expr_id: ExprId, // 4B
expr_var: Variable, // 4B
fn_var: Variable, // 4B
closure_var: Variable, // 4B
called_via: CalledVia, // 2B
},
RunLowLevel {
op: LowLevel, // 1B
args: PoolVec<(Variable, ExprId)>, // 8B
ret_var: Variable, // 4B
},
Closure {
args: PoolVec<(Variable, NodeId<Pattern2>)>, // 8B
uniq_symbol: Symbol, // 8B This is a globally unique symbol for the closure
body_id: ExprId, // 4B
function_type: Variable, // 4B
recursive: Recursive, // 1B
extra: NodeId<ClosureExtra>, // 4B
},
// Product Types
Record {
record_var: Variable, // 4B
fields: PoolVec<RecordField>, // 8B
},
/// Empty record constant
EmptyRecord,
/// Look up exactly one field on a record, e.g. (expr).foo.
Access {
field: PoolStr, // 4B
expr: ExprId, // 4B
record_var: Variable, // 4B
ext_var: Variable, // 4B
field_var: Variable, // 4B
},
/// field accessor as a function, e.g. (.foo) expr
Accessor {
function_var: Variable, // 4B
closure_var: Variable, // 4B
field: PoolStr, // 4B
record_var: Variable, // 4B
ext_var: Variable, // 4B
field_var: Variable, // 4B
},
Update {
symbol: Symbol, // 8B
updates: PoolVec<RecordField>, // 8B
record_var: Variable, // 4B
ext_var: Variable, // 4B
},
// Sum Types
Tag {
name: PoolStr, // 4B
variant_var: Variable, // 4B
ext_var: Variable, // 4B
arguments: PoolVec<(Variable, ExprId)>, // 8B
},
Blank, // Rendered as empty box in editor
// Compiles, but will crash if reached
RuntimeError(/* TODO make a version of RuntimeError that fits in 15B */),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Problem {
RanOutOfNodeIds,
}
pub type Res<T> = Result<T, Problem>;
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum IntStyle {
Decimal,
Octal,
Hex,
Binary,
}
impl IntStyle {
pub fn from_base(base: roc_parse::ast::Base) -> Self {
use roc_parse::ast::Base;
match base {
Base::Decimal => Self::Decimal,
Base::Octal => Self::Octal,
Base::Hex => Self::Hex,
Base::Binary => Self::Binary,
}
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum IntVal {
I64(i64),
U64(u64),
I32(i32),
U32(u32),
I16(i16),
U16(u16),
I8(i8),
U8(u8),
}
#[test]
fn size_of_intval() {
assert_eq!(std::mem::size_of::<IntVal>(), 16);
}
#[derive(Debug, Copy, Clone, PartialEq)]
pub enum FloatVal {
F64(f64),
F32(f32),
}
#[derive(Debug)]
pub struct WhenBranch {
pub patterns: PoolVec<Pattern2>, // 4B
pub body: ExprId, // 3B
pub guard: Option<ExprId>, // 4B
}
/// This is overflow data from a Closure variant, which needs to store
/// more than 32B of total data
#[derive(Debug)]
pub struct ClosureExtra {
pub return_type: Variable, // 4B
pub captured_symbols: PoolVec<(Symbol, Variable)>, // 8B
pub closure_type: Variable, // 4B
pub closure_ext_var: Variable, // 4B
}

View file

@ -0,0 +1,162 @@
use crate::{
lang::core::{expr::record_field::RecordField, val_def::value_def_to_string},
mem_pool::pool::Pool,
};
use super::expr2::{Expr2, ExprId};
use roc_types::subs::Variable;
pub fn expr2_to_string(node_id: ExprId, pool: &Pool) -> String {
let mut full_string = String::new();
let expr2 = pool.get(node_id);
expr2_to_string_helper(expr2, 0, pool, &mut full_string);
full_string
}
fn get_spacing(indent_level: usize) -> String {
std::iter::repeat(" ")
.take(indent_level)
.collect::<Vec<&str>>()
.join("")
}
fn expr2_to_string_helper(
expr2: &Expr2,
indent_level: usize,
pool: &Pool,
out_string: &mut String,
) {
out_string.push_str(&get_spacing(indent_level));
match expr2 {
Expr2::SmallStr(arr_string) => out_string.push_str(&format!(
"{}{}{}",
"SmallStr(\"",
arr_string.as_str(),
"\")",
)),
Expr2::Str(pool_str) => {
out_string.push_str(&format!("{}{}{}", "Str(\"", pool_str.as_str(pool), "\")",))
}
Expr2::Blank => out_string.push_str("Blank"),
Expr2::EmptyRecord => out_string.push_str("EmptyRecord"),
Expr2::Record { record_var, fields } => {
out_string.push_str("Record:\n");
out_string.push_str(&var_to_string(record_var, indent_level + 1));
out_string.push_str(&format!("{}fields: [\n", get_spacing(indent_level + 1)));
let mut first_child = true;
for field in fields.iter(pool) {
if !first_child {
out_string.push_str(", ")
} else {
first_child = false;
}
match field {
RecordField::InvalidLabelOnly(pool_str, var) => {
out_string.push_str(&format!(
"{}({}, Var({:?})",
get_spacing(indent_level + 2),
pool_str.as_str(pool),
var,
));
}
RecordField::LabelOnly(pool_str, var, symbol) => {
out_string.push_str(&format!(
"{}({}, Var({:?}), Symbol({:?})",
get_spacing(indent_level + 2),
pool_str.as_str(pool),
var,
symbol
));
}
RecordField::LabeledValue(pool_str, var, val_node_id) => {
out_string.push_str(&format!(
"{}({}, Var({:?}), Expr2(\n",
get_spacing(indent_level + 2),
pool_str.as_str(pool),
var,
));
let val_expr2 = pool.get(*val_node_id);
expr2_to_string_helper(val_expr2, indent_level + 3, pool, out_string);
out_string.push_str(&format!("{})\n", get_spacing(indent_level + 2)));
}
}
}
out_string.push_str(&format!("{}]\n", get_spacing(indent_level + 1)));
}
Expr2::List { elem_var, elems } => {
out_string.push_str("List:\n");
out_string.push_str(&var_to_string(elem_var, indent_level + 1));
out_string.push_str(&format!("{}elems: [\n", get_spacing(indent_level + 1)));
let mut first_elt = true;
for elem_expr2_id in elems.iter(pool) {
if !first_elt {
out_string.push_str(", ")
} else {
first_elt = false;
}
let elem_expr2 = pool.get(*elem_expr2_id);
expr2_to_string_helper(elem_expr2, indent_level + 2, pool, out_string)
}
out_string.push_str(&format!("{}]\n", get_spacing(indent_level + 1)));
}
Expr2::InvalidLookup(pool_str) => {
out_string.push_str(&format!("InvalidLookup({})", pool_str.as_str(pool)));
}
Expr2::SmallInt { text, .. } => {
out_string.push_str(&format!("SmallInt({})", text.as_str(pool)));
}
Expr2::LetValue {
def_id, body_id, ..
} => {
out_string.push_str(&format!(
"LetValue(def_id: >>{:?}), body_id: >>{:?})",
value_def_to_string(pool.get(*def_id), pool),
pool.get(*body_id)
));
}
Expr2::Call { .. } => {
out_string.push_str(&format!("Call({:?})", expr2,));
}
Expr2::Closure { args, .. } => {
out_string.push_str("Closure:\n");
out_string.push_str(&format!("{}args: [\n", get_spacing(indent_level + 1)));
for (_, pattern_id) in args.iter(pool) {
let arg_pattern2 = pool.get(*pattern_id);
out_string.push_str(&format!(
"{}{:?}\n",
get_spacing(indent_level + 2),
arg_pattern2
));
}
}
&Expr2::Var { .. } => {
out_string.push_str(&format!("{:?}", expr2,));
}
Expr2::RuntimeError { .. } => {
out_string.push_str("RuntimeError\n");
}
other => todo!("Implement for {:?}", other),
}
out_string.push('\n');
}
fn var_to_string(some_var: &Variable, indent_level: usize) -> String {
format!("{}Var({:?})\n", get_spacing(indent_level + 1), some_var)
}

View file

@ -0,0 +1,707 @@
use bumpalo::Bump;
use roc_can::expr::{IntValue, Recursive};
use roc_can::num::{
finish_parsing_base, finish_parsing_float, finish_parsing_num, ParsedNumResult,
};
use roc_can::operator::desugar_expr;
use roc_collections::all::MutSet;
use roc_module::symbol::Symbol;
use roc_parse::{ast::Expr, pattern::PatternType};
use roc_problem::can::{Problem, RuntimeError};
use roc_region::all::{Loc, Region};
use super::{expr2::Expr2, output::Output};
use crate::canonicalization::canonicalize::{
canonicalize_fields, canonicalize_lookup, canonicalize_when_branch, CanonicalizeRecordProblem,
};
use crate::lang::core::declaration::decl_to_let;
use crate::lang::core::def::def::{canonicalize_defs, sort_can_defs};
use crate::lang::core::expr::expr2::ClosureExtra;
use crate::lang::core::pattern::to_pattern2;
use crate::lang::core::str::flatten_str_literal;
use crate::mem_pool::shallow_clone::ShallowClone;
use crate::{
lang::{
core::expr::expr2::{ExprId, FloatVal, IntStyle, IntVal},
env::Env,
scope::Scope,
},
mem_pool::{pool_str::PoolStr, pool_vec::PoolVec},
};
pub fn loc_expr_to_expr2<'a>(
arena: &'a Bump,
loc_expr: Loc<Expr<'a>>,
env: &mut Env<'a>,
scope: &mut Scope,
region: Region,
) -> (Expr2, Output) {
let desugared_loc_expr = desugar_expr(arena, arena.alloc(loc_expr));
expr_to_expr2(env, scope, arena.alloc(desugared_loc_expr.value), region)
}
const ZERO: Region = Region::zero();
pub fn expr_to_expr2<'a>(
env: &mut Env<'a>,
scope: &mut Scope,
parse_expr: &'a roc_parse::ast::Expr<'a>,
region: Region,
) -> (Expr2, self::Output) {
use roc_parse::ast::Expr::*;
//dbg!("{:?}", parse_expr);
match parse_expr {
Float(string) => {
match finish_parsing_float(string) {
Ok((string_without_suffix, float, _bound)) => {
let expr = Expr2::Float {
number: FloatVal::F64(float),
var: env.var_store.fresh(),
text: PoolStr::new(string_without_suffix, env.pool),
};
(expr, Output::default())
}
Err((raw, error)) => {
// emit runtime error
let runtime_error = RuntimeError::InvalidFloat(error, ZERO, raw.into());
env.problem(Problem::RuntimeError(runtime_error));
//
// Expr::RuntimeError(runtime_error)
todo!()
}
}
}
Num(string) => {
match finish_parsing_num(string) {
Ok((
parsed,
ParsedNumResult::UnknownNum(int, _) | ParsedNumResult::Int(int, _),
)) => {
let expr = Expr2::SmallInt {
number: IntVal::I64(match int {
IntValue::U128(_) => todo!(),
IntValue::I128(n) => i128::from_ne_bytes(n) as i64, // FIXME
}),
var: env.var_store.fresh(),
// TODO non-hardcode
style: IntStyle::Decimal,
text: PoolStr::new(parsed, env.pool),
};
(expr, Output::default())
}
Ok((parsed, ParsedNumResult::Float(float, _))) => {
let expr = Expr2::Float {
number: FloatVal::F64(float),
var: env.var_store.fresh(),
text: PoolStr::new(parsed, env.pool),
};
(expr, Output::default())
}
Err((raw, error)) => {
// emit runtime error
let runtime_error = RuntimeError::InvalidInt(
error,
roc_parse::ast::Base::Decimal,
ZERO,
raw.into(),
);
env.problem(Problem::RuntimeError(runtime_error));
//
// Expr::RuntimeError(runtime_error)
todo!()
}
}
}
NonBase10Int {
string,
base,
is_negative,
} => {
match finish_parsing_base(string, *base, *is_negative) {
Ok((int, _bound)) => {
let expr = Expr2::SmallInt {
number: IntVal::I64(match int {
IntValue::U128(_) => todo!(),
IntValue::I128(n) => i128::from_ne_bytes(n) as i64, // FIXME
}),
var: env.var_store.fresh(),
// TODO non-hardcode
style: IntStyle::from_base(*base),
text: PoolStr::new(string, env.pool),
};
(expr, Output::default())
}
Err((raw, error)) => {
// emit runtime error
let runtime_error = RuntimeError::InvalidInt(error, *base, ZERO, raw.into());
env.problem(Problem::RuntimeError(runtime_error));
//
// Expr::RuntimeError(runtime_error)
todo!()
}
}
}
Str(literal) => flatten_str_literal(env, scope, literal),
List(items) => {
let mut output = Output::default();
let output_ref = &mut output;
let elems: PoolVec<ExprId> = PoolVec::with_capacity(items.len() as u32, env.pool);
for (node_id, item) in elems.iter_node_ids().zip(items.iter()) {
let (expr, sub_output) = expr_to_expr2(env, scope, &item.value, item.region);
output_ref.union(sub_output);
let expr_id = env.pool.add(expr);
env.pool[node_id] = expr_id;
}
let expr = Expr2::List {
elem_var: env.var_store.fresh(),
elems,
};
(expr, output)
}
Tag(tag) => {
// a tag without any arguments
(
Expr2::Tag {
name: PoolStr::new(tag, env.pool),
variant_var: env.var_store.fresh(),
ext_var: env.var_store.fresh(),
arguments: PoolVec::empty(env.pool),
},
Output::default(),
)
}
RecordUpdate {
fields,
update: loc_update,
} => {
let (can_update, update_out) =
expr_to_expr2(env, scope, &loc_update.value, loc_update.region);
if let Expr2::Var(symbol) = &can_update {
match canonicalize_fields(env, scope, fields.items) {
Ok((can_fields, mut output)) => {
output.references.union_mut(update_out.references);
let answer = Expr2::Update {
record_var: env.var_store.fresh(),
ext_var: env.var_store.fresh(),
symbol: *symbol,
updates: can_fields,
};
(answer, output)
}
Err(CanonicalizeRecordProblem::InvalidOptionalValue {
field_name: _,
field_region: _,
record_region: _,
}) => {
// let runtime_error = roc_problem::can::RuntimeError::InvalidOptionalValue {
// field_name,
// field_region,
// record_region,
// };
//
// env.problem(Problem::RuntimeError(runtime_error));
todo!()
}
}
} else {
// only (optionally qualified) variables can be updated, not arbitrary expressions
// let error = roc_problem::can::RuntimeError::InvalidRecordUpdate {
// region: can_update.region,
// };
//
// let answer = Expr::RuntimeError(error.clone());
//
// env.problems.push(Problem::RuntimeError(error));
//
// (answer, Output::default())
todo!("{:?}", &can_update)
}
}
Record(fields) => {
if fields.is_empty() {
(Expr2::EmptyRecord, Output::default())
} else {
match canonicalize_fields(env, scope, fields.items) {
Ok((can_fields, output)) => (
Expr2::Record {
record_var: env.var_store.fresh(),
fields: can_fields,
},
output,
),
Err(CanonicalizeRecordProblem::InvalidOptionalValue {
field_name: _,
field_region: _,
record_region: _,
}) => {
// let runtime_error = RuntimeError::InvalidOptionalValue {
// field_name,
// field_region,
// record_region,
// };
//
// env.problem(runtime_error);
// (
// Expr::RuntimeError(
// ),
// Output::default(),
//
// )
todo!()
}
}
}
}
Access(record_expr, field) => {
// TODO
let region = ZERO;
let (record_expr_id, output) = to_expr_id(env, scope, record_expr, region);
(
Expr2::Access {
record_var: env.var_store.fresh(),
field_var: env.var_store.fresh(),
ext_var: env.var_store.fresh(),
expr: record_expr_id,
field: PoolStr::new(field, env.pool),
},
output,
)
}
AccessorFunction(field) => (
Expr2::Accessor {
function_var: env.var_store.fresh(),
record_var: env.var_store.fresh(),
ext_var: env.var_store.fresh(),
closure_var: env.var_store.fresh(),
field_var: env.var_store.fresh(),
field: PoolStr::new(field, env.pool),
},
Output::default(),
),
If(branches, final_else) => {
let mut new_branches = Vec::with_capacity(branches.len());
let mut output = Output::default();
for (condition, then_branch) in branches.iter() {
let (cond, cond_output) =
expr_to_expr2(env, scope, &condition.value, condition.region);
let (then_expr, then_output) =
expr_to_expr2(env, scope, &then_branch.value, then_branch.region);
output.references.union_mut(cond_output.references);
output.references.union_mut(then_output.references);
new_branches.push((env.pool.add(cond), env.pool.add(then_expr)));
}
let (else_expr, else_output) =
expr_to_expr2(env, scope, &final_else.value, final_else.region);
output.references.union_mut(else_output.references);
let expr = Expr2::If {
cond_var: env.var_store.fresh(),
expr_var: env.var_store.fresh(),
branches: PoolVec::new(new_branches.into_iter(), env.pool),
final_else: env.pool.add(else_expr),
};
(expr, output)
}
When(loc_cond, branches) => {
// Infer the condition expression's type.
let cond_var = env.var_store.fresh();
let (can_cond, mut output) =
expr_to_expr2(env, scope, &loc_cond.value, loc_cond.region);
// the condition can never be a tail-call
output.tail_call = None;
let can_branches = PoolVec::with_capacity(branches.len() as u32, env.pool);
for (node_id, branch) in can_branches.iter_node_ids().zip(branches.iter()) {
let (can_when_branch, branch_references) =
canonicalize_when_branch(env, scope, *branch, &mut output);
output.references.union_mut(branch_references);
env.pool[node_id] = can_when_branch;
}
// A "when" with no branches is a runtime error, but it will mess things up
// if code gen mistakenly thinks this is a tail call just because its condition
// happened to be one. (The condition gave us our initial output value.)
if branches.is_empty() {
output.tail_call = None;
}
// Incorporate all three expressions into a combined Output value.
let expr = Expr2::When {
expr_var: env.var_store.fresh(),
cond_var,
cond: env.pool.add(can_cond),
branches: can_branches,
};
(expr, output)
}
Closure(loc_arg_patterns, loc_body_expr) => {
// The globally unique symbol that will refer to this closure once it gets converted
// into a top-level procedure for code gen.
//
// In the Foo module, this will look something like Foo.$1 or Foo.$2.
let symbol = env
.closure_name_symbol
.unwrap_or_else(|| env.gen_unique_symbol());
env.closure_name_symbol = None;
// The body expression gets a new scope for canonicalization.
// Shadow `scope` to make sure we don't accidentally use the original one for the
// rest of this block, but keep the original around for later diffing.
let original_scope = scope;
let mut scope = original_scope.shallow_clone();
let can_args = PoolVec::with_capacity(loc_arg_patterns.len() as u32, env.pool);
let mut output = Output::default();
let mut bound_by_argument_patterns = MutSet::default();
for (node_id, loc_pattern) in can_args.iter_node_ids().zip(loc_arg_patterns.iter()) {
let (new_output, can_arg) = to_pattern2(
env,
&mut scope,
roc_parse::pattern::PatternType::FunctionArg,
&loc_pattern.value,
loc_pattern.region,
);
bound_by_argument_patterns
.extend(new_output.references.bound_symbols.iter().copied());
output.union(new_output);
let pattern_id = env.add(can_arg, loc_pattern.region);
env.pool[node_id] = (env.var_store.fresh(), pattern_id);
}
let (body_expr, new_output) =
expr_to_expr2(env, &mut scope, &loc_body_expr.value, loc_body_expr.region);
let mut captured_symbols: MutSet<Symbol> =
new_output.references.lookups.iter().copied().collect();
// filter out the closure's name itself
captured_symbols.remove(&symbol);
// symbols bound either in this pattern or deeper down are not captured!
captured_symbols.retain(|s| !new_output.references.bound_symbols.contains(s));
captured_symbols.retain(|s| !bound_by_argument_patterns.contains(s));
// filter out top-level symbols
// those will be globally available, and don't need to be captured
captured_symbols.retain(|s| !env.top_level_symbols.contains(s));
// filter out imported symbols
// those will be globally available, and don't need to be captured
captured_symbols.retain(|s| s.module_id() == env.home);
// TODO any Closure that has an empty `captured_symbols` list could be excluded!
output.union(new_output);
// filter out aliases
captured_symbols.retain(|s| !output.references.referenced_aliases.contains(s));
// filter out functions that don't close over anything
captured_symbols.retain(|s| !output.non_closures.contains(s));
// Now that we've collected all the references, check to see if any of the args we defined
// went unreferenced. If any did, report them as unused arguments.
for (sub_symbol, region) in scope.symbols() {
if !original_scope.contains_symbol(sub_symbol) {
if !output.references.has_lookup(sub_symbol) {
// The body never referenced this argument we declared. It's an unused argument!
env.problem(Problem::UnusedArgument(symbol, sub_symbol, region));
}
// We shouldn't ultimately count arguments as referenced locals. Otherwise,
// we end up with weird conclusions like the expression (\x -> x + 1)
// references the (nonexistent) local variable x!
output.references.lookups.remove(&sub_symbol);
}
}
env.register_closure(symbol, output.references.clone());
let mut captured_symbols: Vec<_> = captured_symbols
.into_iter()
.map(|s| (s, env.var_store.fresh()))
.collect();
// sort symbols, so we know the order in which they're stored in the closure record
captured_symbols.sort();
// store that this function doesn't capture anything. It will be promoted to a
// top-level function, and does not need to be captured by other surrounding functions.
if captured_symbols.is_empty() {
output.non_closures.insert(symbol);
}
let captured_symbols = PoolVec::new(captured_symbols.into_iter(), env.pool);
let extra = ClosureExtra {
return_type: env.var_store.fresh(), // 4B
captured_symbols, // 8B
closure_type: env.var_store.fresh(), // 4B
closure_ext_var: env.var_store.fresh(), // 4B
};
(
Expr2::Closure {
function_type: env.var_store.fresh(),
uniq_symbol: symbol,
recursive: Recursive::NotRecursive,
args: can_args,
body_id: env.add(body_expr, loc_body_expr.region),
extra: env.pool.add(extra),
},
output,
)
}
Apply(loc_fn, loc_args, application_style) => {
// The expression that evaluates to the function being called, e.g. `foo` in
// (foo) bar baz
let fn_region = loc_fn.region;
// Canonicalize the function expression and its arguments
let (fn_expr, mut output) = expr_to_expr2(env, scope, &loc_fn.value, fn_region);
// The function's return type
let args = PoolVec::with_capacity(loc_args.len() as u32, env.pool);
for (node_id, loc_arg) in args.iter_node_ids().zip(loc_args.iter()) {
let (arg_expr_id, arg_out) = to_expr_id(env, scope, &loc_arg.value, loc_arg.region);
env.pool[node_id] = (env.var_store.fresh(), arg_expr_id);
output.references.union_mut(arg_out.references);
}
// Default: We're not tail-calling a symbol (by name), we're tail-calling a function value.
output.tail_call = None;
let expr = match fn_expr {
Expr2::Var(ref symbol) => {
output.references.calls.insert(*symbol);
// we're tail-calling a symbol by name, check if it's the tail-callable symbol
output.tail_call = match &env.tailcallable_symbol {
Some(tc_sym) if *tc_sym == *symbol => Some(*symbol),
Some(_) | None => None,
};
// IDEA: Expr2::CallByName?
let fn_expr_id = env.add(fn_expr, fn_region);
Expr2::Call {
args,
expr_id: fn_expr_id,
expr_var: env.var_store.fresh(),
fn_var: env.var_store.fresh(),
closure_var: env.var_store.fresh(),
called_via: *application_style,
}
}
Expr2::RuntimeError() => {
// We can't call a runtime error; bail out by propagating it!
return (fn_expr, output);
}
Expr2::Tag {
variant_var,
ext_var,
name,
..
} => Expr2::Tag {
variant_var,
ext_var,
name,
arguments: args,
},
_ => {
// This could be something like ((if True then fn1 else fn2) arg1 arg2).
let fn_expr_id = env.add(fn_expr, fn_region);
Expr2::Call {
args,
expr_id: fn_expr_id,
expr_var: env.var_store.fresh(),
fn_var: env.var_store.fresh(),
closure_var: env.var_store.fresh(),
called_via: *application_style,
}
}
};
(expr, output)
}
Defs(loc_defs, loc_ret) => {
let (unsorted, mut scope, defs_output, symbols_introduced) = canonicalize_defs(
env,
Output::default(),
scope,
loc_defs,
PatternType::DefExpr,
);
// The def as a whole is a tail call iff its return expression is a tail call.
// Use its output as a starting point because its tail_call already has the right answer!
let (ret_expr, mut output) =
expr_to_expr2(env, &mut scope, &loc_ret.value, loc_ret.region);
output
.introduced_variables
.union(&defs_output.introduced_variables);
output.references.union_mut(defs_output.references);
// Now that we've collected all the references, check to see if any of the new idents
// we defined went unused by the return expression. If any were unused, report it.
for (symbol, region) in symbols_introduced {
if !output.references.has_lookup(symbol) {
env.problem(Problem::UnusedDef(symbol, region));
}
}
let (can_defs, output) = sort_can_defs(env, unsorted, output);
match can_defs {
Ok(decls) => {
let mut expr = ret_expr;
for declaration in decls.into_iter().rev() {
expr = decl_to_let(env.pool, env.var_store, declaration, expr);
}
(expr, output)
}
Err(_err) => {
// TODO: fix this to be something from Expr2
// (RuntimeError(err), output)
todo!()
}
}
}
PrecedenceConflict { .. } => {
// use roc_problem::can::RuntimeError::*;
//
// let problem = PrecedenceProblem::BothNonAssociative(
// *whole_region,
// binop1.clone(),
// binop2.clone(),
// );
//
// env.problem(Problem::PrecedenceProblem(problem.clone()));
//
// (
// RuntimeError(InvalidPrecedence(problem, region)),
// Output::default(),
// )
todo!()
}
MalformedClosure => {
// use roc_problem::can::RuntimeError::*;
// (RuntimeError(MalformedClosure(region)), Output::default())
todo!()
}
MalformedIdent(_name, _problem) => {
// use roc_problem::can::RuntimeError::*;
//
// let problem = MalformedIdentifier((*name).into(), region);
// env.problem(Problem::RuntimeError(problem.clone()));
//
// (RuntimeError(problem), Output::default())
todo!()
}
Var {
module_name, // module_name will only be filled if the original Roc code stated something like `5 + SomeModule.myVar`, module_name will be blank if it was `5 + myVar`
ident,
} => canonicalize_lookup(env, scope, module_name, ident, region),
// Below this point, we shouln't see any of these nodes anymore because
// operator desugaring should have removed them!
bad_expr @ ParensAround(_) => {
panic!(
"A ParensAround did not get removed during operator desugaring somehow: {:#?}",
bad_expr
);
}
bad_expr @ SpaceBefore(_, _) => {
panic!(
"A SpaceBefore did not get removed during operator desugaring somehow: {:#?}",
bad_expr
);
}
bad_expr @ SpaceAfter(_, _) => {
panic!(
"A SpaceAfter did not get removed during operator desugaring somehow: {:#?}",
bad_expr
);
}
bad_expr @ BinOps { .. } => {
panic!(
"A binary operator chain did not get desugared somehow: {:#?}",
bad_expr
);
}
bad_expr @ UnaryOp(_, _) => {
panic!(
"A unary operator did not get desugared somehow: {:#?}",
bad_expr
);
}
rest => todo!("not yet implemented {:?}", rest),
}
}
pub fn to_expr_id<'a>(
env: &mut Env<'a>,
scope: &mut Scope,
parse_expr: &'a roc_parse::ast::Expr<'a>,
region: Region,
) -> (ExprId, Output) {
let (expr, output) = expr_to_expr2(env, scope, parse_expr, region);
(env.add(expr, region), output)
}

View file

@ -0,0 +1,51 @@
use roc_collections::all::MutMap;
use roc_module::ident::Lowercase;
use roc_module::symbol::Symbol;
use roc_types::subs::Variable;
#[derive(Clone, Debug, PartialEq, Default)]
pub struct IntroducedVariables {
// Rigids must be unique within a type annotation.
// E.g. in `identity : a -> a`, there should only be one
// variable (a rigid one, with name "a").
// Hence `rigids : Map<Lowercase, Variable>`
//
// But then between annotations, the same name can occur multiple times,
// but a variable can only have one name. Therefore
// `ftv : Map<Variable, Lowercase>`.
pub wildcards: Vec<Variable>,
pub var_by_name: MutMap<Lowercase, Variable>,
pub name_by_var: MutMap<Variable, Lowercase>,
pub host_exposed_aliases: MutMap<Symbol, Variable>,
}
impl IntroducedVariables {
pub fn insert_named(&mut self, name: Lowercase, var: Variable) {
self.var_by_name.insert(name.clone(), var);
self.name_by_var.insert(var, name);
}
pub fn insert_wildcard(&mut self, var: Variable) {
self.wildcards.push(var);
}
pub fn insert_host_exposed_alias(&mut self, symbol: Symbol, var: Variable) {
self.host_exposed_aliases.insert(symbol, var);
}
pub fn union(&mut self, other: &Self) {
self.wildcards.extend(other.wildcards.iter().cloned());
self.var_by_name.extend(other.var_by_name.clone());
self.name_by_var.extend(other.name_by_var.clone());
self.host_exposed_aliases
.extend(other.host_exposed_aliases.clone());
}
pub fn var_by_name(&self, name: &Lowercase) -> Option<&Variable> {
self.var_by_name.get(name)
}
pub fn name_by_var(&self, var: Variable) -> Option<&Lowercase> {
self.name_by_var.get(&var)
}
}

View file

@ -0,0 +1,6 @@
pub mod expr2;
pub mod expr2_to_string;
pub mod expr_to_expr2;
mod introduced_vars;
pub(crate) mod output;
pub mod record_field;

View file

@ -0,0 +1,30 @@
use crate::{
lang::core::{def::def::References, types::Alias},
mem_pool::pool::NodeId,
};
use roc_collections::all::{MutMap, MutSet};
use roc_module::symbol::Symbol;
use super::introduced_vars::IntroducedVariables;
#[derive(Clone, Default, Debug, PartialEq)]
pub struct Output {
pub references: References,
pub tail_call: Option<Symbol>,
pub introduced_variables: IntroducedVariables,
pub aliases: MutMap<Symbol, NodeId<Alias>>,
pub non_closures: MutSet<Symbol>,
}
impl Output {
pub fn union(&mut self, other: Self) {
self.references.union_mut(other.references);
if let (None, Some(later)) = (self.tail_call, other.tail_call) {
self.tail_call = Some(later);
}
self.aliases.extend(other.aliases);
self.non_closures.extend(other.non_closures);
}
}

View file

@ -0,0 +1,49 @@
use roc_types::subs::Variable;
use crate::mem_pool::pool_str::PoolStr;
use roc_module::symbol::Symbol;
use super::expr2::ExprId;
#[derive(Debug)]
pub enum RecordField {
InvalidLabelOnly(PoolStr, Variable),
LabelOnly(PoolStr, Variable, Symbol),
LabeledValue(PoolStr, Variable, ExprId),
}
use RecordField::*;
impl RecordField {
pub fn get_record_field_var(&self) -> &Variable {
match self {
InvalidLabelOnly(_, var) => var,
LabelOnly(_, var, _) => var,
LabeledValue(_, var, _) => var,
}
}
pub fn get_record_field_pool_str(&self) -> &PoolStr {
match self {
InvalidLabelOnly(pool_str, _) => pool_str,
LabelOnly(pool_str, _, _) => pool_str,
LabeledValue(pool_str, _, _) => pool_str,
}
}
pub fn get_record_field_pool_str_mut(&mut self) -> &mut PoolStr {
match self {
InvalidLabelOnly(pool_str, _) => pool_str,
LabelOnly(pool_str, _, _) => pool_str,
LabeledValue(pool_str, _, _) => pool_str,
}
}
pub fn get_record_field_val_node_id(&self) -> Option<ExprId> {
match self {
InvalidLabelOnly(_, _) => None,
LabelOnly(_, _, _) => None,
LabeledValue(_, _, field_val_id) => Some(*field_val_id),
}
}
}

View file

@ -0,0 +1,61 @@
use crate::{
lang::rigids::Rigids,
mem_pool::{pool::NodeId, pool_vec::PoolVec, shallow_clone::ShallowClone},
};
use roc_module::symbol::Symbol;
use roc_types::subs::Variable;
use super::{
expr::expr2::ExprId,
pattern::PatternId,
types::{Type2, TypeId},
};
#[derive(Debug)]
pub enum FunctionDef {
WithAnnotation {
name: Symbol, // 8B
arguments: PoolVec<(NodeId<Type2>, PatternId)>, // 8B
rigids: NodeId<Rigids>, // 4B
return_type: TypeId, // 4B
body_id: ExprId, // 4B
},
NoAnnotation {
name: Symbol, // 8B
arguments: PoolVec<(Variable, PatternId)>, // 8B
return_var: Variable, // 4B
body_id: ExprId, // 4B
},
}
impl ShallowClone for FunctionDef {
fn shallow_clone(&self) -> Self {
match self {
Self::WithAnnotation {
name,
arguments,
rigids,
return_type,
body_id,
} => Self::WithAnnotation {
name: *name,
arguments: arguments.shallow_clone(),
rigids: *rigids,
return_type: *return_type,
body_id: *body_id,
},
Self::NoAnnotation {
name,
arguments,
return_var,
body_id,
} => Self::NoAnnotation {
name: *name,
arguments: arguments.shallow_clone(),
return_var: *return_var,
body_id: *body_id,
},
}
}
}

View file

@ -0,0 +1,10 @@
use super::expr::expr2::ExprId;
#[derive(Debug)]
pub struct AppHeader {
pub app_name: String,
pub packages_base: String,
pub imports: Vec<String>,
pub provides: Vec<String>,
pub ast_node_id: ExprId, // TODO probably want to create and use HeaderId
}

View file

@ -0,0 +1,10 @@
pub mod ast;
mod declaration;
pub mod def;
pub mod expr;
pub mod fun_def;
pub mod header;
pub mod pattern;
pub mod str;
pub mod types;
pub mod val_def;

View file

@ -0,0 +1,651 @@
#![allow(clippy::all)]
#![allow(dead_code)]
#![allow(unused_imports)]
use bumpalo::collections::Vec as BumpVec;
use roc_can::expr::{unescape_char, IntValue};
use roc_can::num::{
finish_parsing_base, finish_parsing_float, finish_parsing_num, ParsedNumResult,
};
use roc_collections::all::BumpMap;
use roc_error_macros::todo_opaques;
use roc_module::symbol::{Interns, Symbol};
use roc_parse::ast::{StrLiteral, StrSegment};
use roc_parse::pattern::PatternType;
use roc_problem::can::{MalformedPatternProblem, Problem, RuntimeError, ShadowKind};
use roc_region::all::Region;
use roc_types::subs::Variable;
use crate::ast_error::{ASTResult, UnexpectedPattern2VariantSnafu};
use crate::constrain::Constraint;
use crate::lang::core::expr::expr_to_expr2::to_expr_id;
use crate::lang::env::Env;
use crate::lang::scope::Scope;
use crate::mem_pool::pool::{NodeId, Pool};
use crate::mem_pool::pool_str::PoolStr;
use crate::mem_pool::pool_vec::PoolVec;
use crate::mem_pool::shallow_clone::ShallowClone;
use super::expr::expr2::{ExprId, FloatVal, IntVal};
use super::expr::output::Output;
use super::types::Type2;
pub type PatternId = NodeId<Pattern2>;
#[derive(Debug)]
pub enum Pattern2 {
Identifier(Symbol), // 8B
NumLiteral(Variable, i64), // 4B + 8B
IntLiteral(IntVal), // 16B
FloatLiteral(FloatVal), // 16B
StrLiteral(PoolStr), // 8B
CharacterLiteral(char), // 4B
Underscore, // 0B
Tag {
whole_var: Variable, // 4B
ext_var: Variable, // 4B
tag_name: PoolStr, // 8B
arguments: PoolVec<(Variable, PatternId)>, // 8B
},
RecordDestructure {
whole_var: Variable, // 4B
ext_var: Variable, // 4B
destructs: PoolVec<RecordDestruct>, // 8B
},
// Runtime Exceptions
// TODO: figure out how to better handle regions
// to keep this member under 32. With 2 Regions
// it ends up at size 40
Shadowed {
shadowed_ident: PoolStr,
// definition: Region,
// shadowed_at: Region,
},
/// Example: (5 = 1 + 2) is an unsupported pattern in an assignment; Int patterns aren't allowed in assignments!
UnsupportedPattern(Region),
// parse error patterns
MalformedPattern(MalformedPatternProblem, Region),
}
impl ShallowClone for Pattern2 {
fn shallow_clone(&self) -> Self {
todo!()
}
}
#[derive(Debug)]
pub struct PatternState2<'a> {
pub headers: BumpMap<Symbol, Type2>,
pub vars: BumpVec<'a, Variable>,
pub constraints: BumpVec<'a, Constraint<'a>>,
}
#[derive(Debug)]
pub struct RecordDestruct {
pub var: Variable, // 4B
pub label: PoolStr, // 8B
pub symbol: Symbol, // 8B
pub typ: NodeId<DestructType>, // 4B
}
#[derive(Clone, Debug)]
pub enum DestructType {
Required,
Optional(Variable, ExprId), // 4B + 4B
Guard(Variable, PatternId), // 4B + 4B
}
pub fn as_pattern_id<'a>(
env: &mut Env<'a>,
scope: &mut Scope,
pattern_id: PatternId,
pattern_type: PatternType,
pattern: &roc_parse::ast::Pattern<'a>,
region: Region,
) -> Output {
let (output, can_pattern) = to_pattern2(env, scope, pattern_type, pattern, region);
env.pool[pattern_id] = can_pattern;
env.set_region(pattern_id, region);
output
}
pub fn to_pattern_id<'a>(
env: &mut Env<'a>,
scope: &mut Scope,
pattern_type: PatternType,
pattern: &roc_parse::ast::Pattern<'a>,
region: Region,
) -> (Output, PatternId) {
let (output, can_pattern) = to_pattern2(env, scope, pattern_type, pattern, region);
let pattern_id = env.pool.add(can_pattern);
env.set_region(pattern_id, region);
(output, pattern_id)
}
pub fn to_pattern2<'a>(
env: &mut Env<'a>,
scope: &mut Scope,
pattern_type: PatternType,
pattern: &roc_parse::ast::Pattern<'a>,
region: Region,
) -> (Output, Pattern2) {
use roc_parse::ast::Pattern::*;
use PatternType::*;
let mut output = Output::default();
let can_pattern = match pattern {
Identifier(name) => match scope.introduce(
(*name).into(),
&env.exposed_ident_ids,
&mut env.ident_ids,
region,
) {
Ok(symbol) => {
output.references.bound_symbols.insert(symbol);
Pattern2::Identifier(symbol)
}
Err((original_region, shadow)) => {
env.problem(Problem::RuntimeError(RuntimeError::Shadowing {
original_region,
shadow: shadow.clone(),
kind: ShadowKind::Variable,
}));
let name: &str = shadow.value.as_ref();
Pattern2::Shadowed {
shadowed_ident: PoolStr::new(name, env.pool),
}
}
},
QualifiedIdentifier { .. } => {
let problem = MalformedPatternProblem::QualifiedIdentifier;
malformed_pattern(env, problem, region)
}
Underscore(_) => match pattern_type {
WhenBranch | FunctionArg => Pattern2::Underscore,
TopLevelDef | DefExpr => underscore_in_def(env, region),
},
FloatLiteral(ref string) => match pattern_type {
WhenBranch => match finish_parsing_float(string) {
Err(_error) => {
let problem = MalformedPatternProblem::MalformedFloat;
malformed_pattern(env, problem, region)
}
Ok((_, float, _bound)) => Pattern2::FloatLiteral(FloatVal::F64(float)),
},
ptype => unsupported_pattern(env, ptype, region),
},
NumLiteral(string) => match pattern_type {
WhenBranch => match finish_parsing_num(string) {
Err(_error) => {
let problem = MalformedPatternProblem::MalformedInt;
malformed_pattern(env, problem, region)
}
Ok((_, ParsedNumResult::UnknownNum(int, _bound))) => {
Pattern2::NumLiteral(
env.var_store.fresh(),
match int {
IntValue::U128(_) => todo!(),
IntValue::I128(n) => i128::from_ne_bytes(n) as i64, // FIXME
},
)
}
Ok((_, ParsedNumResult::Int(int, _bound))) => {
Pattern2::IntLiteral(IntVal::I64(match int {
IntValue::U128(_) => todo!(),
IntValue::I128(n) => i128::from_ne_bytes(n) as i64, // FIXME
}))
}
Ok((_, ParsedNumResult::Float(int, _bound))) => {
Pattern2::FloatLiteral(FloatVal::F64(int))
}
},
ptype => unsupported_pattern(env, ptype, region),
},
NonBase10Literal {
string,
base,
is_negative,
} => match pattern_type {
WhenBranch => match finish_parsing_base(string, *base, *is_negative) {
Err(_error) => {
let problem = MalformedPatternProblem::MalformedBase(*base);
malformed_pattern(env, problem, region)
}
Ok((int, _bound)) => {
let int = match int {
IntValue::U128(_) => todo!(),
IntValue::I128(n) => i128::from_ne_bytes(n) as i64, // FIXME
};
if *is_negative {
Pattern2::IntLiteral(IntVal::I64(-int))
} else {
Pattern2::IntLiteral(IntVal::I64(int))
}
}
},
ptype => unsupported_pattern(env, ptype, region),
},
StrLiteral(literal) => match pattern_type {
WhenBranch => flatten_str_literal(env.pool, literal),
ptype => unsupported_pattern(env, ptype, region),
},
SingleQuote(string) => match pattern_type {
WhenBranch => {
let mut it = string.chars().peekable();
if let Some(char) = it.next() {
if it.peek().is_none() {
Pattern2::CharacterLiteral(char)
} else {
// multiple chars is found
let problem = MalformedPatternProblem::MultipleCharsInSingleQuote;
malformed_pattern(env, problem, region)
}
} else {
// no characters found
let problem = MalformedPatternProblem::EmptySingleQuote;
malformed_pattern(env, problem, region)
}
}
ptype => unsupported_pattern(env, ptype, region),
},
Tag(name) => {
// Canonicalize the tag's name.
Pattern2::Tag {
whole_var: env.var_store.fresh(),
ext_var: env.var_store.fresh(),
tag_name: PoolStr::new(name, env.pool),
arguments: PoolVec::empty(env.pool),
}
}
OpaqueRef(..) => todo_opaques!(),
Apply(tag, patterns) => {
let can_patterns = PoolVec::with_capacity(patterns.len() as u32, env.pool);
for (loc_pattern, node_id) in (*patterns).iter().zip(can_patterns.iter_node_ids()) {
let (new_output, can_pattern) = to_pattern2(
env,
scope,
pattern_type,
&loc_pattern.value,
loc_pattern.region,
);
output.union(new_output);
let can_pattern_id = env.pool.add(can_pattern);
env.pool[node_id] = (env.var_store.fresh(), can_pattern_id);
}
match tag.value {
Tag(name) => Pattern2::Tag {
whole_var: env.var_store.fresh(),
ext_var: env.var_store.fresh(),
tag_name: PoolStr::new(name, env.pool),
arguments: can_patterns,
},
_ => unreachable!("Other patterns cannot be applied"),
}
}
RecordDestructure(patterns) => {
let ext_var = env.var_store.fresh();
let whole_var = env.var_store.fresh();
let destructs = PoolVec::with_capacity(patterns.len() as u32, env.pool);
let opt_erroneous = None;
for (node_id, loc_pattern) in destructs.iter_node_ids().zip((*patterns).iter()) {
match loc_pattern.value {
Identifier(label) => {
match scope.introduce(
label.into(),
&env.exposed_ident_ids,
&mut env.ident_ids,
region,
) {
Ok(symbol) => {
output.references.bound_symbols.insert(symbol);
let destruct = RecordDestruct {
var: env.var_store.fresh(),
label: PoolStr::new(label, env.pool),
symbol,
typ: env.pool.add(DestructType::Required),
};
env.pool[node_id] = destruct;
env.set_region(node_id, loc_pattern.region);
}
Err((original_region, shadow)) => {
env.problem(Problem::RuntimeError(RuntimeError::Shadowing {
original_region,
shadow: shadow.clone(),
kind: ShadowKind::Variable,
}));
// let shadowed = Pattern2::Shadowed {
// definition: original_region,
// shadowed_at: loc_pattern.region,
// shadowed_ident: shadow.value,
// };
// No matter what the other patterns
// are, we're definitely shadowed and will
// get a runtime exception as soon as we
// encounter the first bad pattern.
// opt_erroneous = Some();
// env.pool[node_id] = sha;
// env.set_region(node_id, loc_pattern.region);
todo!("we must both report/store the problem, but also not lose any information")
}
};
}
RequiredField(label, loc_guard) => {
// a guard does not introduce the label into scope!
let symbol = scope.ignore(label.into(), &mut env.ident_ids);
let (new_output, can_guard) = to_pattern_id(
env,
scope,
pattern_type,
&loc_guard.value,
loc_guard.region,
);
let destruct = RecordDestruct {
var: env.var_store.fresh(),
label: PoolStr::new(label, env.pool),
symbol,
typ: env
.pool
.add(DestructType::Guard(env.var_store.fresh(), can_guard)),
};
output.union(new_output);
env.pool[node_id] = destruct;
env.set_region(node_id, loc_pattern.region);
}
OptionalField(label, loc_default) => {
// an optional DOES introduce the label into scope!
match scope.introduce(
label.into(),
&env.exposed_ident_ids,
&mut env.ident_ids,
region,
) {
Ok(symbol) => {
let (can_default, expr_output) =
to_expr_id(env, scope, &loc_default.value, loc_default.region);
// an optional field binds the symbol!
output.references.bound_symbols.insert(symbol);
output.union(expr_output);
let destruct = RecordDestruct {
var: env.var_store.fresh(),
label: PoolStr::new(label, env.pool),
symbol,
typ: env.pool.add(DestructType::Optional(
env.var_store.fresh(),
can_default,
)),
};
env.pool[node_id] = destruct;
env.set_region(node_id, loc_pattern.region);
}
Err((original_region, shadow)) => {
env.problem(Problem::RuntimeError(RuntimeError::Shadowing {
original_region,
shadow: shadow.clone(),
kind: ShadowKind::Variable,
}));
// No matter what the other patterns
// are, we're definitely shadowed and will
// get a runtime exception as soon as we
// encounter the first bad pattern.
// opt_erroneous = Some(Pattern::Shadowed(original_region, shadow));
todo!("must report problem but also not loose any information")
}
};
}
_ => unreachable!("Any other pattern should have given a parse error"),
}
}
// If we encountered an erroneous pattern (e.g. one with shadowing),
// use the resulting RuntimeError. Otherwise, return a successful record destructure.
opt_erroneous.unwrap_or(Pattern2::RecordDestructure {
whole_var,
ext_var,
destructs,
})
}
RequiredField(_name, _loc_pattern) => {
unreachable!("should have been handled in RecordDestructure");
}
OptionalField(_name, _loc_pattern) => {
unreachable!("should have been handled in RecordDestructure");
}
Malformed(_str) => {
let problem = MalformedPatternProblem::Unknown;
malformed_pattern(env, problem, region)
}
MalformedIdent(_str, bad_ident) => {
let problem = MalformedPatternProblem::BadIdent(*bad_ident);
malformed_pattern(env, problem, region)
}
SpaceBefore(sub_pattern, _) | SpaceAfter(sub_pattern, _) => {
return to_pattern2(env, scope, pattern_type, sub_pattern, region)
}
};
(output, can_pattern)
}
pub fn symbols_from_pattern(pool: &Pool, initial: &Pattern2) -> Vec<Symbol> {
use Pattern2::*;
let mut symbols = Vec::new();
let mut stack = vec![initial];
while let Some(pattern) = stack.pop() {
match pattern {
Identifier(symbol) => {
symbols.push(*symbol);
}
Tag { arguments, .. } => {
for (_, pat_id) in arguments.iter(pool) {
let pat = pool.get(*pat_id);
stack.push(pat);
}
}
RecordDestructure { destructs, .. } => {
for destruct in destructs.iter(pool) {
let destruct_type = pool.get(destruct.typ);
if let DestructType::Guard(_, subpattern_id) = &destruct_type {
let subpattern = pool.get(*subpattern_id);
stack.push(subpattern);
} else {
symbols.push(destruct.symbol);
}
}
}
NumLiteral(_, _)
| IntLiteral(_)
| FloatLiteral(_)
| StrLiteral(_)
| CharacterLiteral(_)
| Underscore
| MalformedPattern(_, _)
| Shadowed { .. }
| UnsupportedPattern(_) => {}
}
}
symbols
}
pub fn get_identifier_string(pattern: &Pattern2, interns: &Interns) -> ASTResult<String> {
match pattern {
Pattern2::Identifier(symbol) => Ok(symbol.as_str(interns).to_string()),
other => UnexpectedPattern2VariantSnafu {
required_pattern2: "Identifier".to_string(),
encountered_pattern2: format!("{:?}", other),
}
.fail()?,
}
}
pub fn symbols_and_variables_from_pattern(
pool: &Pool,
initial: &Pattern2,
initial_var: Variable,
) -> Vec<(Symbol, Variable)> {
use Pattern2::*;
let mut symbols = Vec::new();
let mut stack = vec![(initial_var, initial)];
while let Some((variable, pattern)) = stack.pop() {
match pattern {
Identifier(symbol) => {
symbols.push((*symbol, variable));
}
Tag { arguments, .. } => {
for (var, pat_id) in arguments.iter(pool) {
let pat = pool.get(*pat_id);
stack.push((*var, pat));
}
}
RecordDestructure { destructs, .. } => {
for destruct in destructs.iter(pool) {
let destruct_type = pool.get(destruct.typ);
if let DestructType::Guard(_, subpattern_id) = &destruct_type {
let subpattern = pool.get(*subpattern_id);
stack.push((destruct.var, subpattern));
} else {
symbols.push((destruct.symbol, destruct.var));
}
}
}
NumLiteral(_, _)
| IntLiteral(_)
| FloatLiteral(_)
| StrLiteral(_)
| CharacterLiteral(_)
| Underscore
| MalformedPattern(_, _)
| Shadowed { .. }
| UnsupportedPattern(_) => {}
}
}
symbols
}
/// When we detect an unsupported pattern type (e.g. 5 = 1 + 2 is unsupported because you can't
/// assign to Int patterns), report it to Env and return an UnsupportedPattern runtime error pattern.
fn unsupported_pattern<'a>(
env: &mut Env<'a>,
pattern_type: PatternType,
region: Region,
) -> Pattern2 {
use roc_problem::can::BadPattern;
env.problem(Problem::UnsupportedPattern(
BadPattern::Unsupported(pattern_type),
region,
));
Pattern2::UnsupportedPattern(region)
}
fn underscore_in_def<'a>(env: &mut Env<'a>, region: Region) -> Pattern2 {
use roc_problem::can::BadPattern;
env.problem(Problem::UnsupportedPattern(
BadPattern::UnderscoreInDef,
region,
));
Pattern2::UnsupportedPattern(region)
}
pub(crate) fn flatten_str_literal(pool: &mut Pool, literal: &StrLiteral<'_>) -> Pattern2 {
use roc_parse::ast::StrLiteral::*;
match literal {
PlainLine(str_slice) => Pattern2::StrLiteral(PoolStr::new(str_slice, pool)),
Line(segments) => flatten_str_lines(pool, &[segments]),
Block(lines) => flatten_str_lines(pool, lines),
}
}
pub(crate) fn flatten_str_lines(pool: &mut Pool, lines: &[&[StrSegment<'_>]]) -> Pattern2 {
use StrSegment::*;
let mut buf = String::new();
for line in lines {
for segment in line.iter() {
match segment {
Plaintext(string) => {
buf.push_str(string);
}
Unicode(loc_digits) => {
todo!("parse unicode digits {:?}", loc_digits);
}
Interpolated(loc_expr) => {
return Pattern2::UnsupportedPattern(loc_expr.region);
}
EscapedChar(escaped) => buf.push(unescape_char(escaped)),
}
}
}
Pattern2::StrLiteral(PoolStr::new(&buf, pool))
}
/// When we detect a malformed pattern like `3.X` or `0b5`,
/// report it to Env and return an UnsupportedPattern runtime error pattern.
fn malformed_pattern<'a>(
env: &mut Env<'a>,
problem: MalformedPatternProblem,
region: Region,
) -> Pattern2 {
env.problem(Problem::RuntimeError(RuntimeError::MalformedPattern(
problem, region,
)));
Pattern2::MalformedPattern(problem, region)
}

View file

@ -0,0 +1,252 @@
use roc_error_macros::internal_error;
use roc_module::{called_via::CalledVia, symbol::Symbol};
use roc_parse::ast::StrLiteral;
use crate::{
ast_error::{ASTResult, UnexpectedASTNodeSnafu},
lang::{
core::expr::{
expr2::{ArrString, ARR_STRING_CAPACITY},
expr_to_expr2::expr_to_expr2,
},
env::Env,
scope::Scope,
},
mem_pool::{pool::Pool, pool_str::PoolStr, pool_vec::PoolVec},
};
use super::expr::{
expr2::{Expr2, ExprId},
output::Output,
};
pub(crate) fn flatten_str_literal<'a>(
env: &mut Env<'a>,
scope: &mut Scope,
literal: &StrLiteral<'a>,
) -> (Expr2, Output) {
use roc_parse::ast::StrLiteral::*;
match literal {
PlainLine(str_slice) => {
// TODO use smallstr
let expr = Expr2::Str(PoolStr::new(str_slice, env.pool));
(expr, Output::default())
}
Line(segments) => flatten_str_lines(env, scope, &[segments]),
Block(lines) => flatten_str_lines(env, scope, lines),
}
}
enum StrSegment {
Interpolation(Expr2),
Plaintext(PoolStr),
}
fn flatten_str_lines<'a>(
env: &mut Env<'a>,
scope: &mut Scope,
lines: &[&[roc_parse::ast::StrSegment<'a>]],
) -> (Expr2, Output) {
use roc_parse::ast::StrSegment::*;
let mut buf = String::new();
let mut segments = Vec::new();
let mut output = Output::default();
for line in lines {
for segment in line.iter() {
match segment {
Plaintext(string) => {
buf.push_str(string);
}
Unicode(loc_hex_digits) => match u32::from_str_radix(loc_hex_digits.value, 16) {
Ok(code_pt) => match std::char::from_u32(code_pt) {
Some(ch) => {
buf.push(ch);
}
None => {
// env.problem(Problem::InvalidUnicodeCodePt(loc_hex_digits.region));
//
// return (
// Expr::RuntimeError(RuntimeError::InvalidUnicodeCodePt(
// loc_hex_digits.region,
// )),
// output,
// );
todo!()
}
},
Err(_) => {
// env.problem(Problem::InvalidHexadecimal(loc_hex_digits.region));
//
// return (
// Expr::RuntimeError(RuntimeError::InvalidHexadecimal(
// loc_hex_digits.region,
// )),
// output,
// );
todo!()
}
},
Interpolated(loc_expr) => {
if roc_can::expr::is_valid_interpolation(loc_expr.value) {
// Interpolations desugar to Str.concat calls
output.references.calls.insert(Symbol::STR_CONCAT);
if !buf.is_empty() {
segments.push(StrSegment::Plaintext(PoolStr::new(&buf, env.pool)));
buf = String::new();
}
let (loc_expr, new_output) =
expr_to_expr2(env, scope, loc_expr.value, loc_expr.region);
output.union(new_output);
segments.push(StrSegment::Interpolation(loc_expr));
} else {
// env.problem(Problem::InvalidInterpolation(loc_expr.region));
//
// return (
// Expr::RuntimeError(RuntimeError::InvalidInterpolation(loc_expr.region)),
// output,
// );
todo!()
}
}
EscapedChar(escaped) => buf.push(roc_can::expr::unescape_char(escaped)),
}
}
}
if !buf.is_empty() {
segments.push(StrSegment::Plaintext(PoolStr::new(&buf, env.pool)));
}
(desugar_str_segments(env, segments), output)
}
/// Resolve string interpolations by desugaring a sequence of StrSegments
/// into nested calls to Str.concat
fn desugar_str_segments(env: &mut Env, segments: Vec<StrSegment>) -> Expr2 {
use StrSegment::*;
let pool = &mut env.pool;
let var_store = &mut env.var_store;
let mut iter = segments.into_iter().rev();
let mut expr = match iter.next() {
Some(Plaintext(pool_str)) => Expr2::Str(pool_str),
Some(Interpolation(expr_id)) => expr_id,
None => {
// No segments? Empty string!
let pool_str = PoolStr::new("", pool);
Expr2::Str(pool_str)
}
};
for seg in iter {
let new_expr = match seg {
Plaintext(string) => Expr2::Str(string),
Interpolation(expr_id) => expr_id,
};
let concat_expr_id = pool.add(Expr2::Var(Symbol::STR_CONCAT));
let args = vec![
(var_store.fresh(), pool.add(new_expr)),
(var_store.fresh(), pool.add(expr)),
];
let args = PoolVec::new(args.into_iter(), pool);
let new_call = Expr2::Call {
args,
expr_id: concat_expr_id,
expr_var: var_store.fresh(),
fn_var: var_store.fresh(),
closure_var: var_store.fresh(),
called_via: CalledVia::Space,
};
expr = new_call
}
expr
}
pub fn update_str_expr(
node_id: ExprId,
new_char: char,
insert_index: usize,
pool: &mut Pool,
) -> ASTResult<()> {
let str_expr = pool.get_mut(node_id);
enum Either {
MyArrString(ArrString),
OldPoolStr(PoolStr),
NewPoolStr(PoolStr),
}
let insert_either = match str_expr {
Expr2::SmallStr(arr_string) => {
if arr_string.len() < arr_string.capacity() {
let mut new_bytes: [u8; ARR_STRING_CAPACITY] = Default::default();
let arr_bytes = arr_string.as_str().as_bytes();
new_bytes[..insert_index].copy_from_slice(&arr_bytes[..insert_index]);
new_bytes[insert_index] = new_char as u8;
new_bytes[insert_index + 1..arr_bytes.len() + 1]
.copy_from_slice(&arr_bytes[insert_index..]);
let new_str = unsafe {
// all old characters have been checked on file load, new_char has been checked inside editor/src/editor/mvc/ed_update.rs
std::str::from_utf8_unchecked(&new_bytes[..arr_bytes.len() + 1])
};
let new_arr_string = match ArrString::from(new_str) {
Ok(arr_string) => arr_string,
Err(e) => {
internal_error!("Failed to build valid ArrayString from str: {:?}", e)
}
};
Either::MyArrString(new_arr_string)
} else {
let mut new_string = arr_string.as_str().to_owned();
new_string.insert(insert_index, new_char);
let new_pool_str = PoolStr::new(&new_string, pool);
Either::NewPoolStr(new_pool_str)
}
}
Expr2::Str(old_pool_str) => Either::OldPoolStr(*old_pool_str),
other => UnexpectedASTNodeSnafu {
required_node_type: "SmallStr or Str",
encountered_node_type: format!("{:?}", other),
}
.fail()?,
};
match insert_either {
Either::MyArrString(arr_string) => {
pool.set(node_id, Expr2::SmallStr(arr_string));
}
Either::OldPoolStr(old_pool_str) => {
let mut new_string = old_pool_str.as_str(pool).to_owned();
new_string.insert(insert_index, new_char);
let new_pool_str = PoolStr::new(&new_string, pool);
pool.set(node_id, Expr2::Str(new_pool_str))
}
Either::NewPoolStr(new_pool_str) => pool.set(node_id, Expr2::Str(new_pool_str)),
}
Ok(())
}

View file

@ -0,0 +1,871 @@
#![allow(clippy::all)]
#![allow(dead_code)]
#![allow(unused_imports)]
// use roc_can::expr::Output;
use roc_collections::all::{MutMap, MutSet};
use roc_error_macros::todo_abilities;
use roc_module::ident::{Ident, Lowercase, TagName, Uppercase};
use roc_module::symbol::Symbol;
use roc_region::all::{Loc, Region};
use roc_types::types::{Problem, RecordField};
use roc_types::{subs::Variable, types::ErrorType};
use crate::lang::env::Env;
use crate::lang::scope::Scope;
use crate::mem_pool::pool::{NodeId, Pool};
use crate::mem_pool::pool_str::PoolStr;
use crate::mem_pool::pool_vec::PoolVec;
use crate::mem_pool::shallow_clone::ShallowClone;
pub type TypeId = NodeId<Type2>;
const TYPE2_SIZE: () = assert!(std::mem::size_of::<Type2>() == 3 * 8 + 4);
#[derive(Debug)]
pub enum Type2 {
Variable(Variable), // 4B
Alias(Symbol, PoolVec<TypeId>, TypeId), // 24B = 8B + 8B + 4B + pad
Opaque(Symbol, PoolVec<TypeId>, TypeId), // 24B = 8B + 8B + 4B + pad
AsAlias(Symbol, PoolVec<(PoolStr, TypeId)>, TypeId), // 24B = 8B + 8B + 4B + pad
// 24B
HostExposedAlias {
name: Symbol, // 8B
arguments: PoolVec<(PoolStr, TypeId)>, // 8B
actual_var: Variable, // 4B
actual: TypeId, // 4B
},
EmptyTagUnion,
TagUnion(PoolVec<(TagName, PoolVec<Type2>)>, TypeId), // 12B = 8B + 4B
RecursiveTagUnion(Variable, PoolVec<(TagName, PoolVec<Type2>)>, TypeId), // 16B = 4B + 8B + 4B
EmptyRec,
Record(PoolVec<(PoolStr, RecordField<TypeId>)>, TypeId), // 12B = 8B + 4B
Function(PoolVec<Type2>, TypeId, TypeId), // 16B = 8B + 4B + 4B
Apply(Symbol, PoolVec<Type2>), // 16B = 8B + 8B
Erroneous(Problem2), // 24B
}
#[derive(Debug)]
pub enum Problem2 {
CanonicalizationProblem,
CircularType(Symbol, NodeId<ErrorType>), // 12B = 8B + 4B
CyclicAlias(Symbol, PoolVec<Symbol>), // 20B = 8B + 12B
UnrecognizedIdent(PoolStr), // 8B
Shadowed(Loc<PoolStr>),
BadTypeArguments {
symbol: Symbol, // 8B
type_got: u8, // 1B
alias_needs: u8, // 1B
},
InvalidModule,
SolvedTypeError,
}
impl ShallowClone for Type2 {
fn shallow_clone(&self) -> Self {
match self {
Self::Variable(var) => Self::Variable(*var),
Self::Alias(symbol, args, alias_type_id) => {
Self::Alias(*symbol, args.shallow_clone(), alias_type_id.clone())
}
Self::Opaque(symbol, args, alias_type_id) => {
Self::Opaque(*symbol, args.shallow_clone(), alias_type_id.clone())
}
Self::Record(fields, ext_id) => Self::Record(fields.shallow_clone(), ext_id.clone()),
Self::Function(args, closure_type_id, ret_type_id) => Self::Function(
args.shallow_clone(),
closure_type_id.clone(),
ret_type_id.clone(),
),
rest => todo!("{:?}", rest),
}
}
}
impl Type2 {
fn substitute(_pool: &mut Pool, _subs: &MutMap<Variable, TypeId>, _type_id: TypeId) {
todo!()
}
pub fn variables(&self, pool: &mut Pool) -> MutSet<Variable> {
use Type2::*;
let mut stack = vec![self];
let mut result = MutSet::default();
while let Some(this) = stack.pop() {
match this {
Variable(v) => {
result.insert(*v);
}
Alias(_, _, actual) | AsAlias(_, _, actual) | Opaque(_, _, actual) => {
stack.push(pool.get(*actual));
}
HostExposedAlias {
actual_var, actual, ..
} => {
result.insert(*actual_var);
stack.push(pool.get(*actual));
}
EmptyTagUnion | EmptyRec | Erroneous(_) => {}
TagUnion(tags, ext) => {
for (_, args) in tags.iter(pool) {
stack.extend(args.iter(pool));
}
stack.push(pool.get(*ext));
}
RecursiveTagUnion(rec, tags, ext) => {
for (_, args) in tags.iter(pool) {
stack.extend(args.iter(pool));
}
stack.push(pool.get(*ext));
result.insert(*rec);
}
Record(fields, ext) => {
for (_, field) in fields.iter(pool) {
stack.push(pool.get(*field.as_inner()));
}
stack.push(pool.get(*ext));
}
Function(args, closure, result) => {
stack.extend(args.iter(pool));
stack.push(pool.get(*closure));
stack.push(pool.get(*result));
}
Apply(_, args) => {
stack.extend(args.iter(pool));
}
}
}
result
}
pub fn contains_symbol(&self, _pool: &mut Pool, _needle: Symbol) -> bool {
todo!()
}
pub fn substitute_alias(&self, _pool: &mut Pool, _needle: Symbol, _actual: Self) {
todo!()
}
}
impl NodeId<Type2> {
pub fn variables(&self, _pool: &mut Pool) -> MutSet<Variable> {
todo!()
}
}
/// A temporary data structure to return a bunch of values to Def construction
pub enum Signature {
FunctionWithAliases {
annotation: Type2,
arguments: PoolVec<Type2>,
closure_type_id: TypeId,
return_type_id: TypeId,
},
Function {
arguments: PoolVec<Type2>,
closure_type_id: TypeId,
return_type_id: TypeId,
},
Value {
annotation: Type2,
},
}
pub enum Annotation2 {
Annotation {
named_rigids: MutMap<Lowercase, Variable>,
unnamed_rigids: MutSet<Variable>,
symbols: MutSet<Symbol>,
signature: Signature,
},
Erroneous(roc_types::types::Problem),
}
pub fn to_annotation2<'a>(
env: &mut Env,
scope: &mut Scope,
annotation: &'a roc_parse::ast::TypeAnnotation<'a>,
region: Region,
) -> Annotation2 {
let mut references = References::default();
let annotation = to_type2(env, scope, &mut references, annotation, region);
// we dealias until we hit a non-alias, then we either hit a function type (and produce a
// function annotation) or anything else (and produce a value annotation)
match annotation {
Type2::Function(arguments, closure_type_id, return_type_id) => {
let References {
named,
unnamed,
symbols,
..
} = references;
let signature = Signature::Function {
arguments,
closure_type_id,
return_type_id,
};
Annotation2::Annotation {
named_rigids: named,
unnamed_rigids: unnamed,
symbols,
signature,
}
}
Type2::Alias(_, _, _) => {
// most of the time, the annotation is not an alias, so this case is comparatively
// less efficient
shallow_dealias(env, references, annotation)
}
_ => {
let References {
named,
unnamed,
symbols,
..
} = references;
let signature = Signature::Value { annotation };
Annotation2::Annotation {
named_rigids: named,
unnamed_rigids: unnamed,
symbols,
signature,
}
}
}
}
fn shallow_dealias<'a>(env: &mut Env, references: References, annotation: Type2) -> Annotation2 {
let References {
named,
unnamed,
symbols,
..
} = references;
let mut inner = &annotation;
loop {
match inner {
Type2::Alias(_, _, actual) => {
inner = env.pool.get(*actual);
}
Type2::Function(arguments, closure_type_id, return_type_id) => {
let signature = Signature::FunctionWithAliases {
arguments: arguments.shallow_clone(),
closure_type_id: *closure_type_id,
return_type_id: *return_type_id,
annotation,
};
return Annotation2::Annotation {
named_rigids: named,
unnamed_rigids: unnamed,
symbols,
signature,
};
}
_ => {
let signature = Signature::Value { annotation };
return Annotation2::Annotation {
named_rigids: named,
unnamed_rigids: unnamed,
symbols,
signature,
};
}
}
}
}
#[derive(Default)]
pub struct References {
named: MutMap<Lowercase, Variable>,
unnamed: MutSet<Variable>,
hidden: MutSet<Variable>,
symbols: MutSet<Symbol>,
}
pub fn to_type_id<'a>(
env: &mut Env,
scope: &mut Scope,
rigids: &mut References,
annotation: &roc_parse::ast::TypeAnnotation<'a>,
region: Region,
) -> TypeId {
let type2 = to_type2(env, scope, rigids, annotation, region);
env.add(type2, region)
}
pub fn as_type_id<'a>(
env: &mut Env,
scope: &mut Scope,
rigids: &mut References,
type_id: TypeId,
annotation: &roc_parse::ast::TypeAnnotation<'a>,
region: Region,
) {
let type2 = to_type2(env, scope, rigids, annotation, region);
env.pool[type_id] = type2;
env.set_region(type_id, region);
}
pub fn to_type2<'a>(
env: &mut Env,
scope: &mut Scope,
references: &mut References,
annotation: &roc_parse::ast::TypeAnnotation<'a>,
region: Region,
) -> Type2 {
use roc_parse::ast::Pattern;
use roc_parse::ast::TypeAnnotation::*;
use roc_parse::ast::TypeHeader;
match annotation {
Apply(module_name, ident, targs) => {
match to_type_apply(env, scope, references, module_name, ident, targs, region) {
TypeApply::Apply(symbol, args) => {
references.symbols.insert(symbol);
Type2::Apply(symbol, args)
}
TypeApply::Alias(symbol, args, actual) => {
references.symbols.insert(symbol);
Type2::Alias(symbol, args, actual)
}
TypeApply::Erroneous(_problem) => {
// Type2::Erroneous(problem)
todo!()
}
}
}
Function(argument_types, return_type) => {
let arguments = PoolVec::with_capacity(argument_types.len() as u32, env.pool);
for (type_id, loc_arg) in arguments.iter_node_ids().zip(argument_types.iter()) {
as_type_id(
env,
scope,
references,
type_id,
&loc_arg.value,
loc_arg.region,
);
}
let return_type_id = to_type_id(
env,
scope,
references,
&return_type.value,
return_type.region,
);
let closure_type = Type2::Variable(env.var_store.fresh());
let closure_type_id = env.pool.add(closure_type);
Type2::Function(arguments, closure_type_id, return_type_id)
}
BoundVariable(v) => {
// A rigid type variable. The parser should have already ensured that the name is indeed a lowercase.
let v = Lowercase::from(*v);
match references.named.get(&v) {
Some(var) => Type2::Variable(*var),
None => {
let var = env.var_store.fresh();
references.named.insert(v, var);
Type2::Variable(var)
}
}
}
Inferred => {
let var = env.var_store.fresh();
Type2::Variable(var)
}
Wildcard | Malformed(_) => {
let var = env.var_store.fresh();
references.unnamed.insert(var);
Type2::Variable(var)
}
Record { fields, ext, .. } => {
let field_types_map =
can_assigned_fields(env, scope, references, &fields.items, region);
let field_types = PoolVec::with_capacity(field_types_map.len() as u32, env.pool);
for (node_id, (label, field)) in field_types.iter_node_ids().zip(field_types_map) {
let poolstr = PoolStr::new(label.as_str(), env.pool);
let rec_field = match field {
RecordField::Optional(_) => {
let field_id = env.pool.add(field.into_inner());
RecordField::Optional(field_id)
}
RecordField::Demanded(_) => {
let field_id = env.pool.add(field.into_inner());
RecordField::Demanded(field_id)
}
RecordField::Required(_) => {
let field_id = env.pool.add(field.into_inner());
RecordField::Required(field_id)
}
};
env.pool[node_id] = (poolstr, rec_field);
}
let ext_type = match ext {
Some(loc_ann) => to_type_id(env, scope, references, &loc_ann.value, region),
None => env.add(Type2::EmptyRec, region),
};
Type2::Record(field_types, ext_type)
}
TagUnion { tags, ext, .. } => {
let tag_types_vec = can_tags(env, scope, references, tags.items, region);
let tag_types = PoolVec::with_capacity(tag_types_vec.len() as u32, env.pool);
for (node_id, (tag_name, field)) in tag_types.iter_node_ids().zip(tag_types_vec) {
env.pool[node_id] = (tag_name, field);
}
let ext_type = match ext {
Some(loc_ann) => to_type_id(env, scope, references, &loc_ann.value, region),
None => env.add(Type2::EmptyTagUnion, region),
};
Type2::TagUnion(tag_types, ext_type)
}
As(
loc_inner,
_spaces,
TypeHeader {
name,
vars: loc_vars,
},
) => {
// e.g. `{ x : Int, y : Int } as Point`
let symbol = match scope.introduce(
name.value.into(),
&env.exposed_ident_ids,
&mut env.ident_ids,
region,
) {
Ok(symbol) => symbol,
Err((_original_region, _shadow)) => {
// let problem = Problem2::Shadowed(original_region, shadow.clone());
// env.problem(roc_problem::can::Problem::ShadowingInAnnotation {
// original_region,
// shadow,
// });
// return Type2::Erroneous(problem);
todo!();
}
};
let inner_type = to_type2(env, scope, references, &loc_inner.value, region);
let vars = PoolVec::with_capacity(loc_vars.len() as u32, env.pool);
let lowercase_vars = PoolVec::with_capacity(loc_vars.len() as u32, env.pool);
for ((loc_var, named_id), var_id) in loc_vars
.iter()
.zip(lowercase_vars.iter_node_ids())
.zip(vars.iter_node_ids())
{
let var = match loc_var.value {
Pattern::Identifier(name) if name.chars().next().unwrap().is_lowercase() => {
name
}
_ => unreachable!("I thought this was validated during parsing"),
};
let var_name = Lowercase::from(var);
if let Some(var) = references.named.get(&var_name) {
let poolstr = PoolStr::new(var_name.as_str(), env.pool);
let type_id = env.pool.add(Type2::Variable(*var));
env.pool[var_id] = (poolstr.shallow_clone(), type_id);
env.pool[named_id] = (poolstr, *var);
env.set_region(named_id, loc_var.region);
} else {
let var = env.var_store.fresh();
references.named.insert(var_name.clone(), var);
let poolstr = PoolStr::new(var_name.as_str(), env.pool);
let type_id = env.pool.add(Type2::Variable(var));
env.pool[var_id] = (poolstr.shallow_clone(), type_id);
env.pool[named_id] = (poolstr, var);
env.set_region(named_id, loc_var.region);
}
}
let alias_actual = inner_type;
// TODO instantiate recursive tag union
// let alias_actual = if let Type2::TagUnion(tags, ext) = inner_type {
// let rec_var = env.var_store.fresh();
//
// let mut new_tags = Vec::with_capacity(tags.len());
// for (tag_name, args) in tags {
// let mut new_args = Vec::with_capacity(args.len());
// for arg in args {
// let mut new_arg = arg.clone();
// new_arg.substitute_alias(symbol, &Type2::Variable(rec_var));
// new_args.push(new_arg);
// }
// new_tags.push((tag_name.clone(), new_args));
// }
// Type2::RecursiveTagUnion(rec_var, new_tags, ext)
// } else {
// inner_type
// };
let mut hidden_variables = MutSet::default();
hidden_variables.extend(alias_actual.variables(env.pool));
for (_, var) in lowercase_vars.iter(env.pool) {
hidden_variables.remove(var);
}
let alias_actual_id = env.pool.add(alias_actual);
scope.add_alias(env.pool, symbol, lowercase_vars, alias_actual_id);
let alias = scope.lookup_alias(symbol).unwrap();
// local_aliases.insert(symbol, alias.clone());
// TODO host-exposed
// if vars.is_empty() && env.home == symbol.module_id() {
// let actual_var = env.var_store.fresh();
// rigids.host_exposed.insert(symbol, actual_var);
// Type::HostExposedAlias {
// name: symbol,
// arguments: vars,
// actual: Box::new(alias.typ.clone()),
// actual_var,
// }
// } else {
// Type::Alias(symbol, vars, Box::new(alias.typ.clone()))
// }
Type2::AsAlias(symbol, vars, alias.actual)
}
Where { .. } => todo_abilities!(),
SpaceBefore(nested, _) | SpaceAfter(nested, _) => {
to_type2(env, scope, references, nested, region)
}
}
}
// TODO trim down these arguments!
#[allow(clippy::too_many_arguments)]
fn can_assigned_fields<'a>(
env: &mut Env,
scope: &mut Scope,
rigids: &mut References,
fields: &&[Loc<roc_parse::ast::AssignedField<'a, roc_parse::ast::TypeAnnotation<'a>>>],
region: Region,
) -> MutMap<Lowercase, RecordField<Type2>> {
use roc_parse::ast::AssignedField::*;
use roc_types::types::RecordField::*;
// SendMap doesn't have a `with_capacity`
let mut field_types = MutMap::default();
// field names we've seen so far in this record
let mut seen = std::collections::HashMap::with_capacity(fields.len());
'outer: for loc_field in fields.iter() {
let mut field = &loc_field.value;
// use this inner loop to unwrap the SpaceAfter/SpaceBefore
// when we find the name of this field, break out of the loop
// with that value, so we can check whether the field name is
// a duplicate
let new_name = 'inner: loop {
match field {
RequiredValue(field_name, _, annotation) => {
let field_type =
to_type2(env, scope, rigids, &annotation.value, annotation.region);
let label = Lowercase::from(field_name.value);
field_types.insert(label.clone(), Required(field_type));
break 'inner label;
}
OptionalValue(field_name, _, annotation) => {
let field_type =
to_type2(env, scope, rigids, &annotation.value, annotation.region);
let label = Lowercase::from(field_name.value);
field_types.insert(label.clone(), Optional(field_type));
break 'inner label;
}
LabelOnly(loc_field_name) => {
// Interpret { a, b } as { a : a, b : b }
let field_name = Lowercase::from(loc_field_name.value);
let field_type = {
if let Some(var) = rigids.named.get(&field_name) {
Type2::Variable(*var)
} else {
let field_var = env.var_store.fresh();
rigids.named.insert(field_name.clone(), field_var);
Type2::Variable(field_var)
}
};
field_types.insert(field_name.clone(), Required(field_type));
break 'inner field_name;
}
SpaceBefore(nested, _) | SpaceAfter(nested, _) => {
// check the nested field instead
field = nested;
continue 'inner;
}
Malformed(_) => {
// TODO report this?
// completely skip this element, advance to the next tag
continue 'outer;
}
}
};
// ensure that the new name is not already in this record:
// note that the right-most tag wins when there are two with the same name
if let Some(replaced_region) = seen.insert(new_name.clone(), loc_field.region) {
env.problem(roc_problem::can::Problem::DuplicateRecordFieldType {
field_name: new_name.into(),
record_region: region,
field_region: loc_field.region,
replaced_region,
});
}
}
field_types
}
fn can_tags<'a>(
env: &mut Env,
scope: &mut Scope,
rigids: &mut References,
tags: &'a [Loc<roc_parse::ast::Tag<'a>>],
region: Region,
) -> Vec<(TagName, PoolVec<Type2>)> {
use roc_parse::ast::Tag;
let mut tag_types = Vec::with_capacity(tags.len());
// tag names we've seen so far in this tag union
let mut seen = std::collections::HashMap::with_capacity(tags.len());
'outer: for loc_tag in tags.iter() {
let mut tag = &loc_tag.value;
// use this inner loop to unwrap the SpaceAfter/SpaceBefore
// when we find the name of this tag, break out of the loop
// with that value, so we can check whether the tag name is
// a duplicate
let new_name = 'inner: loop {
match tag {
Tag::Apply { name, args } => {
let arg_types = PoolVec::with_capacity(args.len() as u32, env.pool);
for (type_id, loc_arg) in arg_types.iter_node_ids().zip(args.iter()) {
as_type_id(env, scope, rigids, type_id, &loc_arg.value, loc_arg.region);
}
let tag_name = TagName(name.value.into());
tag_types.push((tag_name.clone(), arg_types));
break 'inner tag_name;
}
Tag::SpaceBefore(nested, _) | Tag::SpaceAfter(nested, _) => {
// check the nested tag instead
tag = nested;
continue 'inner;
}
Tag::Malformed(_) => {
// TODO report this?
// completely skip this element, advance to the next tag
continue 'outer;
}
}
};
// ensure that the new name is not already in this tag union:
// note that the right-most tag wins when there are two with the same name
if let Some(replaced_region) = seen.insert(new_name.clone(), loc_tag.region) {
env.problem(roc_problem::can::Problem::DuplicateTag {
tag_region: loc_tag.region,
tag_union_region: region,
replaced_region,
tag_name: new_name,
});
}
}
tag_types
}
enum TypeApply {
Apply(Symbol, PoolVec<Type2>),
Alias(Symbol, PoolVec<TypeId>, TypeId),
Erroneous(roc_types::types::Problem),
}
#[inline(always)]
fn to_type_apply<'a>(
env: &mut Env,
scope: &mut Scope,
rigids: &mut References,
module_name: &str,
ident: &str,
type_arguments: &[Loc<roc_parse::ast::TypeAnnotation<'a>>],
region: Region,
) -> TypeApply {
let symbol = if module_name.is_empty() {
// Since module_name was empty, this is an unqualified type.
// Look it up in scope!
let ident: Ident = (*ident).into();
match scope.lookup(&ident, region) {
Ok(symbol) => symbol,
Err(problem) => {
env.problem(roc_problem::can::Problem::RuntimeError(problem));
return TypeApply::Erroneous(Problem::UnrecognizedIdent(ident.into()));
}
}
} else {
match env.qualified_lookup(module_name, ident, region) {
Ok(symbol) => symbol,
Err(problem) => {
// Either the module wasn't imported, or
// it was imported but it doesn't expose this ident.
env.problem(roc_problem::can::Problem::RuntimeError(problem));
return TypeApply::Erroneous(Problem::UnrecognizedIdent((*ident).into()));
}
}
};
let argument_type_ids = PoolVec::with_capacity(type_arguments.len() as u32, env.pool);
for (type_id, loc_arg) in argument_type_ids.iter_node_ids().zip(type_arguments.iter()) {
as_type_id(env, scope, rigids, type_id, &loc_arg.value, loc_arg.region);
}
let args = type_arguments;
let opt_alias = scope.lookup_alias(symbol);
match opt_alias {
Some(ref alias) => {
// use a known alias
let actual = alias.actual;
let mut substitutions: MutMap<Variable, TypeId> = MutMap::default();
if alias.targs.len() != args.len() {
let error = TypeApply::Erroneous(Problem::BadTypeArguments {
symbol,
region,
alias_needs: alias.targs.len() as u8,
type_got: args.len() as u8,
});
return error;
}
let arguments = PoolVec::with_capacity(type_arguments.len() as u32, env.pool);
let it = arguments.iter_node_ids().zip(
argument_type_ids
.iter_node_ids()
.zip(alias.targs.iter_node_ids()),
);
for (node_id, (type_id, loc_var_id)) in it {
let loc_var = &env.pool[loc_var_id];
let name = loc_var.0.shallow_clone();
let var = loc_var.1;
env.pool[node_id] = (name, type_id);
substitutions.insert(var, type_id);
}
// make sure the recursion variable is freshly instantiated
// have to allocate these outside of the if for lifetime reasons...
let new = env.var_store.fresh();
let fresh = env.pool.add(Type2::Variable(new));
if let Type2::RecursiveTagUnion(rvar, ref tags, ext) = &mut env.pool[actual] {
substitutions.insert(*rvar, fresh);
env.pool[actual] = Type2::RecursiveTagUnion(new, tags.shallow_clone(), *ext);
}
// make sure hidden variables are freshly instantiated
for var_id in alias.hidden_variables.iter_node_ids() {
let var = env.pool[var_id];
let fresh = env.pool.add(Type2::Variable(env.var_store.fresh()));
substitutions.insert(var, fresh);
}
// instantiate variables
Type2::substitute(env.pool, &substitutions, actual);
let type_arguments = PoolVec::with_capacity(arguments.len() as u32, env.pool);
for (node_id, type_id) in arguments
.iter_node_ids()
.zip(type_arguments.iter_node_ids())
{
let typ = env.pool[node_id].1;
env.pool[type_id] = typ;
}
TypeApply::Alias(symbol, type_arguments, actual)
}
None => TypeApply::Apply(symbol, argument_type_ids),
}
}
#[derive(Debug)]
pub struct Alias {
pub targs: PoolVec<(PoolStr, Variable)>,
pub actual: TypeId,
/// hidden type variables, like the closure variable in `a -> b`
pub hidden_variables: PoolVec<Variable>,
}
impl ShallowClone for Alias {
fn shallow_clone(&self) -> Self {
Self {
targs: self.targs.shallow_clone(),
hidden_variables: self.hidden_variables.shallow_clone(),
actual: self.actual,
}
}
}

View file

@ -0,0 +1,101 @@
use crate::{
lang::{core::expr::expr2_to_string::expr2_to_string, rigids::Rigids},
mem_pool::{
pool::{NodeId, Pool},
shallow_clone::ShallowClone,
},
};
use roc_types::subs::Variable;
use super::{
expr::expr2::ExprId,
pattern::{Pattern2, PatternId},
types::TypeId,
};
#[derive(Debug)]
pub enum ValueDef {
WithAnnotation {
pattern_id: PatternId, // 4B
expr_id: ExprId, // 4B
type_id: TypeId,
rigids: Rigids,
expr_var: Variable, // 4B
},
NoAnnotation {
pattern_id: PatternId, // 4B
expr_id: ExprId, // 4B
expr_var: Variable, // 4B
},
}
impl ShallowClone for ValueDef {
fn shallow_clone(&self) -> Self {
match self {
Self::WithAnnotation {
pattern_id,
expr_id,
type_id,
rigids,
expr_var,
} => Self::WithAnnotation {
pattern_id: *pattern_id,
expr_id: *expr_id,
type_id: *type_id,
rigids: rigids.shallow_clone(),
expr_var: *expr_var,
},
Self::NoAnnotation {
pattern_id,
expr_id,
expr_var,
} => Self::NoAnnotation {
pattern_id: *pattern_id,
expr_id: *expr_id,
expr_var: *expr_var,
},
}
}
}
impl ValueDef {
pub fn get_expr_id(&self) -> ExprId {
match self {
ValueDef::WithAnnotation { expr_id, .. } => *expr_id,
ValueDef::NoAnnotation { expr_id, .. } => *expr_id,
}
}
pub fn get_pattern_id(&self) -> NodeId<Pattern2> {
match self {
ValueDef::WithAnnotation { pattern_id, .. } => *pattern_id,
ValueDef::NoAnnotation { pattern_id, .. } => *pattern_id,
}
}
}
pub fn value_def_to_string(val_def: &ValueDef, pool: &Pool) -> String {
match val_def {
ValueDef::WithAnnotation {
pattern_id,
expr_id,
type_id,
rigids,
expr_var,
} => {
format!("WithAnnotation {{ pattern_id: {:?}, expr_id: {:?}, type_id: {:?}, rigids: {:?}, expr_var: {:?}}}", pool.get(*pattern_id), expr2_to_string(*expr_id, pool), pool.get(*type_id), rigids, expr_var)
}
ValueDef::NoAnnotation {
pattern_id,
expr_id,
expr_var,
} => {
format!(
"NoAnnotation {{ pattern_id: {:?}, expr_id: {:?}, expr_var: {:?}}}",
pool.get(*pattern_id),
expr2_to_string(*expr_id, pool),
expr_var
)
}
}
}

187
crates/ast/src/lang/env.rs Normal file
View file

@ -0,0 +1,187 @@
use crate::mem_pool::pool::{NodeId, Pool};
use bumpalo::{collections::Vec as BumpVec, Bump};
use roc_collections::all::{MutMap, MutSet};
use roc_module::ident::{Ident, Lowercase, ModuleName};
use roc_module::symbol::{IdentIds, IdentIdsByModule, ModuleId, ModuleIds, Symbol};
use roc_problem::can::{Problem, RuntimeError};
use roc_region::all::{Loc, Region};
use roc_types::subs::VarStore;
use super::core::def::def::References;
/// TODO document
#[derive(Debug)]
pub struct Env<'a> {
pub home: ModuleId,
pub var_store: &'a mut VarStore,
pub pool: &'a mut Pool,
pub arena: &'a Bump,
pub problems: BumpVec<'a, Problem>,
pub dep_idents: IdentIdsByModule,
pub module_ids: &'a ModuleIds,
pub ident_ids: IdentIds,
pub exposed_ident_ids: IdentIds,
pub closures: MutMap<Symbol, References>,
/// Symbols which were referenced by qualified lookups.
pub qualified_lookups: MutSet<Symbol>,
pub top_level_symbols: MutSet<Symbol>,
pub closure_name_symbol: Option<Symbol>,
pub tailcallable_symbol: Option<Symbol>,
}
impl<'a> Env<'a> {
#[allow(clippy::too_many_arguments)]
pub fn new(
home: ModuleId,
arena: &'a Bump,
pool: &'a mut Pool,
var_store: &'a mut VarStore,
dep_idents: IdentIdsByModule,
module_ids: &'a ModuleIds,
exposed_ident_ids: IdentIds,
) -> Env<'a> {
Env {
home,
arena,
pool,
problems: BumpVec::new_in(arena),
var_store,
dep_idents,
module_ids,
ident_ids: exposed_ident_ids.clone(), // we start with these, but will add more later using Scope.introduce
exposed_ident_ids,
closures: MutMap::default(),
qualified_lookups: MutSet::default(),
tailcallable_symbol: None,
closure_name_symbol: None,
top_level_symbols: MutSet::default(),
}
}
pub fn add<T>(&mut self, item: T, region: Region) -> NodeId<T> {
let id = self.pool.add(item);
self.set_region(id, region);
id
}
pub fn problem(&mut self, problem: Problem) {
self.problems.push(problem);
}
pub fn set_region<T>(&mut self, _node_id: NodeId<T>, _region: Region) {
dbg!("Don't Forget to set the region eventually");
}
pub fn register_closure(&mut self, symbol: Symbol, references: References) {
self.closures.insert(symbol, references);
}
/// Generates a unique, new symbol like "$1" or "$5",
/// using the home module as the module_id.
///
/// This is used, for example, during canonicalization of an Expr::Closure
/// to generate a unique symbol to refer to that closure.
pub fn gen_unique_symbol(&mut self) -> Symbol {
let ident_id = self.ident_ids.gen_unique();
Symbol::new(self.home, ident_id)
}
/// Returns Err if the symbol resolved, but it was not exposed by the given module
pub fn qualified_lookup(
&mut self,
module_name: &str,
ident: &str,
region: Region,
) -> Result<Symbol, RuntimeError> {
debug_assert!(
!module_name.is_empty(),
"Called env.qualified_lookup with an unqualified ident: {:?}",
ident
);
let module_name: ModuleName = module_name.into();
match self.module_ids.get_id(&module_name) {
Some(module_id) => {
// You can do qualified lookups on your own module, e.g.
// if I'm in the Foo module, I can do a `Foo.bar` lookup.
if module_id == self.home {
match self.ident_ids.get_id(ident) {
Some(ident_id) => {
let symbol = Symbol::new(module_id, ident_id);
self.qualified_lookups.insert(symbol);
Ok(symbol)
}
None => Err(RuntimeError::LookupNotInScope(
Loc {
value: Ident::from(ident),
region,
},
self.ident_ids
.ident_strs()
.map(|(_, string)| string.into())
.collect(),
)),
}
} else {
match self.dep_idents.get(&module_id) {
Some(exposed_ids) => match exposed_ids.get_id(ident) {
Some(ident_id) => {
let symbol = Symbol::new(module_id, ident_id);
self.qualified_lookups.insert(symbol);
Ok(symbol)
}
None => {
let exposed_values = exposed_ids
.ident_strs()
.filter(|(_, ident)| {
ident.starts_with(|c: char| c.is_lowercase())
})
.map(|(_, ident)| Lowercase::from(ident))
.collect();
Err(RuntimeError::ValueNotExposed {
module_name,
ident: Ident::from(ident),
region,
exposed_values,
})
}
},
None => Err(RuntimeError::ModuleNotImported {
module_name,
imported_modules: self
.dep_idents
.keys()
.filter_map(|module_id| self.module_ids.get_name(*module_id))
.map(|module_name| module_name.as_ref().into())
.collect(),
region,
module_exists: true,
}),
}
}
}
None => Err(RuntimeError::ModuleNotImported {
module_name,
imported_modules: self
.module_ids
.available_modules()
.map(|string| string.as_ref().into())
.collect(),
region,
module_exists: false,
}),
}
}
}

View file

@ -0,0 +1,4 @@
pub mod core;
pub mod env;
mod rigids;
pub mod scope;

View file

@ -0,0 +1,83 @@
use std::{
collections::{HashMap, HashSet},
hash::BuildHasherDefault,
};
use crate::mem_pool::{
pool::Pool, pool_str::PoolStr, pool_vec::PoolVec, shallow_clone::ShallowClone,
};
use roc_collections::all::WyHash;
use roc_module::ident::Lowercase;
use roc_types::subs::Variable;
#[derive(Debug)]
pub struct Rigids {
// Rigid type variable = type variable where type is specified by the programmer
pub names: PoolVec<(Option<PoolStr>, Variable)>, // 8B
padding: [u8; 1],
}
#[allow(clippy::needless_collect)]
impl Rigids {
pub fn new(
named: HashMap<Lowercase, Variable, BuildHasherDefault<WyHash>>,
unnamed: HashSet<Variable, BuildHasherDefault<WyHash>>,
pool: &mut Pool,
) -> Self {
let names = PoolVec::with_capacity((named.len() + unnamed.len()) as u32, pool);
let mut temp_names = Vec::new();
temp_names.extend(named.iter().map(|(name, var)| (Some(name.as_str()), *var)));
temp_names.extend(unnamed.iter().map(|var| (None, *var)));
for (node_id, (opt_name, variable)) in names.iter_node_ids().zip(temp_names) {
let poolstr = opt_name.map(|name| PoolStr::new(name, pool));
pool[node_id] = (poolstr, variable);
}
Self {
names,
padding: Default::default(),
}
}
pub fn named(&self, pool: &mut Pool) -> PoolVec<(PoolStr, Variable)> {
let named = self
.names
.iter(pool)
.filter_map(|(opt_pool_str, var)| {
opt_pool_str.as_ref().map(|pool_str| (*pool_str, *var))
})
.collect::<Vec<(PoolStr, Variable)>>();
PoolVec::new(named.into_iter(), pool)
}
pub fn unnamed(&self, pool: &mut Pool) -> PoolVec<Variable> {
let unnamed = self
.names
.iter(pool)
.filter_map(|(opt_pool_str, var)| {
if opt_pool_str.is_none() {
Some(*var)
} else {
None
}
})
.collect::<Vec<Variable>>();
PoolVec::new(unnamed.into_iter(), pool)
}
}
impl ShallowClone for Rigids {
fn shallow_clone(&self) -> Self {
Self {
names: self.names.shallow_clone(),
padding: self.padding,
}
}
}

View file

@ -0,0 +1,353 @@
#![allow(clippy::all)]
#![allow(dead_code)]
#![allow(unused_imports)]
use std::fmt;
use crate::ast_error::ASTResult;
use crate::mem_pool::pool::Pool;
use crate::mem_pool::pool_str::PoolStr;
use crate::mem_pool::pool_vec::PoolVec;
use crate::mem_pool::shallow_clone::ShallowClone;
use roc_collections::all::{MutMap, MutSet};
use roc_module::ident::{Ident, Lowercase};
use roc_module::symbol::{
get_module_ident_ids, get_module_ident_ids_mut, IdentIds, IdentIdsByModule, Interns, ModuleId,
Symbol,
};
use roc_problem::can::RuntimeError;
use roc_region::all::{Loc, Region};
use roc_types::{
builtin_aliases,
solved_types::{BuiltinAlias, FreeVars, SolvedType},
subs::{VarId, VarStore, Variable},
};
use super::core::types::{Alias, Type2, TypeId};
use super::env::Env;
fn solved_type_to_type_id(
pool: &mut Pool,
solved_type: &SolvedType,
free_vars: &mut FreeVars,
var_store: &mut VarStore,
) -> TypeId {
let typ2 = to_type2(pool, solved_type, free_vars, var_store);
pool.add(typ2)
}
fn to_type2(
pool: &mut Pool,
solved_type: &SolvedType,
free_vars: &mut FreeVars,
var_store: &mut VarStore,
) -> Type2 {
match solved_type {
// TODO(opaques): take opaques into account
SolvedType::Alias(symbol, solved_type_variables, _todo, solved_actual, _kind) => {
let type_variables = PoolVec::with_capacity(solved_type_variables.len() as u32, pool);
for (type_variable_node_id, solved_arg) in type_variables
.iter_node_ids()
.zip(solved_type_variables.iter())
{
let typ2 = to_type2(pool, solved_arg, free_vars, var_store);
let node = pool.add(typ2);
pool[type_variable_node_id] = node;
}
let actual_typ2 = to_type2(pool, solved_actual, free_vars, var_store);
let actual = pool.add(actual_typ2);
let typ2 = Type2::Alias(*symbol, type_variables, actual);
typ2
}
SolvedType::TagUnion(tags, ext) => {
let new_tags = PoolVec::with_capacity(tags.len() as u32, pool);
for (tag_node_id, (tag_name, args)) in new_tags.iter_node_ids().zip(tags.iter()) {
let new_args: PoolVec<Type2> = PoolVec::with_capacity(args.len() as u32, pool);
for (arg_node_id, arg) in new_args.iter_node_ids().zip(args.iter()) {
let node = to_type2(pool, arg, free_vars, var_store);
pool[arg_node_id] = node;
}
pool[tag_node_id] = (tag_name.clone(), new_args);
}
let actual_typ2 = to_type2(pool, ext, free_vars, var_store);
let actual = pool.add(actual_typ2);
let typ2 = Type2::TagUnion(new_tags, actual);
typ2
}
SolvedType::Flex(var_id) => {
Type2::Variable(var_id_to_flex_var(*var_id, free_vars, var_store))
}
SolvedType::EmptyTagUnion => Type2::EmptyTagUnion,
rest => todo!("{:?}", rest),
}
}
fn var_id_to_flex_var(
var_id: VarId,
free_vars: &mut FreeVars,
var_store: &mut VarStore,
) -> Variable {
if let Some(var) = free_vars.unnamed_vars.get(&var_id) {
*var
} else {
let var = var_store.fresh();
free_vars.unnamed_vars.insert(var_id, var);
var
}
}
#[derive(Debug)]
pub struct Scope {
/// All the identifiers in scope, mapped to were they were defined and
/// the Symbol they resolve to.
idents: MutMap<Ident, (Symbol, Region)>,
/// A cache of all the symbols in scope. This makes lookups much
/// faster when checking for unused defs and unused arguments.
symbols: MutMap<Symbol, Region>,
/// The type aliases currently in scope
aliases: MutMap<Symbol, Alias>,
/// The current module being processed. This will be used to turn
/// unqualified idents into Symbols.
home: ModuleId,
}
impl Scope {
pub fn new(home: ModuleId, pool: &mut Pool, var_store: &mut VarStore) -> Scope {
let solved_aliases = builtin_aliases::aliases();
let mut aliases = MutMap::default();
for (symbol, builtin_alias) in solved_aliases {
// let BuiltinAlias { region, vars, typ } = builtin_alias;
let BuiltinAlias { vars, typ, .. } = builtin_alias;
let mut free_vars = FreeVars::default();
// roc_types::solved_types::to_type(&typ, &mut free_vars, var_store);
let actual = solved_type_to_type_id(pool, &typ, &mut free_vars, var_store);
// make sure to sort these variables to make them line up with the type arguments
let mut type_variables: Vec<_> = free_vars.unnamed_vars.into_iter().collect();
type_variables.sort();
debug_assert_eq!(vars.len(), type_variables.len());
let variables = PoolVec::with_capacity(vars.len() as u32, pool);
let it = variables
.iter_node_ids()
.zip(vars.iter())
.zip(type_variables);
for ((node_id, loc_name), (_, var)) in it {
// TODO region is ignored, but "fake" anyway. How to resolve?
let name = PoolStr::new(loc_name.value.as_str(), pool);
pool[node_id] = (name, var);
}
let alias = Alias {
actual,
/// We know that builtin aliases have no hidden variables (e.g. in closures)
hidden_variables: PoolVec::empty(pool),
targs: variables,
};
aliases.insert(symbol, alias);
}
let idents = Symbol::default_in_scope();
let idents: MutMap<_, _> = idents.into_iter().collect();
Scope {
home,
idents,
symbols: MutMap::default(),
aliases,
}
}
pub fn idents(&self) -> impl Iterator<Item = (&Ident, &(Symbol, Region))> {
self.idents.iter()
}
pub fn symbols(&self) -> impl Iterator<Item = (Symbol, Region)> + '_ {
self.symbols.iter().map(|(x, y)| (*x, *y))
}
pub fn contains_ident(&self, ident: &Ident) -> bool {
self.idents.contains_key(ident)
}
pub fn contains_symbol(&self, symbol: Symbol) -> bool {
self.symbols.contains_key(&symbol)
}
pub fn num_idents(&self) -> usize {
self.idents.len()
}
pub fn lookup(&mut self, ident: &Ident, region: Region) -> Result<Symbol, RuntimeError> {
match self.idents.get(ident) {
Some((symbol, _)) => Ok(*symbol),
None => Err(RuntimeError::LookupNotInScope(
Loc {
region,
value: ident.clone().into(),
},
self.idents.keys().map(|v| v.as_ref().into()).collect(),
)),
}
}
pub fn lookup_alias(&self, symbol: Symbol) -> Option<&Alias> {
self.aliases.get(&symbol)
}
/// Introduce a new ident to scope.
///
/// Returns Err if this would shadow an existing ident, including the
/// Symbol and Region of the ident we already had in scope under that name.
pub fn introduce(
&mut self,
ident: Ident,
exposed_ident_ids: &IdentIds,
all_ident_ids: &mut IdentIds,
region: Region,
) -> Result<Symbol, (Region, Loc<Ident>)> {
match self.idents.get(&ident) {
Some((_, original_region)) => {
let shadow = Loc {
value: ident,
region,
};
Err((*original_region, shadow))
}
None => {
// If this IdentId was already added previously
// when the value was exposed in the module header,
// use that existing IdentId. Otherwise, create a fresh one.
let ident_id = match exposed_ident_ids.get_id(ident.as_str()) {
Some(ident_id) => ident_id,
None => all_ident_ids.add_str(ident.as_str()),
};
let symbol = Symbol::new(self.home, ident_id);
self.symbols.insert(symbol, region);
self.idents.insert(ident, (symbol, region));
Ok(symbol)
}
}
}
/// Ignore an identifier.
///
/// Used for record guards like { x: Just _ }
pub fn ignore(&mut self, ident: Ident, all_ident_ids: &mut IdentIds) -> Symbol {
let ident_id = all_ident_ids.add_str(ident.as_str());
Symbol::new(self.home, ident_id)
}
/// Import a Symbol from another module into this module's top-level scope.
///
/// Returns Err if this would shadow an existing ident, including the
/// Symbol and Region of the ident we already had in scope under that name.
pub fn import(
&mut self,
ident: Ident,
symbol: Symbol,
region: Region,
) -> Result<(), (Symbol, Region)> {
match self.idents.get(&ident) {
Some(shadowed) => Err(*shadowed),
None => {
self.symbols.insert(symbol, region);
self.idents.insert(ident, (symbol, region));
Ok(())
}
}
}
pub fn add_alias(
&mut self,
pool: &mut Pool,
name: Symbol,
vars: PoolVec<(PoolStr, Variable)>,
typ: TypeId,
) {
let mut hidden_variables = MutSet::default();
hidden_variables.extend(typ.variables(pool));
for loc_var in vars.iter(pool) {
hidden_variables.remove(&loc_var.1);
}
let hidden_variables_vec = PoolVec::with_capacity(hidden_variables.len() as u32, pool);
for (node_id, var) in hidden_variables_vec.iter_node_ids().zip(hidden_variables) {
pool[node_id] = var;
}
let alias = Alias {
targs: vars,
hidden_variables: hidden_variables_vec,
actual: typ,
};
self.aliases.insert(name, alias);
}
pub fn contains_alias(&mut self, name: Symbol) -> bool {
self.aliases.contains_key(&name)
}
pub fn fill_scope(&mut self, env: &Env, all_ident_ids: &mut IdentIdsByModule) -> ASTResult<()> {
let ident_ids = get_module_ident_ids(all_ident_ids, &env.home)?.clone();
for (_, ident_ref) in ident_ids.ident_strs() {
self.introduce(
ident_ref.into(),
&env.exposed_ident_ids,
get_module_ident_ids_mut(all_ident_ids, &env.home)?,
Region::zero(),
)?;
}
Ok(())
}
}
impl ShallowClone for Scope {
fn shallow_clone(&self) -> Self {
Self {
idents: self.idents.clone(),
symbols: self.symbols.clone(),
aliases: self
.aliases
.iter()
.map(|(s, a)| (*s, a.shallow_clone()))
.collect(),
home: self.home,
}
}
}

8
crates/ast/src/lib.rs Normal file
View file

@ -0,0 +1,8 @@
pub mod ast_error;
mod canonicalization;
pub mod constrain;
pub mod lang;
pub mod mem_pool;
pub mod module;
pub mod parse;
pub mod solve_type;

View file

@ -0,0 +1,4 @@
pub mod pool;
pub mod pool_str;
pub mod pool_vec;
pub mod shallow_clone;

View file

@ -0,0 +1,269 @@
/// A memory pool of 32-byte nodes. The node value 0 is reserved for the pool's
/// use, and valid nodes may never have that value.
///
/// Internally, the pool is divided into pages of 4096 bytes. It stores nodes
/// into one page at a time, and when it runs out, it uses mmap to reserve an
/// anonymous memory page in which to store nodes.
///
/// Since nodes are 32 bytes, one page can store 128 nodes; you can access a
/// particular node by its NodeId, which is an opaque wrapper around a pointer.
///
/// Pages also use the node value 0 (all 0 bits) to mark nodes as unoccupied.
/// This is important for performance.
use std::any::type_name;
use std::ffi::c_void;
use std::marker::PhantomData;
use std::mem::{align_of, size_of, MaybeUninit};
pub const NODE_BYTES: usize = 32;
// Each page has 128 slots. Each slot holds one 32B node
// This means each page is 4096B, which is the size of a memory page
// on typical systems where the compiler will be run.
//
// Nice things about this system include:
// * Allocating a new page is as simple as asking the OS for a memory page.
// * Since each node is 32B, each node's memory address will be a multiple of 16.
// * Thanks to the free lists and our consistent chunk sizes, we should
// end up with very little fragmentation.
// * Finding a slot for a given node should be very fast: see if the relevant
// free list has any openings; if not, try the next size up.
//
// Less nice things include:
// * This system makes it very hard to ever give a page back to the OS.
// We could try doing the Mesh Allocator strategy: whenever we allocate
// something, assign it to a random slot in the page, and then periodically
// try to merge two pages into one (by locking and remapping them in the OS)
// and then returning the redundant physical page back to the OS. This should
// work in theory, but is pretty complicated, and we'd need to schedule it.
// Keep in mind that we can't use the Mesh Allocator itself because it returns
// usize pointers, which would be too big for us to have 16B nodes.
// On the plus side, we could be okay with higher memory usage early on,
// and then later use the Mesh strategy to reduce long-running memory usage.
//
// With this system, we can allocate up to 4B nodes. If we wanted to keep
// a generational index in there, like https://crates.io/crates/sharded-slab
// does, we could use some of the 32 bits for that. For example, if we wanted
// to have a 5-bit generational index (supporting up to 32 generations), then
// we would have 27 bits remaining, meaning we could only support at most
// 134M nodes. Since the editor has a separate Pool for each module, is that
// enough for any single module we'll encounter in practice? Probably, and
// especially if we allocate super large collection literals on the heap instead
// of in the pool.
//
// Another possible design is to try to catch reuse bugs using an "ASan" like
// approach: in development builds, whenever we "free" a particular slot, we
// can add it to a dev-build-only "freed nodes" list and don't hand it back
// out (so, we leak the memory.) Then we can (again, in development builds only)
// check to see if we're about to store something in zeroed-out memory; if so, check
// to see if it was
#[derive(Debug, Eq)]
pub struct NodeId<T> {
pub(super) index: u32,
pub(super) _phantom: PhantomData<T>,
}
impl<T> Clone for NodeId<T> {
fn clone(&self) -> Self {
NodeId {
index: self.index,
_phantom: PhantomData::default(),
}
}
}
impl<T> PartialEq for NodeId<T> {
fn eq(&self, other: &Self) -> bool {
self.index == other.index
}
}
impl<T> Copy for NodeId<T> {}
#[derive(Debug)]
pub struct Pool {
pub(super) nodes: *mut [MaybeUninit<u8>; NODE_BYTES],
num_nodes: u32,
capacity: u32,
// free_1node_slots: Vec<NodeId<T>>,
}
impl Pool {
pub fn with_capacity(nodes: u32) -> Self {
// round up number of nodes requested to nearest page size in bytes
let bytes_per_page = page_size::get();
let node_bytes = NODE_BYTES * nodes as usize;
let leftover = node_bytes % bytes_per_page;
let bytes_to_mmap = if leftover == 0 {
node_bytes
} else {
node_bytes + bytes_per_page - leftover
};
let nodes = unsafe {
// mmap anonymous memory pages - that is, contiguous virtual memory
// addresses from the OS which will be lazily translated into
// physical memory one 4096-byte page at a time, once we actually
// try to read or write in that page's address range.
#[cfg(unix)]
{
use libc::{MAP_ANONYMOUS, MAP_PRIVATE, PROT_READ, PROT_WRITE};
libc::mmap(
std::ptr::null_mut(),
bytes_to_mmap,
PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS,
0,
0,
)
}
#[cfg(windows)]
{
use winapi::um::memoryapi::VirtualAlloc;
use winapi::um::winnt::PAGE_READWRITE;
use winapi::um::winnt::{MEM_COMMIT, MEM_RESERVE};
VirtualAlloc(
std::ptr::null_mut(),
bytes_to_mmap,
MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE,
)
}
} as *mut [MaybeUninit<u8>; NODE_BYTES];
// This is our actual capacity, in nodes.
// It might be higher than the requested capacity due to rounding up
// to nearest page size.
let capacity = (bytes_to_mmap / NODE_BYTES) as u32;
Pool {
nodes,
num_nodes: 0,
capacity,
}
}
pub fn add<T>(&mut self, node: T) -> NodeId<T> {
// It's only safe to store this if T fits in S.
debug_assert!(
size_of::<T>() <= NODE_BYTES,
"{} has a size of {}, but it needs to be at most {}",
type_name::<T>(),
size_of::<T>(),
NODE_BYTES
);
let node_id = self.reserve(1);
let node_ptr = self.get_ptr(node_id);
unsafe { node_ptr.write(MaybeUninit::new(node)) };
node_id
}
/// Reserves the given number of contiguous node slots, and returns
/// the NodeId of the first one. We only allow reserving 2^32 in a row.
pub(super) fn reserve<T>(&mut self, nodes: u32) -> NodeId<T> {
// TODO once we have a free list, look in there for an open slot first!
let index = self.num_nodes;
if index < self.capacity {
self.num_nodes = index + nodes;
NodeId {
index,
_phantom: PhantomData::default(),
}
} else {
todo!("pool ran out of capacity. TODO reallocate the nodes pointer to map to a bigger space. Can use mremap on Linux, but must memcpy lots of bytes on macOS and Windows.");
}
}
pub fn get<'a, 'b, T>(&'a self, node_id: NodeId<T>) -> &'b T {
unsafe {
let node_ptr = self.get_ptr(node_id) as *const T;
&*node_ptr
}
}
pub fn get_mut<T>(&mut self, node_id: NodeId<T>) -> &mut T {
unsafe {
let node_ptr = self.get_ptr(node_id) as *mut T;
&mut *node_ptr
}
}
pub fn set<T>(&mut self, node_id: NodeId<T>, element: T) {
unsafe {
let node_ptr = self.get_ptr(node_id);
node_ptr.write(MaybeUninit::new(element));
}
}
fn get_ptr<T>(&self, node_id: NodeId<T>) -> *mut MaybeUninit<T> {
let node_offset = unsafe { self.nodes.offset(node_id.index as isize) };
// This checks if the node_offset is aligned to T
assert!(0 == (node_offset as usize) & (align_of::<T>() - 1));
node_offset as *mut MaybeUninit<T>
}
// A node is available iff its bytes are all zeroes
#[allow(dead_code)]
fn is_available<T>(&self, node_id: NodeId<T>) -> bool {
debug_assert_eq!(size_of::<T>(), NODE_BYTES);
unsafe {
let node_ptr = self.nodes.offset(node_id.index as isize) as *const [u8; NODE_BYTES];
*node_ptr == [0; NODE_BYTES]
}
}
}
impl<T> std::ops::Index<NodeId<T>> for Pool {
type Output = T;
fn index(&self, node_id: NodeId<T>) -> &Self::Output {
self.get(node_id)
}
}
impl<T> std::ops::IndexMut<NodeId<T>> for Pool {
fn index_mut(&mut self, node_id: NodeId<T>) -> &mut Self::Output {
self.get_mut(node_id)
}
}
impl Drop for Pool {
fn drop(&mut self) {
unsafe {
#[cfg(unix)]
{
libc::munmap(
self.nodes as *mut c_void,
NODE_BYTES * self.capacity as usize,
);
}
#[cfg(windows)]
{
use winapi::um::memoryapi::VirtualFree;
use winapi::um::winnt::MEM_RELEASE;
VirtualFree(
self.nodes as *mut c_void,
NODE_BYTES * self.capacity as usize,
MEM_RELEASE,
);
}
}
}
}

View file

@ -0,0 +1,86 @@
use super::pool::{NodeId, Pool, NODE_BYTES};
use super::shallow_clone::ShallowClone;
use std::ffi::c_void;
use std::marker::PhantomData;
use std::mem::size_of;
/// A string containing at most 2^32 pool-allocated bytes.
#[derive(Debug, Copy, Clone)]
pub struct PoolStr {
first_node_id: NodeId<()>,
len: u32,
}
#[test]
fn pool_str_size() {
assert_eq!(size_of::<PoolStr>(), 8);
}
impl PoolStr {
pub fn new(string: &str, pool: &mut Pool) -> Self {
debug_assert!(string.len() <= u32::MAX as usize);
let chars_per_node = NODE_BYTES / size_of::<char>();
let number_of_nodes = f64::ceil(string.len() as f64 / chars_per_node as f64) as u32;
if number_of_nodes > 0 {
let first_node_id = pool.reserve(number_of_nodes);
let index = first_node_id.index as isize;
let next_node_ptr = unsafe { pool.nodes.offset(index) } as *mut c_void;
unsafe {
libc::memcpy(
next_node_ptr,
string.as_ptr() as *const c_void,
string.len(),
);
}
PoolStr {
first_node_id,
len: string.len() as u32,
}
} else {
PoolStr {
first_node_id: NodeId {
index: 0,
_phantom: PhantomData::default(),
},
len: 0,
}
}
}
pub fn as_str(&self, pool: &Pool) -> &str {
unsafe {
let node_ptr = pool.nodes.offset(self.first_node_id.index as isize) as *const u8;
let node_slice: &[u8] = std::slice::from_raw_parts(node_ptr, self.len as usize);
std::str::from_utf8_unchecked(&node_slice[0..self.len as usize])
}
}
#[allow(clippy::len_without_is_empty)]
pub fn len(&self, pool: &Pool) -> usize {
let contents = self.as_str(pool);
contents.len()
}
pub fn is_empty(&self, pool: &Pool) -> bool {
self.len(pool) == 0
}
}
impl ShallowClone for PoolStr {
fn shallow_clone(&self) -> Self {
// Question: should this fully clone, or is a shallow copy
// (and the aliasing it entails) OK?
Self {
first_node_id: self.first_node_id,
len: self.len,
}
}
}

View file

@ -0,0 +1,323 @@
use super::pool::{NodeId, Pool, NODE_BYTES};
use super::shallow_clone::ShallowClone;
use std::any::type_name;
use std::cmp::Ordering;
use std::ffi::c_void;
use std::marker::PhantomData;
use std::mem::size_of;
/// An array of at most 2^32 pool-allocated nodes.
#[derive(Debug)]
pub struct PoolVec<T> {
first_node_id: NodeId<T>,
len: u32,
}
#[test]
fn pool_vec_size() {
assert_eq!(size_of::<PoolVec<()>>(), 8);
}
impl<'a, T: 'a + Sized> PoolVec<T> {
pub fn empty(pool: &mut Pool) -> Self {
Self::new(std::iter::empty(), pool)
}
pub fn with_capacity(len: u32, pool: &mut Pool) -> Self {
debug_assert!(
size_of::<T>() <= NODE_BYTES,
"{} has a size of {}",
type_name::<T>(),
size_of::<T>()
);
if len == 0 {
Self::empty(pool)
} else {
let first_node_id = pool.reserve(len);
PoolVec { first_node_id, len }
}
}
pub fn len(&self) -> usize {
self.len as usize
}
pub fn is_empty(&self) -> bool {
self.len == 0
}
pub fn new<I: ExactSizeIterator<Item = T>>(nodes: I, pool: &mut Pool) -> Self {
debug_assert!(nodes.len() <= u32::MAX as usize);
debug_assert!(size_of::<T>() <= NODE_BYTES);
let len = nodes.len() as u32;
if len > 0 {
let first_node_id = pool.reserve(len);
let index = first_node_id.index as isize;
let mut next_node_ptr = unsafe { pool.nodes.offset(index) } as *mut T;
for (indx_inc, node) in nodes.enumerate() {
unsafe {
*next_node_ptr = node;
next_node_ptr = pool.nodes.offset(index + (indx_inc as isize) + 1) as *mut T;
}
}
PoolVec { first_node_id, len }
} else {
PoolVec {
first_node_id: NodeId {
index: 0,
_phantom: PhantomData::default(),
},
len: 0,
}
}
}
pub fn iter(&self, pool: &'a Pool) -> impl ExactSizeIterator<Item = &'a T> {
self.pool_list_iter(pool)
}
pub fn iter_mut(&self, pool: &'a mut Pool) -> impl ExactSizeIterator<Item = &'a mut T> {
self.pool_list_iter_mut(pool)
}
pub fn iter_node_ids(&self) -> impl ExactSizeIterator<Item = NodeId<T>> {
self.pool_list_iter_node_ids()
}
/// Private version of into_iter which exposes the implementation detail
/// of PoolVecIter. We don't want that struct to be public, but we
/// actually do want to have this separate function for code reuse
/// in the iterator's next() method.
#[inline(always)]
fn pool_list_iter(&self, pool: &'a Pool) -> PoolVecIter<'a, T> {
PoolVecIter {
pool,
current_node_id: self.first_node_id,
len_remaining: self.len,
}
}
#[inline(always)]
fn pool_list_iter_mut(&self, pool: &'a Pool) -> PoolVecIterMut<'a, T> {
PoolVecIterMut {
pool,
current_node_id: self.first_node_id,
len_remaining: self.len,
}
}
#[inline(always)]
fn pool_list_iter_node_ids(&self) -> PoolVecIterNodeIds<T> {
PoolVecIterNodeIds {
current_node_id: self.first_node_id,
len_remaining: self.len,
}
}
pub fn free<S>(self, pool: &'a mut Pool) {
// zero out the memory
unsafe {
let index = self.first_node_id.index as isize;
let node_ptr = pool.nodes.offset(index) as *mut c_void;
let bytes = self.len as usize * NODE_BYTES;
libc::memset(node_ptr, 0, bytes);
}
// TODO insert it into the pool's free list
}
}
impl<T> ShallowClone for PoolVec<T> {
fn shallow_clone(&self) -> Self {
// Question: should this fully clone, or is a shallow copy
// (and the aliasing it entails) OK?
Self {
first_node_id: self.first_node_id,
len: self.len,
}
}
}
struct PoolVecIter<'a, T> {
pool: &'a Pool,
current_node_id: NodeId<T>,
len_remaining: u32,
}
impl<'a, T> ExactSizeIterator for PoolVecIter<'a, T>
where
T: 'a,
{
fn len(&self) -> usize {
self.len_remaining as usize
}
}
impl<'a, T> Iterator for PoolVecIter<'a, T>
where
T: 'a,
{
type Item = &'a T;
fn next(&mut self) -> Option<Self::Item> {
let len_remaining = self.len_remaining;
match len_remaining.cmp(&1) {
Ordering::Greater => {
// Get the current node
let index = self.current_node_id.index;
let node_ptr = unsafe { self.pool.nodes.offset(index as isize) } as *const T;
// Advance the node pointer to the next node in the current page
self.current_node_id = NodeId {
index: index + 1,
_phantom: PhantomData::default(),
};
self.len_remaining = len_remaining - 1;
Some(unsafe { &*node_ptr })
}
Ordering::Equal => {
self.len_remaining = 0;
// Don't advance the node pointer's node, because that might
// advance past the end of the page!
let index = self.current_node_id.index;
let node_ptr = unsafe { self.pool.nodes.offset(index as isize) } as *const T;
Some(unsafe { &*node_ptr })
}
Ordering::Less => {
// len_remaining was 0
None
}
}
}
}
struct PoolVecIterMut<'a, T> {
pool: &'a Pool,
current_node_id: NodeId<T>,
len_remaining: u32,
}
impl<'a, T> ExactSizeIterator for PoolVecIterMut<'a, T>
where
T: 'a,
{
fn len(&self) -> usize {
self.len_remaining as usize
}
}
impl<'a, T> Iterator for PoolVecIterMut<'a, T>
where
T: 'a,
{
type Item = &'a mut T;
fn next(&mut self) -> Option<Self::Item> {
let len_remaining = self.len_remaining;
match len_remaining.cmp(&1) {
Ordering::Greater => {
// Get the current node
let index = self.current_node_id.index;
let node_ptr = unsafe { self.pool.nodes.offset(index as isize) } as *mut T;
// Advance the node pointer to the next node in the current page
self.current_node_id = NodeId {
index: index + 1,
_phantom: PhantomData::default(),
};
self.len_remaining = len_remaining - 1;
Some(unsafe { &mut *node_ptr })
}
Ordering::Equal => {
self.len_remaining = 0;
// Don't advance the node pointer's node, because that might
// advance past the end of the page!
let index = self.current_node_id.index;
let node_ptr = unsafe { self.pool.nodes.offset(index as isize) } as *mut T;
Some(unsafe { &mut *node_ptr })
}
Ordering::Less => {
// len_remaining was 0
None
}
}
}
}
struct PoolVecIterNodeIds<T> {
current_node_id: NodeId<T>,
len_remaining: u32,
}
impl<T> ExactSizeIterator for PoolVecIterNodeIds<T> {
fn len(&self) -> usize {
self.len_remaining as usize
}
}
impl<T> Iterator for PoolVecIterNodeIds<T> {
type Item = NodeId<T>;
fn next(&mut self) -> Option<Self::Item> {
let len_remaining = self.len_remaining;
match len_remaining.cmp(&1) {
Ordering::Greater => {
// Get the current node
let current = self.current_node_id;
let index = current.index;
// Advance the node pointer to the next node in the current page
self.current_node_id = NodeId {
index: index + 1,
_phantom: PhantomData::default(),
};
self.len_remaining = len_remaining - 1;
Some(current)
}
Ordering::Equal => {
self.len_remaining = 0;
// Don't advance the node pointer's node, because that might
// advance past the end of the page!
Some(self.current_node_id)
}
Ordering::Less => {
// len_remaining was 0
None
}
}
}
}
#[test]
fn pool_vec_iter_test() {
let expected_vec: Vec<usize> = vec![2, 4, 8, 16];
let mut test_pool = Pool::with_capacity(1024);
let pool_vec = PoolVec::new(expected_vec.clone().into_iter(), &mut test_pool);
let current_vec: Vec<usize> = pool_vec.iter(&test_pool).copied().collect();
assert_eq!(current_vec, expected_vec);
}

View file

@ -0,0 +1,35 @@
use roc_can::expected::Expected;
use roc_can::expected::PExpected;
/// Clones the outer node, but does not clone any nodeids
pub trait ShallowClone {
fn shallow_clone(&self) -> Self;
}
impl<T> ShallowClone for Expected<T>
where
T: ShallowClone,
{
fn shallow_clone(&self) -> Self {
use Expected::*;
match self {
NoExpectation(t) => NoExpectation(t.shallow_clone()),
ForReason(reason, t, region) => ForReason(reason.clone(), t.shallow_clone(), *region),
FromAnnotation(loc_pat, n, source, t) => {
FromAnnotation(loc_pat.clone(), *n, *source, t.shallow_clone())
}
}
}
}
impl<T: ShallowClone> ShallowClone for PExpected<T> {
fn shallow_clone(&self) -> Self {
use PExpected::*;
match self {
NoExpectation(t) => NoExpectation(t.shallow_clone()),
ForReason(reason, t, region) => ForReason(reason.clone(), t.shallow_clone(), *region),
}
}
}

38
crates/ast/src/module.rs Normal file
View file

@ -0,0 +1,38 @@
use bumpalo::Bump;
use roc_load::{LoadedModule, Threading};
use roc_target::TargetInfo;
use std::path::Path;
pub fn load_module(src_file: &Path, threading: Threading) -> LoadedModule {
let subs_by_module = Default::default();
let arena = Bump::new();
let loaded = roc_load::load_and_typecheck(
&arena,
src_file.to_path_buf(),
src_file.parent().unwrap_or_else(|| {
panic!(
"src_file {:?} did not have a parent directory but I need to have one.",
src_file
)
}),
subs_by_module,
TargetInfo::default_x86_64(),
roc_reporting::report::RenderTarget::ColorTerminal,
threading,
);
match loaded {
Ok(x) => x,
Err(roc_load::LoadingProblem::FormattedReport(report)) => {
panic!(
"Failed to load module from src_file: {:?}. Report: {}",
src_file, report
);
}
Err(e) => panic!(
"Failed to load module from src_file {:?}: {:?}",
src_file, e
),
}
}

View file

@ -0,0 +1,2 @@
pub mod parse_ast;
pub mod parse_header;

View file

@ -0,0 +1,54 @@
use bumpalo::Bump;
use roc_module::symbol::Interns;
use roc_region::all::Region;
use crate::{
ast_error::ASTResult,
lang::{
core::{
ast::AST,
def::{def2::DefId, def_to_def2::str_to_def2},
expr::expr2::Expr2,
},
env::Env,
scope::Scope,
},
};
use super::parse_header;
pub fn parse_from_string<'a>(
code_str: &'a str,
env: &mut Env<'a>,
ast_arena: &'a Bump,
interns: &mut Interns,
) -> ASTResult<AST> {
let blank_line_indx = code_str
.find("\n\n")
.expect("I was expecting two newline chars to split header and rest of code.");
let header_str = &code_str[0..blank_line_indx];
let tail_str = &code_str[blank_line_indx..];
let mut scope = Scope::new(env.home, env.pool, env.var_store);
scope.fill_scope(env, &mut interns.all_ident_ids)?;
let region = Region::zero();
let mut def_ids = Vec::<DefId>::new();
let def2_vec = str_to_def2(ast_arena, tail_str, env, &mut scope, region)?;
for def2 in def2_vec {
let def_id = env.pool.add(def2);
def_ids.push(def_id);
}
let ast_node_id = env.pool.add(Expr2::Blank);
Ok(AST {
header: parse_header::parse_from_string(header_str, ast_node_id),
def_ids,
})
}

View file

@ -0,0 +1,12 @@
use crate::lang::core::{expr::expr2::ExprId, header::AppHeader};
// TODO don't use mock struct and actually parse string
pub fn parse_from_string(_header_str: &str, ast_node_id: ExprId) -> AppHeader {
AppHeader {
app_name: "\"untitled-app\"".to_owned(),
packages_base: "\"c-platform/main.roc\"".to_owned(),
imports: vec![],
provides: vec!["main".to_owned()],
ast_node_id,
}
}

1971
crates/ast/src/solve_type.rs Normal file

File diff suppressed because it is too large Load diff

42
crates/bindgen/Cargo.toml Normal file
View file

@ -0,0 +1,42 @@
[package]
name = "roc-bindgen"
version = "0.1.0"
authors = ["The Roc Contributors"]
license = "UPL-1.0"
repository = "https://github.com/rtfeldman/roc"
edition = "2021"
description = "A CLI for roc-bindgen"
[[bin]]
name = "roc-bindgen"
path = "src/main.rs"
test = false
bench = false
[dependencies]
roc_std = { path = "../roc_std" }
roc_can = { path = "../compiler/can" }
roc_mono = { path = "../compiler/mono" }
roc_load = { path = "../compiler/load" }
roc_reporting = { path = "../reporting" }
roc_types = { path = "../compiler/types" }
roc_builtins = { path = "../compiler/builtins" }
roc_module = { path = "../compiler/module" }
roc_collections = { path = "../compiler/collections" }
roc_target = { path = "../compiler/roc_target" }
roc_error_macros = { path = "../error_macros" }
bumpalo = { version = "3.8.0", features = ["collections"] }
target-lexicon = "0.12.3"
clap = { version = "3.1.15", default-features = false, features = ["std", "color", "suggestions", "derive"] }
strum = "0.24.0"
strum_macros = "0.24"
indexmap = "1.8.1"
[dev-dependencies]
pretty_assertions = "1.0.0"
tempfile = "3.2.0"
indoc = "1.0.3"
cli_utils = { path = "../cli_utils" }
roc_test_utils = { path = "../test_utils" }
dircpy = "0.3.9"
ctor = "0.1.22"

View file

@ -0,0 +1,40 @@
use std::io;
static TEMPLATE: &[u8] = include_bytes!("../templates/template.c");
pub fn write_template(writer: &mut impl io::Write) -> io::Result<()> {
writer.write_all(TEMPLATE)?;
Ok(())
}
// pub fn write_bindings(_writer: &mut impl io::Write) -> io::Result<()> {
// extern struct RocStr roc__mainForHost_1_exposed();
// int main() {
// struct RocStr str = roc__mainForHost_1_exposed();
// // Determine str_len and the str_bytes pointer,
// // taking into account the small string optimization.
// size_t str_len = roc_str_len(str);
// char* str_bytes;
// if (is_small_str(str)) {
// str_bytes = (char*)&str;
// } else {
// str_bytes = str.bytes;
// }
// // Write to stdout
// if (write(1, str_bytes, str_len) >= 0) {
// // Writing succeeded!
// return 0;
// } else {
// printf("Error writing to stdout: %s\n", strerror(errno));
// return 1;
// }
// }
// Ok(())
// }

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,18 @@
use std::io;
static TEMPLATE: &[u8] = include_bytes!("../templates/template.zig");
pub fn write_template(writer: &mut impl io::Write) -> io::Result<()> {
writer.write_all(TEMPLATE)?;
Ok(())
}
pub fn write_bindings(_writer: &mut impl io::Write) -> io::Result<()> {
// extern "C" {
// #[link_name = "roc__mainForHost_1_exposed"]
// fn roc_main() -> RocStr;
// }
Ok(())
}

View file

@ -0,0 +1,36 @@
use roc_collections::MutMap;
use roc_types::subs::Variable;
#[derive(Copy, Clone, Debug, Default)]
struct EnumId(u64);
impl EnumId {
pub fn to_name(self) -> String {
format!("U{}", self.0)
}
}
/// Whenever we register a new tag union type,
/// give it a unique and short name (e.g. U1, U2, U3...)
/// and then from then on, whenever we ask for that
/// same record type, return the same name.
#[derive(Default)]
pub struct Enums {
by_variable: MutMap<Variable, EnumId>,
next_id: EnumId,
}
impl Enums {
pub fn get_name(&mut self, var: Variable) -> String {
match self.by_variable.get(&var) {
Some(struct_id) => struct_id.to_name(),
None => self.next_id().to_name(),
}
}
fn next_id(&mut self) -> EnumId {
self.next_id.0 += 1;
self.next_id
}
}

View file

@ -0,0 +1,7 @@
pub mod bindgen_c;
pub mod bindgen_rs;
pub mod bindgen_zig;
pub mod enums;
pub mod load;
pub mod structs;
pub mod types;

View file

@ -0,0 +1,84 @@
use crate::types::{Env, Types};
use bumpalo::Bump;
use roc_load::{LoadedModule, Threading};
use roc_reporting::report::RenderTarget;
use roc_target::{Architecture, TargetInfo};
use std::io;
use std::path::{Path, PathBuf};
use strum::IntoEnumIterator;
use target_lexicon::Triple;
pub fn load_types(
full_file_path: PathBuf,
dir: &Path,
threading: Threading,
) -> Result<Vec<(Types, TargetInfo)>, io::Error> {
let target_info = (&Triple::host()).into();
let arena = &Bump::new();
let subs_by_module = Default::default();
let LoadedModule {
module_id: home,
mut can_problems,
mut type_problems,
mut declarations_by_id,
mut solved,
interns,
..
} = roc_load::load_and_typecheck(
arena,
full_file_path,
dir,
subs_by_module,
target_info,
RenderTarget::Generic,
threading,
)
.expect("Problem loading platform module");
let decls = declarations_by_id.remove(&home).unwrap();
let subs = solved.inner_mut();
let can_problems = can_problems.remove(&home).unwrap_or_default();
let type_problems = type_problems.remove(&home).unwrap_or_default();
if !can_problems.is_empty() || !type_problems.is_empty() {
todo!(
"Gracefully report compilation problems during bindgen: {:?}, {:?}",
can_problems,
type_problems
);
}
let variables = (0..decls.len()).filter_map(|index| {
use roc_can::expr::DeclarationTag::*;
match decls.declarations[index] {
Value | Function(_) | Recursive(_) | TailRecursive(_) => Some(decls.variables[index]),
Destructure(_) => {
// figure out if we need to export non-identifier defs - when would that
// happen?
None
}
MutualRecursion { .. } => {
// handled by future iterations
None
}
Expectation => {
// not publicly visible
None
}
}
});
let types_and_targets = Architecture::iter()
.map(|arch| {
let target_info = arch.into();
let mut env = Env::new(arena, subs, &interns, target_info);
(env.vars_to_types(variables.clone()), target_info)
})
.collect();
Ok(types_and_targets)
}

114
crates/bindgen/src/main.rs Normal file
View file

@ -0,0 +1,114 @@
use clap::Parser;
use roc_bindgen::bindgen_rs;
use roc_bindgen::load::load_types;
use roc_load::Threading;
use std::ffi::OsStr;
use std::fs::File;
use std::io::{ErrorKind, Write};
use std::path::PathBuf;
use std::process;
/// Printed in error messages if you try to use an unsupported extension.
const SUPPORTED_EXTENSIONS: &str = ".c, .rs, .zig, and .json";
// TODO add an option for --targets so that you can specify
// e.g. 64-bit, 32-bit, *and* 16-bit (which can matter for alignment because of pointers)
#[derive(Debug, Parser)]
#[clap(about)]
struct Opts {
/// The path to the `platform` module .roc file
platform_module: PathBuf,
/// The output file, e.g. `test.rs`
dest: PathBuf,
}
enum OutputType {
Rust,
C,
Zig,
Json,
}
pub fn main() {
let opts = Opts::parse();
let input_path = opts.platform_module;
let cwd = std::env::current_dir().unwrap();
let output_path = opts.dest;
let output_type = match output_path.extension().and_then(OsStr::to_str) {
Some("rs") => OutputType::Rust,
Some("c") => OutputType::C,
Some("zig") => OutputType::Zig,
Some("json") => OutputType::Json,
Some(other) => {
eprintln!(
"Unsupported output file extension: \".{}\" - currently supported extensions are {}",
other,
SUPPORTED_EXTENSIONS
);
process::exit(1);
}
None => {
eprintln!("The output file path needs to have a file extension in order to tell what output format to use. Currently supported extensions are {}", SUPPORTED_EXTENSIONS);
process::exit(1);
}
};
match load_types(input_path.clone(), &cwd, Threading::AllAvailable) {
Ok(types_and_targets) => {
let mut file = File::create(output_path.clone()).unwrap_or_else(|err| {
eprintln!(
"Unable to create output file {} - {:?}",
output_path.display(),
err
);
process::exit(1);
});
let mut buf;
match output_type {
OutputType::Rust => {
buf = std::str::from_utf8(bindgen_rs::HEADER).unwrap().to_string();
let body = bindgen_rs::emit(&types_and_targets);
buf.push_str(&body);
}
OutputType::C => todo!("TODO: Generate bindings for C"),
OutputType::Zig => todo!("TODO: Generate bindings for Zig"),
OutputType::Json => todo!("TODO: Generate bindings for JSON"),
};
file.write_all(buf.as_bytes()).unwrap_or_else(|err| {
eprintln!(
"Unable to write bindings to output file {} - {:?}",
output_path.display(),
err
);
process::exit(1);
});
println!(
"🎉 Generated type declarations in:\n\n\t{}",
output_path.display()
);
}
Err(err) => match err.kind() {
ErrorKind::NotFound => {
eprintln!("Platform module file not found: {}", input_path.display());
process::exit(1);
}
error => {
eprintln!(
"Error loading platform module file {} - {:?}",
input_path.display(),
error
);
process::exit(1);
}
},
}
}

View file

@ -0,0 +1,36 @@
use roc_collections::MutMap;
use roc_types::subs::Variable;
#[derive(Copy, Clone, Debug, Default)]
struct StructId(u64);
impl StructId {
pub fn to_name(self) -> String {
format!("R{}", self.0)
}
}
/// Whenever we register a new Roc record type,
/// give it a unique and short name (e.g. R1, R2, R3...)
/// and then from then on, whenever we ask for that
/// same record type, return the same name.
#[derive(Default)]
pub struct Structs {
by_variable: MutMap<Variable, StructId>,
next_id: StructId,
}
impl Structs {
pub fn get_name(&mut self, var: Variable) -> String {
match self.by_variable.get(&var) {
Some(struct_id) => struct_id.to_name(),
None => self.next_id().to_name(),
}
}
fn next_id(&mut self) -> StructId {
self.next_id.0 += 1;
self.next_id
}
}

925
crates/bindgen/src/types.rs Normal file
View file

@ -0,0 +1,925 @@
use crate::enums::Enums;
use crate::structs::Structs;
use bumpalo::Bump;
use roc_builtins::bitcode::{
FloatWidth::*,
IntWidth::{self, *},
};
use roc_collections::VecMap;
use roc_module::symbol::{Interns, Symbol};
use roc_mono::layout::{
cmp_fields, ext_var_is_empty_tag_union, round_up_to_alignment, Builtin, Layout, LayoutCache,
UnionLayout,
};
use roc_target::TargetInfo;
use roc_types::{
subs::{Content, FlatType, GetSubsSlice, Subs, UnionTags, Variable},
types::RecordField,
};
use std::fmt::Display;
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct TypeId(usize);
impl TypeId {
/// Used when making recursive pointers, which need to temporarily
/// have *some* TypeId value until we later in the process determine
/// their real TypeId and can go back and fix them up.
pub(crate) const PENDING: Self = Self(usize::MAX);
}
#[derive(Debug, Clone)]
pub struct Types {
// These are all indexed by TypeId
types: Vec<RocType>,
sizes: Vec<u32>,
aligns: Vec<u32>,
/// Dependencies - that is, which type depends on which other type.
/// This is important for declaration order in C; we need to output a
/// type declaration earlier in the file than where it gets referenced by another type.
deps: VecMap<TypeId, Vec<TypeId>>,
target: TargetInfo,
}
impl Types {
pub fn with_capacity(cap: usize, target_info: TargetInfo) -> Self {
Self {
target: target_info,
types: Vec::with_capacity(cap),
sizes: Vec::new(),
aligns: Vec::new(),
deps: VecMap::with_capacity(cap),
}
}
pub fn add(&mut self, typ: RocType, layout: Layout<'_>) -> TypeId {
let id = TypeId(self.types.len());
self.types.push(typ);
self.sizes
.push(layout.stack_size_without_alignment(self.target));
self.aligns.push(layout.alignment_bytes(self.target));
id
}
pub fn depends(&mut self, id: TypeId, depends_on: TypeId) {
self.deps.get_or_insert(id, Vec::new).push(depends_on);
}
pub fn get_type(&self, id: TypeId) -> &RocType {
match self.types.get(id.0) {
Some(typ) => typ,
None => unreachable!(),
}
}
/// Contrast this with the size_ignoring_alignment method
pub fn size_rounded_to_alignment(&self, id: TypeId) -> u32 {
let size_ignoring_alignment = self.size_ignoring_alignment(id);
let alignment = self.align(id);
round_up_to_alignment(size_ignoring_alignment, alignment)
}
/// Contrast this with the size_rounded_to_alignment method
pub fn size_ignoring_alignment(&self, id: TypeId) -> u32 {
match self.sizes.get(id.0) {
Some(size) => *size,
None => unreachable!(),
}
}
pub fn align(&self, id: TypeId) -> u32 {
match self.aligns.get(id.0) {
Some(align) => *align,
None => unreachable!(),
}
}
pub fn replace(&mut self, id: TypeId, typ: RocType) {
debug_assert!(self.types.get(id.0).is_some());
self.types[id.0] = typ;
}
pub fn ids(&self) -> impl ExactSizeIterator<Item = TypeId> {
(0..self.types.len()).map(TypeId)
}
pub fn sorted_ids(&self) -> Vec<TypeId> {
use roc_collections::{ReferenceMatrix, TopologicalSort};
let mut matrix = ReferenceMatrix::new(self.types.len());
for type_id in self.ids() {
for dep in self.deps.get(&type_id).iter().flat_map(|x| x.iter()) {
matrix.set_row_col(type_id.0, dep.0, true);
}
}
match matrix.topological_sort_into_groups() {
TopologicalSort::Groups { groups } => groups
.into_iter()
.flatten()
.rev()
.map(|n| TypeId(n as usize))
.collect(),
TopologicalSort::HasCycles {
groups: _,
nodes_in_cycle,
} => unreachable!("Cyclic type definitions: {:?}", nodes_in_cycle),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum RocType {
RocStr,
Bool,
Num(RocNum),
RocList(TypeId),
RocDict(TypeId, TypeId),
RocSet(TypeId),
RocBox(TypeId),
TagUnion(RocTagUnion),
Struct {
name: String,
fields: Vec<(String, TypeId)>,
},
TagUnionPayload {
name: String,
fields: Vec<(usize, TypeId)>,
},
/// A recursive pointer, e.g. in StrConsList : [Nil, Cons Str StrConsList],
/// this would be the field of Cons containing the (recursive) StrConsList type,
/// and the TypeId is the TypeId of StrConsList itself.
RecursivePointer(TypeId),
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Copy)]
pub enum RocNum {
I8,
U8,
I16,
U16,
I32,
U32,
I64,
U64,
I128,
U128,
F32,
F64,
F128,
Dec,
}
impl RocNum {
/// These sizes don't vary by target.
pub fn size(&self) -> u32 {
use core::mem::size_of;
let answer = match self {
RocNum::I8 => size_of::<i8>(),
RocNum::U8 => size_of::<u8>(),
RocNum::I16 => size_of::<i16>(),
RocNum::U16 => size_of::<u16>(),
RocNum::I32 => size_of::<i32>(),
RocNum::U32 => size_of::<u32>(),
RocNum::I64 => size_of::<i64>(),
RocNum::U64 => size_of::<u64>(),
RocNum::I128 => size_of::<roc_std::I128>(),
RocNum::U128 => size_of::<roc_std::U128>(),
RocNum::F32 => size_of::<f32>(),
RocNum::F64 => size_of::<f64>(),
RocNum::F128 => todo!(),
RocNum::Dec => size_of::<roc_std::RocDec>(),
};
answer as u32
}
}
impl RocType {
/// Useful when determining whether to derive Copy in a Rust type.
pub fn has_pointer(&self, types: &Types) -> bool {
match self {
RocType::Bool
| RocType::Num(_)
| RocType::TagUnion(RocTagUnion::Enumeration { .. }) => false,
RocType::RocStr
| RocType::RocList(_)
| RocType::RocDict(_, _)
| RocType::RocSet(_)
| RocType::RocBox(_)
| RocType::TagUnion(RocTagUnion::NonNullableUnwrapped { .. })
| RocType::TagUnion(RocTagUnion::NullableUnwrapped { .. })
| RocType::TagUnion(RocTagUnion::NullableWrapped { .. })
| RocType::TagUnion(RocTagUnion::Recursive { .. })
| RocType::RecursivePointer { .. } => true,
RocType::TagUnion(RocTagUnion::NonRecursive { tags, .. }) => {
tags.iter().any(|(_, payloads)| {
payloads
.iter()
.any(|id| types.get_type(*id).has_pointer(types))
})
}
RocType::Struct { fields, .. } => fields
.iter()
.any(|(_, type_id)| types.get_type(*type_id).has_pointer(types)),
RocType::TagUnionPayload { fields, .. } => fields
.iter()
.any(|(_, type_id)| types.get_type(*type_id).has_pointer(types)),
}
}
/// Useful when determining whether to derive Eq, Ord, and Hash in a Rust type.
pub fn has_float(&self, types: &Types) -> bool {
self.has_float_help(types, &[])
}
fn has_float_help(&self, types: &Types, do_not_recurse: &[TypeId]) -> bool {
match self {
RocType::Num(num) => {
use RocNum::*;
match num {
F32 | F64 | F128 => true,
I8 | U8 | I16 | U16 | I32 | U32 | I64 | U64 | I128 | U128 | Dec => false,
}
}
RocType::RocStr
| RocType::Bool
| RocType::TagUnion(RocTagUnion::Enumeration { .. }) => false,
RocType::RocList(id) | RocType::RocSet(id) | RocType::RocBox(id) => {
types.get_type(*id).has_float_help(types, do_not_recurse)
}
RocType::RocDict(key_id, val_id) => {
types
.get_type(*key_id)
.has_float_help(types, do_not_recurse)
|| types
.get_type(*val_id)
.has_float_help(types, do_not_recurse)
}
RocType::Struct { fields, .. } => fields.iter().any(|(_, type_id)| {
types
.get_type(*type_id)
.has_float_help(types, do_not_recurse)
}),
RocType::TagUnionPayload { fields, .. } => fields.iter().any(|(_, type_id)| {
types
.get_type(*type_id)
.has_float_help(types, do_not_recurse)
}),
RocType::TagUnion(RocTagUnion::Recursive { tags, .. })
| RocType::TagUnion(RocTagUnion::NonRecursive { tags, .. }) => {
tags.iter().any(|(_, payloads)| {
payloads
.iter()
.any(|id| types.get_type(*id).has_float_help(types, do_not_recurse))
})
}
RocType::TagUnion(RocTagUnion::NullableWrapped { non_null_tags, .. }) => {
non_null_tags.iter().any(|(_, _, payloads)| {
payloads
.iter()
.any(|id| types.get_type(*id).has_float_help(types, do_not_recurse))
})
}
RocType::TagUnion(RocTagUnion::NullableUnwrapped {
non_null_payload: content,
..
})
| RocType::TagUnion(RocTagUnion::NonNullableUnwrapped { content, .. })
| RocType::RecursivePointer(content) => {
if do_not_recurse.contains(content) {
false
} else {
let mut do_not_recurse: Vec<TypeId> = do_not_recurse.into();
do_not_recurse.push(*content);
types
.get_type(*content)
.has_float_help(types, &do_not_recurse)
}
}
}
}
/// Useful when determining whether to derive Default in a Rust type.
pub fn has_enumeration(&self, types: &Types) -> bool {
match self {
RocType::TagUnion { .. } | RocType::RecursivePointer { .. } => true,
RocType::RocStr | RocType::Bool | RocType::Num(_) => false,
RocType::RocList(id) | RocType::RocSet(id) | RocType::RocBox(id) => {
types.get_type(*id).has_enumeration(types)
}
RocType::RocDict(key_id, val_id) => {
types.get_type(*key_id).has_enumeration(types)
|| types.get_type(*val_id).has_enumeration(types)
}
RocType::Struct { fields, .. } => fields
.iter()
.any(|(_, type_id)| types.get_type(*type_id).has_enumeration(types)),
RocType::TagUnionPayload { fields, .. } => fields
.iter()
.any(|(_, type_id)| types.get_type(*type_id).has_enumeration(types)),
}
}
}
impl From<IntWidth> for RocNum {
fn from(width: IntWidth) -> Self {
match width {
IntWidth::U8 => RocNum::U8,
IntWidth::U16 => RocNum::U16,
IntWidth::U32 => RocNum::U32,
IntWidth::U64 => RocNum::U64,
IntWidth::U128 => RocNum::U128,
IntWidth::I8 => RocNum::I8,
IntWidth::I16 => RocNum::I16,
IntWidth::I32 => RocNum::I32,
IntWidth::I64 => RocNum::I64,
IntWidth::I128 => RocNum::I128,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum RocTagUnion {
Enumeration {
name: String,
tags: Vec<String>,
},
/// A non-recursive tag union
/// e.g. `Result a e : [Ok a, Err e]`
NonRecursive {
name: String,
tags: Vec<(String, Option<TypeId>)>,
discriminant_type: RocNum,
},
/// A recursive tag union (general case)
/// e.g. `Expr : [Sym Str, Add Expr Expr]`
Recursive {
name: String,
tags: Vec<(String, Option<TypeId>)>,
discriminant_type: RocNum,
},
/// A recursive tag union with just one constructor
/// Optimization: No need to store a tag ID (the payload is "unwrapped")
/// e.g. `RoseTree a : [Tree a (List (RoseTree a))]`
NonNullableUnwrapped {
name: String,
content: TypeId,
},
/// A recursive tag union that has an empty variant
/// Optimization: Represent the empty variant as null pointer => no memory usage & fast comparison
/// It has more than one other variant, so they need tag IDs (payloads are "wrapped")
/// e.g. `FingerTree a : [Empty, Single a, More (Some a) (FingerTree (Tuple a)) (Some a)]`
/// see also: https://youtu.be/ip92VMpf_-A?t=164
NullableWrapped {
name: String,
null_tag: String,
non_null_tags: Vec<(u16, String, Option<TypeId>)>,
},
/// A recursive tag union with only two variants, where one is empty.
/// Optimizations: Use null for the empty variant AND don't store a tag ID for the other variant.
/// e.g. `ConsList a : [Nil, Cons a (ConsList a)]`
NullableUnwrapped {
name: String,
/// e.g. Nil in `StrConsList : [Nil, Cons Str (ConsList Str)]`
null_tag: String,
/// e.g. Cons in `StrConsList : [Nil, Cons Str (ConsList Str)]`
non_null_tag: String,
/// There must be a payload associated with the non-null tag.
/// Otherwise, this would have been an Enumeration!
non_null_payload: TypeId,
/// True iff the first tag (alphabetically) is represented by null.
/// If this is false, it means the second tag is represented by null instead.
null_represents_first_tag: bool,
},
}
pub struct Env<'a> {
arena: &'a Bump,
subs: &'a Subs,
layout_cache: LayoutCache<'a>,
interns: &'a Interns,
struct_names: Structs,
enum_names: Enums,
pending_recursive_types: VecMap<TypeId, Layout<'a>>,
known_recursive_types: VecMap<Layout<'a>, TypeId>,
target: TargetInfo,
}
impl<'a> Env<'a> {
pub fn new(arena: &'a Bump, subs: &'a Subs, interns: &'a Interns, target: TargetInfo) -> Self {
Env {
arena,
subs,
interns,
struct_names: Default::default(),
enum_names: Default::default(),
pending_recursive_types: Default::default(),
known_recursive_types: Default::default(),
layout_cache: LayoutCache::new(target),
target,
}
}
pub fn vars_to_types<I>(&mut self, variables: I) -> Types
where
I: Iterator<Item = Variable>,
{
let mut types = Types::with_capacity(variables.size_hint().0, self.target);
for var in variables {
self.add_type(var, &mut types);
}
self.resolve_pending_recursive_types(&mut types);
types
}
fn add_type(&mut self, var: Variable, types: &mut Types) -> TypeId {
let layout = self
.layout_cache
.from_var(self.arena, var, self.subs)
.expect("Something weird ended up in the content");
add_type_help(self, layout, var, None, types)
}
fn resolve_pending_recursive_types(&mut self, types: &mut Types) {
// TODO if VecMap gets a drain() method, use that instead of doing take() and into_iter
let pending = core::mem::take(&mut self.pending_recursive_types);
for (type_id, layout) in pending.into_iter() {
let actual_type_id = self.known_recursive_types.get(&layout).unwrap_or_else(|| {
unreachable!(
"There was no known recursive TypeId for the pending recursive type {:?}",
layout
);
});
debug_assert!(
matches!(types.get_type(type_id), RocType::RecursivePointer(TypeId::PENDING)),
"The TypeId {:?} was registered as a pending recursive pointer, but was not stored in Types as one.",
type_id
);
// size and alignment shouldn't change; this is still
// a RecursivePointer, it's just pointing to something else.
types.replace(type_id, RocType::RecursivePointer(*actual_type_id));
}
}
}
fn add_type_help<'a>(
env: &mut Env<'a>,
layout: Layout<'a>,
var: Variable,
opt_name: Option<Symbol>,
types: &mut Types,
) -> TypeId {
let subs = env.subs;
match subs.get_content_without_compacting(var) {
Content::FlexVar(_)
| Content::RigidVar(_)
| Content::FlexAbleVar(_, _)
| Content::RigidAbleVar(_, _) => {
todo!("TODO give a nice error message for a non-concrete type being passed to the host")
}
Content::Structure(FlatType::Record(fields, ext)) => {
let it = fields
.unsorted_iterator(subs, *ext)
.expect("something weird in content")
.flat_map(|(label, field)| {
match field {
RecordField::Required(field_var) | RecordField::Demanded(field_var) => {
Some((label.to_string(), field_var))
}
RecordField::Optional(_) => {
// drop optional fields
None
}
}
});
let name = match opt_name {
Some(sym) => sym.as_str(env.interns).to_string(),
None => env.struct_names.get_name(var),
};
add_struct(env, name, it, types, layout, |name, fields| {
RocType::Struct { name, fields }
})
}
Content::Structure(FlatType::TagUnion(tags, ext_var)) => {
debug_assert!(ext_var_is_empty_tag_union(subs, *ext_var));
add_tag_union(env, opt_name, tags, var, types, layout)
}
Content::Structure(FlatType::RecursiveTagUnion(_rec_var, tag_vars, ext_var)) => {
debug_assert!(ext_var_is_empty_tag_union(subs, *ext_var));
add_tag_union(env, opt_name, tag_vars, var, types, layout)
}
Content::Structure(FlatType::Apply(symbol, _)) => match layout {
Layout::Builtin(builtin) => {
add_builtin_type(env, builtin, var, opt_name, types, layout)
}
_ => {
if symbol.is_builtin() {
todo!(
"Handle Apply for builtin symbol {:?} and layout {:?}",
symbol,
layout
)
} else {
todo!(
"Handle non-builtin Apply for symbol {:?} and layout {:?}",
symbol,
layout
)
}
}
},
Content::Structure(FlatType::Func(_, _, _)) => {
todo!()
}
Content::Structure(FlatType::FunctionOrTagUnion(_, _, _)) => {
todo!()
}
Content::Structure(FlatType::Erroneous(_)) => todo!(),
Content::Structure(FlatType::EmptyRecord) => todo!(),
Content::Structure(FlatType::EmptyTagUnion) => {
// This can happen when unwrapping a tag union; don't do anything.
todo!()
}
Content::Alias(name, _, real_var, _) => {
if name.is_builtin() {
match layout {
Layout::Builtin(builtin) => {
add_builtin_type(env, builtin, var, opt_name, types, layout)
}
_ => {
unreachable!()
}
}
} else {
// If this was a non-builtin type alias, we can use that alias name
// in the generated bindings.
add_type_help(env, layout, *real_var, Some(*name), types)
}
}
Content::RangedNumber(_, _) => todo!(),
Content::Error => todo!(),
Content::RecursionVar { structure, .. } => {
let type_id = types.add(RocType::RecursivePointer(TypeId::PENDING), layout);
let structure_layout = env
.layout_cache
.from_var(env.arena, *structure, subs)
.unwrap();
env.pending_recursive_types
.insert(type_id, structure_layout);
type_id
}
Content::LambdaSet(_) => todo!(),
}
}
fn add_builtin_type<'a>(
env: &mut Env<'a>,
builtin: Builtin<'a>,
var: Variable,
opt_name: Option<Symbol>,
types: &mut Types,
layout: Layout<'_>,
) -> TypeId {
use Content::*;
use FlatType::*;
let builtin_type = env.subs.get_content_without_compacting(var);
match (builtin, builtin_type) {
(Builtin::Int(width), _) => match width {
U8 => types.add(RocType::Num(RocNum::U8), layout),
U16 => types.add(RocType::Num(RocNum::U16), layout),
U32 => types.add(RocType::Num(RocNum::U32), layout),
U64 => types.add(RocType::Num(RocNum::U64), layout),
U128 => types.add(RocType::Num(RocNum::U128), layout),
I8 => types.add(RocType::Num(RocNum::I8), layout),
I16 => types.add(RocType::Num(RocNum::I16), layout),
I32 => types.add(RocType::Num(RocNum::I32), layout),
I64 => types.add(RocType::Num(RocNum::I64), layout),
I128 => types.add(RocType::Num(RocNum::I128), layout),
},
(Builtin::Float(width), _) => match width {
F32 => types.add(RocType::Num(RocNum::F32), layout),
F64 => types.add(RocType::Num(RocNum::F64), layout),
F128 => types.add(RocType::Num(RocNum::F128), layout),
},
(Builtin::Decimal, _) => types.add(RocType::Num(RocNum::Dec), layout),
(Builtin::Bool, _) => types.add(RocType::Bool, layout),
(Builtin::Str, _) => types.add(RocType::RocStr, layout),
(Builtin::Dict(key_layout, val_layout), Structure(Apply(Symbol::DICT_DICT, args))) => {
let args = env.subs.get_subs_slice(*args);
debug_assert_eq!(args.len(), 2);
let key_id = add_type_help(env, *key_layout, args[0], opt_name, types);
let val_id = add_type_help(env, *val_layout, args[1], opt_name, types);
let dict_id = types.add(RocType::RocDict(key_id, val_id), layout);
types.depends(dict_id, key_id);
types.depends(dict_id, val_id);
dict_id
}
(Builtin::Set(elem_layout), Structure(Apply(Symbol::SET_SET, args))) => {
let args = env.subs.get_subs_slice(*args);
debug_assert_eq!(args.len(), 1);
let elem_id = add_type_help(env, *elem_layout, args[0], opt_name, types);
let set_id = types.add(RocType::RocSet(elem_id), layout);
types.depends(set_id, elem_id);
set_id
}
(Builtin::List(elem_layout), Structure(Apply(Symbol::LIST_LIST, args))) => {
let args = env.subs.get_subs_slice(*args);
debug_assert_eq!(args.len(), 1);
let elem_id = add_type_help(env, *elem_layout, args[0], opt_name, types);
let list_id = types.add(RocType::RocList(elem_id), layout);
types.depends(list_id, elem_id);
list_id
}
(layout, typ) => todo!("Handle builtin layout {:?} and type {:?}", layout, typ),
}
}
fn add_struct<I, L, F>(
env: &mut Env<'_>,
name: String,
fields: I,
types: &mut Types,
layout: Layout<'_>,
to_type: F,
) -> TypeId
where
I: IntoIterator<Item = (L, Variable)>,
L: Display + Ord,
F: FnOnce(String, Vec<(L, TypeId)>) -> RocType,
{
let subs = env.subs;
let fields_iter = &mut fields.into_iter();
let mut sortables =
bumpalo::collections::Vec::with_capacity_in(fields_iter.size_hint().0, env.arena);
for (label, field_var) in fields_iter {
sortables.push((
label,
field_var,
env.layout_cache
.from_var(env.arena, field_var, subs)
.unwrap(),
));
}
sortables.sort_by(|(label1, _, layout1), (label2, _, layout2)| {
cmp_fields(
label1,
layout1,
label2,
layout2,
env.layout_cache.target_info,
)
});
let fields = sortables
.into_iter()
.map(|(label, field_var, field_layout)| {
let type_id = add_type_help(env, field_layout, field_var, None, types);
(label, type_id)
})
.collect::<Vec<(L, TypeId)>>();
types.add(to_type(name, fields), layout)
}
fn add_tag_union<'a>(
env: &mut Env<'a>,
opt_name: Option<Symbol>,
union_tags: &UnionTags,
var: Variable,
types: &mut Types,
layout: Layout<'a>,
) -> TypeId {
let subs = env.subs;
let mut tags: Vec<(String, Vec<Variable>)> = union_tags
.iter_from_subs(subs)
.map(|(tag_name, payload_vars)| {
let name_str = tag_name.0.as_str().to_string();
(name_str, payload_vars.to_vec())
})
.collect();
let name = match opt_name {
Some(sym) => sym.as_str(env.interns).to_string(),
None => env.enum_names.get_name(var),
};
// Sort tags alphabetically by tag name
tags.sort_by(|(name1, _), (name2, _)| name1.cmp(name2));
let is_recursive = is_recursive_tag_union(layout);
let mut tags: Vec<_> = tags
.into_iter()
.map(|(tag_name, payload_vars)| {
match struct_fields_needed(env, payload_vars.iter().copied()) {
0 => {
// no payload
(tag_name, None)
}
1 if !is_recursive => {
// this isn't recursive and there's 1 payload item, so it doesn't
// need its own struct - e.g. for `[Foo Str, Bar Str]` both of them
// can have payloads of plain old Str, no struct wrapper needed.
let payload_var = payload_vars.get(0).unwrap();
let payload_layout = env
.layout_cache
.from_var(env.arena, *payload_var, env.subs)
.expect("Something weird ended up in the content");
let payload_id = add_type_help(env, payload_layout, *payload_var, None, types);
(tag_name, Some(payload_id))
}
_ => {
// create a RocType for the payload and save it
let struct_name = format!("{}_{}", name, tag_name); // e.g. "MyUnion_MyVariant"
let fields = payload_vars.iter().copied().enumerate();
let struct_id =
add_struct(env, struct_name, fields, types, layout, |name, fields| {
RocType::TagUnionPayload { name, fields }
});
(tag_name, Some(struct_id))
}
}
})
.collect();
let typ = match layout {
Layout::Union(union_layout) => {
use UnionLayout::*;
match union_layout {
// A non-recursive tag union
// e.g. `Result ok err : [Ok ok, Err err]`
NonRecursive(_) => {
let discriminant_type = UnionLayout::discriminant_size(tags.len()).into();
RocType::TagUnion(RocTagUnion::NonRecursive {
name,
tags,
discriminant_type,
})
}
// A recursive tag union (general case)
// e.g. `Expr : [Sym Str, Add Expr Expr]`
Recursive(_) => {
let discriminant_type = UnionLayout::discriminant_size(tags.len()).into();
RocType::TagUnion(RocTagUnion::Recursive {
name,
tags,
discriminant_type,
})
}
// A recursive tag union with just one constructor
// Optimization: No need to store a tag ID (the payload is "unwrapped")
// e.g. `RoseTree a : [Tree a (List (RoseTree a))]`
NonNullableUnwrapped(_) => {
todo!()
}
// A recursive tag union that has an empty variant
// Optimization: Represent the empty variant as null pointer => no memory usage & fast comparison
// It has more than one other variant, so they need tag IDs (payloads are "wrapped")
// e.g. `FingerTree a : [Empty, Single a, More (Some a) (FingerTree (Tuple a)) (Some a)]`
// see also: https://youtu.be/ip92VMpf_-A?t=164
NullableWrapped { .. } => {
todo!()
}
// A recursive tag union with only two variants, where one is empty.
// Optimizations: Use null for the empty variant AND don't store a tag ID for the other variant.
// e.g. `ConsList a : [Nil, Cons a (ConsList a)]`
NullableUnwrapped {
nullable_id: null_represents_first_tag,
other_fields: _, // TODO use this!
} => {
// NullableUnwrapped tag unions should always have exactly 2 tags.
debug_assert_eq!(tags.len(), 2);
let null_tag;
let non_null;
if null_represents_first_tag {
// If nullable_id is true, then the null tag is second, which means
// pop() will return it because it's at the end of the vec.
null_tag = tags.pop().unwrap().0;
non_null = tags.pop().unwrap();
} else {
// The null tag is first, which means the tag with the payload is second.
non_null = tags.pop().unwrap();
null_tag = tags.pop().unwrap().0;
}
let (non_null_tag, non_null_payload) = non_null;
RocType::TagUnion(RocTagUnion::NullableUnwrapped {
name,
null_tag,
non_null_tag,
non_null_payload: non_null_payload.unwrap(),
null_represents_first_tag,
})
}
}
}
Layout::Builtin(Builtin::Int(_)) => RocType::TagUnion(RocTagUnion::Enumeration {
name,
tags: tags.into_iter().map(|(tag_name, _)| tag_name).collect(),
}),
Layout::Builtin(_)
| Layout::Struct { .. }
| Layout::Boxed(_)
| Layout::LambdaSet(_)
| Layout::RecursivePointer => {
// These must be single-tag unions. Bindgen ordinary nonrecursive
// tag unions for them, and let Rust do the unwrapping.
//
// This should be a very rare use case, and it's not worth overcomplicating
// the rest of bindgen to make it do something different.
RocType::TagUnion(RocTagUnion::NonRecursive {
name,
tags,
discriminant_type: RocNum::U8,
})
}
};
let type_id = types.add(typ, layout);
if is_recursive {
env.known_recursive_types.insert(layout, type_id);
}
type_id
}
fn is_recursive_tag_union(layout: Layout) -> bool {
use roc_mono::layout::UnionLayout::*;
match layout {
Layout::Union(tag_union) => match tag_union {
NonRecursive(_) => false,
Recursive(_)
| NonNullableUnwrapped(_)
| NullableWrapped { .. }
| NullableUnwrapped { .. } => true,
},
_ => false,
}
}
fn struct_fields_needed<I: IntoIterator<Item = Variable>>(env: &mut Env<'_>, vars: I) -> usize {
let subs = env.subs;
let arena = env.arena;
vars.into_iter().fold(0, |count, var| {
let layout = env.layout_cache.from_var(arena, var, subs).unwrap();
if layout.is_dropped_because_empty() {
count
} else {
count + 1
}
})
}

View file

@ -0,0 +1,8 @@
// ⚠️ GENERATED CODE ⚠️ - this entire file was generated by the `roc-bindgen` CLI
#![allow(dead_code)]
#![allow(unused_mut)]
#![allow(non_snake_case)]
#![allow(non_camel_case_types)]
#![allow(non_upper_case_globals)]
#![allow(clippy::undocumented_unsafe_blocks)]

View file

@ -0,0 +1,59 @@
#include <errno.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
void* roc_alloc(size_t size, unsigned int alignment) { return malloc(size); }
void* roc_realloc(void* ptr, size_t new_size, size_t old_size, unsigned int alignment) {
return realloc(ptr, new_size);
}
void roc_dealloc(void* ptr, unsigned int alignment) { free(ptr); }
void roc_panic(void* ptr, unsigned int alignment) {
char* msg = (char*)ptr;
fprintf(stderr,
"Application crashed with message\n\n %s\n\nShutting down\n", msg);
exit(0);
}
void* roc_memcpy(void* dest, const void* src, size_t n) {
return memcpy(dest, src, n);
}
void* roc_memset(void* str, int c, size_t n) { return memset(str, c, n); }
///////////////////////////////////////////////////////////////////////////
//
// roc_std
//
///////////////////////////////////////////////////////////////////////////
struct RocStr {
char* bytes;
size_t len;
};
bool is_small_str(struct RocStr str) { return ((ssize_t)str.len) < 0; }
// Determine the length of the string, taking into
// account the small string optimization
size_t roc_str_len(struct RocStr str) {
char* bytes = (char*)&str;
char last_byte = bytes[sizeof(str) - 1];
char last_byte_xored = last_byte ^ 0b10000000;
size_t small_len = (size_t)(last_byte_xored);
size_t big_len = str.len;
// Avoid branch misprediction costs by always
// determining both small_len and big_len,
// so this compiles to a cmov instruction.
if (is_small_str(str)) {
return small_len;
} else {
return big_len;
}
}

View file

@ -0,0 +1,79 @@
#![allow(non_snake_case)]
use core::ffi::c_void;
// TODO don't have these depend on the libc crate; instead, use default
// allocator, built-in memset, etc.
#[no_mangle]
pub unsafe extern "C" fn roc_alloc(size: usize, _alignment: u32) -> *mut c_void {
return libc::malloc(size);
}
#[no_mangle]
pub unsafe extern "C" fn roc_realloc(
c_ptr: *mut c_void,
new_size: usize,
_old_size: usize,
_alignment: u32,
) -> *mut c_void {
return libc::realloc(c_ptr, new_size);
}
#[no_mangle]
pub unsafe extern "C" fn roc_dealloc(c_ptr: *mut c_void, _alignment: u32) {
return libc::free(c_ptr);
}
#[no_mangle]
pub unsafe extern "C" fn roc_panic(c_ptr: *mut c_void, tag_id: u32) {
use std::ffi::CStr;
use std::os::raw::c_char;
match tag_id {
0 => {
let slice = CStr::from_ptr(c_ptr as *const c_char);
let string = slice.to_str().unwrap();
eprintln!("Roc hit a panic: {}", string);
std::process::exit(1);
}
_ => todo!(),
}
}
#[no_mangle]
pub unsafe extern "C" fn roc_memcpy(dst: *mut c_void, src: *mut c_void, n: usize) -> *mut c_void {
libc::memcpy(dst, src, n)
}
#[no_mangle]
pub unsafe extern "C" fn roc_memset(dst: *mut c_void, c: i32, n: usize) -> *mut c_void {
libc::memset(dst, c, n)
}
////////////////////////////////////////////////////////////////////////////
//
// TODO: rust_main should be removed once we use surgical linking everywhere.
// It's just a workaround to get cargo to build an object file the way
// the non-surgical linker needs it to. The surgical linker works on
// executables, not object files, so this workaround is not needed there.
//
////////////////////////////////////////////////////////////////////////////
#[no_mangle]
pub extern "C" fn rust_main() -> i32 {
use roc_std::RocStr;
unsafe {
let roc_str = roc_main();
let len = roc_str.len();
let str_bytes = roc_str.as_bytes().as_ptr() as *const libc::c_void;
if libc::write(1, str_bytes, len) < 0 {
panic!("Writing to stdout failed!");
}
}
// Exit code
0
}

View file

@ -0,0 +1,71 @@
const std = @import("std");
const str = @import("str");
comptime {
// This is a workaround for https://github.com/ziglang/zig/issues/8218
// which is only necessary on macOS.
//
// Once that issue is fixed, we can undo the changes in
// 177cf12e0555147faa4d436e52fc15175c2c4ff0 and go back to passing
// -fcompiler-rt in link.rs instead of doing this. Note that this
// workaround is present in many host.zig files, so make sure to undo
// it everywhere!
if (std.builtin.os.tag == .macos) {
_ = @import("compiler_rt");
}
}
const Align = 2 * @alignOf(usize);
extern fn malloc(size: usize) callconv(.C) ?*align(Align) anyopaque;
extern fn realloc(c_ptr: [*]align(Align) u8, size: usize) callconv(.C) ?*anyopaque;
extern fn free(c_ptr: [*]align(Align) u8) callconv(.C) void;
extern fn memcpy(dst: [*]u8, src: [*]u8, size: usize) callconv(.C) void;
extern fn memset(dst: [*]u8, value: i32, size: usize) callconv(.C) void;
const DEBUG: bool = false;
export fn roc_alloc(size: usize, alignment: u32) callconv(.C) ?*anyopaque {
if (DEBUG) {
var ptr = malloc(size);
const stdout = std.io.getStdOut().writer();
stdout.print("alloc: {d} (alignment {d}, size {d})\n", .{ ptr, alignment, size }) catch unreachable;
return ptr;
} else {
return malloc(size);
}
}
export fn roc_realloc(c_ptr: *anyopaque, new_size: usize, old_size: usize, alignment: u32) callconv(.C) ?*anyopaque {
if (DEBUG) {
const stdout = std.io.getStdOut().writer();
stdout.print("realloc: {d} (alignment {d}, old_size {d})\n", .{ c_ptr, alignment, old_size }) catch unreachable;
}
return realloc(@alignCast(Align, @ptrCast([*]u8, c_ptr)), new_size);
}
export fn roc_dealloc(c_ptr: *anyopaque, alignment: u32) callconv(.C) void {
if (DEBUG) {
const stdout = std.io.getStdOut().writer();
stdout.print("dealloc: {d} (alignment {d})\n", .{ c_ptr, alignment }) catch unreachable;
}
free(@alignCast(Align, @ptrCast([*]u8, c_ptr)));
}
export fn roc_panic(c_ptr: *anyopaque, tag_id: u32) callconv(.C) void {
_ = tag_id;
const stderr = std.io.getStdErr().writer();
const msg = @ptrCast([*:0]const u8, c_ptr);
stderr.print("Application crashed with message\n\n {s}\n\nShutting down\n", .{msg}) catch unreachable;
std.process.exit(0);
}
export fn roc_memcpy(dst: [*]u8, src: [*]u8, size: usize) callconv(.C) void {
return memcpy(dst, src, size);
}
export fn roc_memset(dst: [*]u8, value: i32, size: usize) callconv(.C) void {
return memset(dst, value, size);
}

View file

@ -0,0 +1,37 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "host"
version = "0.1.0"
dependencies = [
"indoc",
"libc",
"roc_std",
]
[[package]]
name = "indoc"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05a0bd019339e5d968b37855180087b7b9d512c5046fbd244cf8c95687927d6e"
[[package]]
name = "libc"
version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56d855069fafbb9b344c0f962150cd2c1187975cb1c22c1522c240d8c4986714"
[[package]]
name = "roc_std"
version = "0.1.0"
dependencies = [
"static_assertions",
]
[[package]]
name = "static_assertions"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f406d6ee68db6796e11ffd7b4d171864c58b7451e79ef9460ea33c287a1f89a7"

View file

@ -0,0 +1,34 @@
# ⚠️ READ THIS BEFORE MODIFYING THIS FILE! ⚠️
#
# This file is a fixture template. If the file you're looking at is
# in the fixture-templates/ directory, then you're all set - go ahead
# and modify it, and it will modify all the fixture tests.
#
# If this file is in the fixtures/ directory, on the other hand, then
# it is gitignored and will be overwritten the next time tests run.
# So you probably don't want to modify it by hand! Instead, modify the
# file with the same name in the fixture-templates/ directory.
[package]
name = "host"
version = "0.1.0"
authors = ["The Roc Contributors"]
license = "UPL-1.0"
edition = "2018"
links = "app"
[lib]
name = "host"
path = "src/lib.rs"
crate-type = ["staticlib", "rlib"]
[[bin]]
name = "host"
path = "src/main.rs"
[dependencies]
roc_std = { path = "../../../../roc_std" }
libc = "0.2"
indoc = "1.0.6"
[workspace]

View file

@ -0,0 +1,15 @@
// ⚠️ READ THIS BEFORE MODIFYING THIS FILE! ⚠️
//
// This file is a fixture template. If the file you're looking at is
// in the fixture-templates/ directory, then you're all set - go ahead
// and modify it, and it will modify all the fixture tests.
//
// If this file is in the fixtures/ directory, on the other hand, then
// it is gitignored and will be overwritten the next time tests run.
// So you probably don't want to modify it by hand! Instead, modify the
// file with the same name in the fixture-templates/ directory.
fn main() {
println!("cargo:rustc-link-lib=dylib=app");
println!("cargo:rustc-link-search=.");
}

View file

@ -0,0 +1,14 @@
// ⚠️ READ THIS BEFORE MODIFYING THIS FILE! ⚠️
//
// This file is a fixture template. If the file you're looking at is
// in the fixture-templates/ directory, then you're all set - go ahead
// and modify it, and it will modify all the fixture tests.
//
// If this file is in the fixtures/ directory, on the other hand, then
// it is gitignored and will be overwritten the next time tests run.
// So you probably don't want to modify it by hand! Instead, modify the
// file with the same name in the fixture-templates/ directory.
extern int rust_main();
int main() { return rust_main(); }

View file

@ -0,0 +1,3 @@
fn main() {
std::process::exit(host::rust_main());
}

View file

@ -0,0 +1,12 @@
Cargo.lock
Cargo.toml
build.rs
host.c
bindings.rs
roc_externs.rs
main.rs
app
dynhost
libapp.so
metadata
preprocessedhost

View file

@ -0,0 +1,14 @@
app "app"
packages { pf: "platform.roc" }
imports []
provides [main] to pf
main = {
default: Job {
command: Command {
tool: SystemTool { name: "test", num: 42 }
},
inputFiles : ["foo"]
}
}

View file

@ -0,0 +1,26 @@
platform "test-platform"
requires {} { main : _ }
exposes []
packages {}
imports []
provides [mainForHost]
Tool : [
SystemTool { name : Str, num : U32 },
FromJob { job : Job, num : U32 }
]
Command : [Command { tool : Tool }]
Job : [
Job { command : Command, inputFiles : List Str },
Foo Str,
# TODO make a recursive tag union test that doesn't try to do mutual recursion,
# just so I can get a PR up.
# WithTool Tool # Mutual recursion; Tool also references Job
]
Rbt : { default: Job }
mainForHost : Rbt
mainForHost = main

View file

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

View file

@ -0,0 +1,6 @@
app "app"
packages { pf: "platform.roc" }
imports []
provides [main] to pf
main = { a: 1995, b: 42 }

View file

@ -0,0 +1,11 @@
platform "test-platform"
requires {} { main : _ }
exposes []
packages {}
imports []
provides [mainForHost]
MyRcd : { a : U64, b : U128 }
mainForHost : MyRcd
mainForHost = main

View file

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

View file

@ -0,0 +1,6 @@
app "app"
packages { pf: "platform.roc" }
imports []
provides [main] to pf
main = Concat (String "Hello, ") (String "World!")

View file

@ -0,0 +1,11 @@
platform "test-platform"
requires {} { main : _ }
exposes []
packages {}
imports []
provides [mainForHost]
Expr : [String Str, Concat Expr Expr]
mainForHost : Expr
mainForHost = main

View file

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

View file

@ -0,0 +1,6 @@
app "app"
packages { pf: "platform.roc" }
imports []
provides [main] to pf
main = Foo

View file

@ -0,0 +1,11 @@
platform "test-platform"
requires {} { main : _ }
exposes []
packages {}
imports []
provides [mainForHost]
MyEnum : [Foo, Bar, Baz]
mainForHost : MyEnum
mainForHost = main

View file

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

View file

@ -0,0 +1,16 @@
app "app"
packages { pf: "platform.roc" }
imports []
provides [main] to pf
main = {
default: Job {
command: Command {
tool: SystemTool { name: "test" },
args: [],
},
job: [],
inputFiles : ["foo"],
}
}

View file

@ -0,0 +1,17 @@
platform "test-platform"
requires {} { main : _ }
exposes []
packages {}
imports []
provides [mainForHost]
Tool : [SystemTool { name: Str }]
Command : [Command { tool : Tool, args: List Str }]
Job : [Job { command : Command, job: List Job, inputFiles : List Str }, Blah Str]
Rbt : { default: Job }
mainForHost : Rbt
mainForHost = main

View file

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

View file

@ -0,0 +1,6 @@
app "app"
packages { pf: "platform.roc" }
imports []
provides [main] to pf
main = { x: { a: 5, b: 24 }, y: "foo", z: [1, 2] }

View file

@ -0,0 +1,13 @@
platform "test-platform"
requires {} { main : _ }
exposes []
packages {}
imports []
provides [mainForHost]
Outer : { x : Inner, y : Str, z : List U8 }
Inner : { a : U16, b : F32 }
mainForHost : Outer
mainForHost = main

View file

@ -0,0 +1,95 @@
mod bindings;
extern "C" {
#[link_name = "roc__mainForHost_1_exposed_generic"]
fn roc_main(_: *mut bindings::Outer);
}
#[no_mangle]
pub extern "C" fn rust_main() -> i32 {
use std::cmp::Ordering;
let outer = unsafe {
let mut ret: core::mem::MaybeUninit<bindings::Outer> = core::mem::MaybeUninit::uninit();
roc_main(ret.as_mut_ptr());
ret.assume_init()
};
// Verify that `inner` has all the expected traits.
{
let inner = outer.x;
assert!(inner == inner); // PartialEq
assert!(inner.clone() == inner.clone()); // Clone
// Since this is a move, later uses of `inner` will fail unless `inner` has Copy
let inner2 = inner; // Copy
assert!(inner2 != Default::default()); // Default
assert!(inner.partial_cmp(&inner) == Some(Ordering::Equal)); // PartialOrd
}
// Verify that `outer` has all the expected traits.
{
assert!(outer == outer); // PartialEq
assert!(outer.clone() == outer.clone()); // Clone
assert!(outer != Default::default()); // Default
assert!(outer.partial_cmp(&outer) == Some(Ordering::Equal)); // PartialOrd
}
println!("Record was: {:?}", outer);
// Exit code
0
}
// Externs required by roc_std and by the Roc app
use core::ffi::c_void;
use std::ffi::CStr;
use std::os::raw::c_char;
#[no_mangle]
pub unsafe extern "C" fn roc_alloc(size: usize, _alignment: u32) -> *mut c_void {
return libc::malloc(size);
}
#[no_mangle]
pub unsafe extern "C" fn roc_realloc(
c_ptr: *mut c_void,
new_size: usize,
_old_size: usize,
_alignment: u32,
) -> *mut c_void {
return libc::realloc(c_ptr, new_size);
}
#[no_mangle]
pub unsafe extern "C" fn roc_dealloc(c_ptr: *mut c_void, _alignment: u32) {
return libc::free(c_ptr);
}
#[no_mangle]
pub unsafe extern "C" fn roc_panic(c_ptr: *mut c_void, tag_id: u32) {
match tag_id {
0 => {
let slice = CStr::from_ptr(c_ptr as *const c_char);
let string = slice.to_str().unwrap();
eprintln!("Roc hit a panic: {}", string);
std::process::exit(1);
}
_ => todo!(),
}
}
#[no_mangle]
pub unsafe extern "C" fn roc_memcpy(dst: *mut c_void, src: *mut c_void, n: usize) -> *mut c_void {
libc::memcpy(dst, src, n)
}
#[no_mangle]
pub unsafe extern "C" fn roc_memset(dst: *mut c_void, c: i32, n: usize) -> *mut c_void {
libc::memset(dst, c, n)
}

View file

@ -0,0 +1,6 @@
app "app"
packages { pf: "platform.roc" }
imports []
provides [main] to pf
main = Cons "World!" (Cons "Hello " Nil)

View file

@ -0,0 +1,11 @@
platform "test-platform"
requires {} { main : _ }
exposes []
packages {}
imports []
provides [mainForHost]
StrConsList : [Nil, Cons Str StrConsList]
mainForHost : StrConsList
mainForHost = main

View file

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

View file

@ -0,0 +1,6 @@
app "app"
packages { pf: "platform.roc" }
imports []
provides [main] to pf
main = Foo "This is a test"

View file

@ -0,0 +1,17 @@
platform "test-platform"
requires {} { main : _ }
exposes []
packages {}
imports []
provides [mainForHost]
# This case is important to test because the U128
# gives the whole struct an alignment of 16, but the
# Str is the largest variant, so the whole union has
# a size of 32 (due to alignment, rounded up from Str's 24),
# and the discriminant is stored in the 8+ bytes of padding
# that all variants have.
NonRecursive : [Foo Str, Bar U128, Blah I32, Baz]
mainForHost : NonRecursive
mainForHost = main

View file

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

View file

@ -0,0 +1,6 @@
app "app"
packages { pf: "platform.roc" }
imports []
provides [main] to pf
main = Foo "This is a test"

View file

@ -0,0 +1,15 @@
platform "test-platform"
requires {} { main : _ }
exposes []
packages {}
imports []
provides [mainForHost]
# This case is important to test because there's no padding
# after the largest variant, so the compiler adds an extra u8
# (rounded up to alignment, so an an extra 8 bytes) in which
# to store the discriminant. We have to bindgen accordingly!
NonRecursive : [Foo Str, Bar I64, Blah I32, Baz]
mainForHost : NonRecursive
mainForHost = main

View file

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

View file

@ -0,0 +1,236 @@
#[macro_use]
extern crate pretty_assertions;
#[macro_use]
extern crate indoc;
mod helpers;
#[cfg(test)]
mod test_gen_rs {
use crate::helpers::generate_bindings;
#[test]
fn basic_record_aliased() {
let module = indoc!(
r#"
MyRcd : { a : U64, b : I128 }
main : MyRcd
main = { a: 1u64, b: 2i128 }
"#
);
assert_eq!(
generate_bindings(module)
.strip_prefix('\n')
.unwrap_or_default(),
indoc!(
r#"
#[cfg(any(
target_arch = "arm",
target_arch = "aarch64",
target_arch = "wasm32",
target_arch = "x86",
target_arch = "x86_64"
))]
#[derive(Clone, Copy, Debug, Default, Eq, Ord, Hash, PartialEq, PartialOrd)]
#[repr(C)]
pub struct MyRcd {
pub b: roc_std::I128,
pub a: u64,
}
"#
)
);
}
#[test]
fn nested_record_aliased() {
let module = indoc!(
r#"
Outer : { x : Inner, y : Str, z : List U8 }
Inner : { a : U16, b : F32 }
main : Outer
main = { x: { a: 5, b: 24 }, y: "foo", z: [1, 2] }
"#
);
assert_eq!(
generate_bindings(module)
.strip_prefix('\n')
.unwrap_or_default(),
indoc!(
r#"
#[cfg(any(
target_arch = "arm",
target_arch = "wasm32",
target_arch = "x86"
))]
#[derive(Clone, Debug, Default, PartialEq, PartialOrd)]
#[repr(C)]
pub struct Outer {
pub x: Inner,
pub y: roc_std::RocStr,
pub z: roc_std::RocList<u8>,
}
#[cfg(any(
target_arch = "arm",
target_arch = "aarch64",
target_arch = "wasm32",
target_arch = "x86",
target_arch = "x86_64"
))]
#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)]
#[repr(C)]
pub struct Inner {
pub b: f32,
pub a: u16,
}
#[cfg(any(
target_arch = "aarch64",
target_arch = "x86_64"
))]
#[derive(Clone, Debug, Default, PartialEq, PartialOrd)]
#[repr(C)]
pub struct Outer {
pub y: roc_std::RocStr,
pub z: roc_std::RocList<u8>,
pub x: Inner,
}
"#
)
);
}
#[test]
fn record_anonymous() {
let module = "main = { a: 1u64, b: 2u128 }";
assert_eq!(
generate_bindings(module)
.strip_prefix('\n')
.unwrap_or_default(),
indoc!(
r#"
#[cfg(any(
target_arch = "arm",
target_arch = "aarch64",
target_arch = "wasm32",
target_arch = "x86",
target_arch = "x86_64"
))]
#[derive(Clone, Copy, Debug, Default, Eq, Ord, Hash, PartialEq, PartialOrd)]
#[repr(C)]
pub struct R1 {
pub b: roc_std::U128,
pub a: u64,
}
"#
)
);
}
#[test]
fn nested_record_anonymous() {
let module = r#"main = { x: { a: 5u16, b: 24f32 }, y: "foo", z: [1u8, 2] }"#;
assert_eq!(
generate_bindings(module)
.strip_prefix('\n')
.unwrap_or_default(),
indoc!(
r#"
#[cfg(any(
target_arch = "arm",
target_arch = "wasm32",
target_arch = "x86"
))]
#[derive(Clone, Debug, Default, PartialEq, PartialOrd)]
#[repr(C)]
pub struct R1 {
pub x: R2,
pub y: roc_std::RocStr,
pub z: roc_std::RocList<u8>,
}
#[cfg(any(
target_arch = "arm",
target_arch = "aarch64",
target_arch = "wasm32",
target_arch = "x86",
target_arch = "x86_64"
))]
#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)]
#[repr(C)]
pub struct R2 {
pub b: f32,
pub a: u16,
}
#[cfg(any(
target_arch = "aarch64",
target_arch = "x86_64"
))]
#[derive(Clone, Debug, Default, PartialEq, PartialOrd)]
#[repr(C)]
pub struct R1 {
pub y: roc_std::RocStr,
pub z: roc_std::RocList<u8>,
pub x: R2,
}
"#
)
);
}
#[test]
fn tag_union_enumeration() {
let module = indoc!(
r#"
Enumeration : [Blah, Foo, Bar,]
main : Enumeration
main = Foo
"#
);
assert_eq!(
generate_bindings(module)
.strip_prefix('\n')
.unwrap_or_default(),
indoc!(
r#"
#[cfg(any(
target_arch = "arm",
target_arch = "aarch64",
target_arch = "wasm32",
target_arch = "x86",
target_arch = "x86_64"
))]
#[derive(Clone, Copy, Eq, Ord, Hash, PartialEq, PartialOrd)]
#[repr(u8)]
pub enum Enumeration {
Bar = 0,
Blah = 1,
Foo = 2,
}
impl core::fmt::Debug for Enumeration {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Bar => f.write_str("Enumeration::Bar"),
Self::Blah => f.write_str("Enumeration::Blah"),
Self::Foo => f.write_str("Enumeration::Foo"),
}
}
}
"#
)
);
}
}

View file

@ -0,0 +1,77 @@
use roc_bindgen::bindgen_rs;
use roc_bindgen::load::load_types;
use roc_load::Threading;
use std::env;
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;
#[allow(dead_code)]
pub fn generate_bindings(decl_src: &str) -> String {
use tempfile::tempdir;
let mut src = indoc!(
r#"
platform "main"
requires {} { nothing : {} }
exposes []
packages {}
imports []
provides [main]
"#
)
.to_string();
src.push_str(decl_src);
let pairs = {
let dir = tempdir().expect("Unable to create tempdir");
let filename = PathBuf::from("platform.roc");
let file_path = dir.path().join(filename);
let full_file_path = file_path.clone();
let mut file = File::create(file_path).unwrap();
writeln!(file, "{}", &src).unwrap();
let result = load_types(full_file_path, dir.path(), Threading::Single);
dir.close().expect("Unable to close tempdir");
result.expect("had problems loading")
};
bindgen_rs::emit(&pairs)
}
#[allow(dead_code)]
pub fn fixtures_dir(dir_name: &str) -> PathBuf {
let mut path = root_dir();
// Descend into cli/tests/fixtures/{dir_name}
path.push("crates");
path.push("bindgen");
path.push("tests");
path.push("fixtures");
path.push(dir_name);
path
}
#[allow(dead_code)]
pub fn root_dir() -> PathBuf {
let mut path = env::current_exe().ok().unwrap();
// Get rid of the filename in target/debug/deps/cli_run-99c65e4e9a1fbd06
path.pop();
// If we're in deps/ get rid of deps/ in target/debug/deps/
if path.ends_with("deps") {
path.pop();
}
// Get rid of target/debug/ so we're back at the project root
path.pop();
path.pop();
path
}

View file

@ -0,0 +1,243 @@
#[macro_use]
extern crate pretty_assertions;
#[macro_use]
extern crate indoc;
extern crate dircpy;
extern crate roc_collections;
mod helpers;
#[cfg(test)]
mod bindgen_cli_run {
use crate::helpers::{fixtures_dir, root_dir};
use cli_utils::helpers::{run_bindgen, run_roc, Out};
use std::fs;
use std::path::Path;
use std::process::Command;
// All of these tests rely on `target/` for the `cli` crate being up-to-date,
// so do a `cargo build` on it first!
#[ctor::ctor]
fn init() {
let args = if cfg!(debug_assertions) {
vec!["build"]
} else {
vec!["build", "--release"]
};
println!(
"Running `cargo {}` on the `cli` crate before running the tests. This may take a bit!",
args.join(" ")
);
let output = Command::new("cargo")
.args(args)
.current_dir(root_dir().join("crates").join("cli"))
.output()
.unwrap_or_else(|err| {
panic!(
"Failed to `cargo build` roc CLI for bindgen CLI tests - error was: {:?}",
err
)
});
assert!(output.status.success());
}
/// This macro does two things.
///
/// First, it generates and runs a separate test for each of the given
/// expected stdout endings. Each of these should test a particular .roc file
/// in the fixtures/ directory. The fixtures themselves run assertions too, but
/// the stdout check verifies that we're actually running the code we think we are;
/// without it, it would be possible that the fixtures are just exiting without running
/// any assertions, and we would have no way to find out!
///
/// Second, this generates an extra test which (non-recursively) traverses the
/// fixtures/ directory and verifies that each of the .roc files in there
/// has had a corresponding test generated in the previous step. This test
/// will fail if we ever add a new .roc file to fixtures/ and forget to
/// add a test for it here!
macro_rules! fixtures {
($($test_name:ident:$fixture_dir:expr => $ends_with:expr,)+) => {
$(
#[test]
#[allow(non_snake_case)]
fn $test_name() {
let dir = fixtures_dir($fixture_dir);
generate_bindings_for(&dir, std::iter::empty());
let out = run_app(&dir.join("app.roc"), std::iter::empty());
assert!(out.status.success());
assert_eq!(out.stderr, "");
assert!(
out.stdout.ends_with($ends_with),
"Unexpected stdout ending - expected {:?} but stdout was: {:?}",
$ends_with,
out.stdout
);
}
)*
#[test]
fn all_fixtures_have_tests() {
use roc_collections::VecSet;
let mut all_fixtures: VecSet<String> = VecSet::default();
$(
all_fixtures.insert($fixture_dir.to_string());
)*
check_for_tests(&mut all_fixtures);
}
}
}
fixtures! {
basic_record:"basic-record" => "Record was: MyRcd { b: 42, a: 1995 }\n",
nested_record:"nested-record" => "Record was: Outer { y: \"foo\", z: [1, 2], x: Inner { b: 24.0, a: 5 } }\n",
enumeration:"enumeration" => "tag_union was: MyEnum::Foo, Bar is: MyEnum::Bar, Baz is: MyEnum::Baz\n",
union_with_padding:"union-with-padding" => indoc!(r#"
tag_union was: NonRecursive::Foo("This is a test")
`Foo "small str"` is: NonRecursive::Foo("small str")
`Foo "A long enough string to not be small"` is: NonRecursive::Foo("A long enough string to not be small")
`Bar 123` is: NonRecursive::Bar(123)
`Baz` is: NonRecursive::Baz
`Blah 456` is: NonRecursive::Blah(456)
"#),
union_without_padding:"union-without-padding" => indoc!(r#"
tag_union was: NonRecursive::Foo("This is a test")
`Foo "small str"` is: NonRecursive::Foo("small str")
`Bar 123` is: NonRecursive::Bar(123)
`Baz` is: NonRecursive::Baz
`Blah 456` is: NonRecursive::Blah(456)
"#),
nullable_unwrapped:"nullable-unwrapped" => indoc!(r#"
tag_union was: StrConsList::Cons("World!", StrConsList::Cons("Hello ", StrConsList::Nil))
`Cons "small str" Nil` is: StrConsList::Cons("small str", StrConsList::Nil)
`Nil` is: StrConsList::Nil
"#),
basic_recursive_union:"basic-recursive-union" => indoc!(r#"
tag_union was: Expr::Concat(Expr::String("Hello, "), Expr::String("World!"))
`Concat (String "Hello, ") (String "World!")` is: Expr::Concat(Expr::String("Hello, "), Expr::String("World!"))
`String "this is a test"` is: Expr::String("this is a test")
"#),
advanced_recursive_union:"advanced-recursive-union" => indoc!(r#"
rbt was: Rbt { default: Job::Job(R1 { command: Command::Command(R2 { tool: Tool::SystemTool(R4 { name: "test", num: 42 }) }), inputFiles: ["foo"] }) }
"#),
list_recursive_union:"list-recursive-union" => indoc!(r#"
rbt was: Rbt { default: Job::Job(R1 { command: Command::Command(R2 { args: [], tool: Tool::SystemTool(R3 { name: "test" }) }), inputFiles: ["foo"], job: [] }) }
"#),
}
fn check_for_tests(all_fixtures: &mut roc_collections::VecSet<String>) {
use roc_collections::VecSet;
// todo!("Remove a bunch of duplication - don't have a ton of files in there.");
let fixtures = fixtures_dir("");
let entries = std::fs::read_dir(fixtures.as_path()).unwrap_or_else(|err| {
panic!(
"Error trying to read {} as a fixtures directory: {}",
fixtures.to_string_lossy(),
err
);
});
for entry in entries {
let entry = entry.unwrap();
if entry.file_type().unwrap().is_dir() {
let fixture_dir_name = entry.file_name().into_string().unwrap();
if !all_fixtures.remove(&fixture_dir_name) {
panic!(
"The bindgen fixture directory {} does not have any corresponding tests in cli_run. Please add one, so if it ever stops working, we'll know about it right away!",
entry.path().to_string_lossy()
);
}
}
}
assert_eq!(all_fixtures, &mut VecSet::default());
}
fn generate_bindings_for<'a, I: IntoIterator<Item = &'a str>>(
platform_dir: &'a Path,
args: I,
) -> Out {
let platform_module_path = platform_dir.join("platform.roc");
let bindings_file = platform_dir.join("src").join("bindings.rs");
let fixture_templates_dir = platform_dir
.parent()
.unwrap()
.parent()
.unwrap()
.join("fixture-templates");
dbg!(&fixture_templates_dir);
// Copy the rust template from the templates directory into the fixture dir.
dircpy::CopyBuilder::new(fixture_templates_dir.join("rust"), platform_dir)
.overwrite(true) // overwrite any files that were already present
.run()
.unwrap();
// Delete the bindings file to make sure we're actually regenerating it!
if bindings_file.exists() {
fs::remove_file(&bindings_file)
.expect("Unable to remove bindings.rs in order to regenerate it in the test");
}
// Generate a fresh bindings.rs for this platform
let bindgen_out = run_bindgen(
// converting these all to String avoids lifetime issues
args.into_iter().map(|arg| arg.to_string()).chain([
platform_module_path.to_str().unwrap().to_string(),
bindings_file.to_str().unwrap().to_string(),
]),
);
// If there is any stderr, it should be reporting the runtime and that's it!
if !(bindgen_out.stderr.is_empty()
|| bindgen_out.stderr.starts_with("runtime: ") && bindgen_out.stderr.ends_with("ms\n"))
{
panic!(
"`roc-bindgen` command had unexpected stderr: {}",
bindgen_out.stderr
);
}
assert!(bindgen_out.status.success(), "bad status {:?}", bindgen_out);
bindgen_out
}
fn run_app<'a, I: IntoIterator<Item = &'a str>>(app_file: &'a Path, args: I) -> Out {
// Generate bindings.rs for this platform
let compile_out = run_roc(
// converting these all to String avoids lifetime issues
args.into_iter()
.map(|arg| arg.to_string())
.chain([app_file.to_str().unwrap().to_string()]),
&[],
);
// If there is any stderr, it should be reporting the runtime and that's it!
if !(compile_out.stderr.is_empty()
|| compile_out.stderr.starts_with("runtime: ") && compile_out.stderr.ends_with("ms\n"))
{
panic!(
"`roc` command had unexpected stderr: {}",
compile_out.stderr
);
}
assert!(compile_out.status.success(), "bad status {:?}", compile_out);
compile_out
}
}

99
crates/cli/Cargo.toml Normal file
View file

@ -0,0 +1,99 @@
[package]
name = "roc_cli"
version = "0.1.0"
authors = ["The Roc Contributors"]
license = "UPL-1.0"
repository = "https://github.com/rtfeldman/roc"
edition = "2021"
description = "A CLI for Roc"
default-run = "roc"
[[bin]]
name = "roc"
path = "src/main.rs"
test = false
bench = false
[features]
default = ["target-aarch64", "target-x86_64", "target-wasm32", "editor"]
wasm32-cli-run = ["target-wasm32", "run-wasm32"]
i386-cli-run = ["target-x86"]
editor = ["roc_editor"]
run-wasm32 = ["wasmer", "wasmer-wasi"]
# Compiling for a different platform than the host can cause linker errors.
target-arm = ["roc_build/target-arm", "roc_repl_cli/target-arm"]
target-aarch64 = ["roc_build/target-aarch64", "roc_repl_cli/target-aarch64"]
target-x86 = ["roc_build/target-x86", "roc_repl_cli/target-x86"]
target-x86_64 = ["roc_build/target-x86_64", "roc_repl_cli/target-x86_64"]
target-wasm32 = ["roc_build/target-wasm32", "roc_repl_cli/target-wasm32"]
target-all = [
"target-aarch64",
"target-arm",
"target-x86",
"target-x86_64",
"target-wasm32"
]
[dependencies]
roc_collections = { path = "../compiler/collections" }
roc_can = { path = "../compiler/can" }
roc_docs = { path = "../docs" }
roc_parse = { path = "../compiler/parse" }
roc_region = { path = "../compiler/region" }
roc_module = { path = "../compiler/module" }
roc_builtins = { path = "../compiler/builtins" }
roc_mono = { path = "../compiler/mono" }
roc_load = { path = "../compiler/load" }
roc_build = { path = "../compiler/build" }
roc_fmt = { path = "../compiler/fmt" }
roc_target = { path = "../compiler/roc_target" }
roc_reporting = { path = "../reporting" }
roc_error_macros = { path = "../error_macros" }
roc_editor = { path = "../editor", optional = true }
roc_linker = { path = "../linker" }
roc_repl_cli = { path = "../repl_cli", optional = true }
clap = { version = "3.1.15", default-features = false, features = ["std", "color", "suggestions"] }
const_format = { version = "0.2.23", features = ["const_generics"] }
bumpalo = { version = "3.8.0", features = ["collections"] }
mimalloc = { version = "0.1.26", default-features = false }
libc = "0.2.106"
errno = "0.2.8"
target-lexicon = "0.12.3"
tempfile = "3.2.0"
wasmer-wasi = { version = "2.2.1", optional = true }
# Wasmer singlepass compiler only works on x86_64.
[target.'cfg(target_arch = "x86_64")'.dependencies]
wasmer = { version = "2.2.1", optional = true, default-features = false, features = ["singlepass", "universal"] }
[target.'cfg(not(target_arch = "x86_64"))'.dependencies]
wasmer = { version = "2.2.1", optional = true, default-features = false, features = ["cranelift", "universal"] }
[dev-dependencies]
wasmer-wasi = "2.2.1"
pretty_assertions = "1.0.0"
roc_test_utils = { path = "../test_utils" }
indoc = "1.0.3"
serial_test = "0.8.0"
criterion = { git = "https://github.com/Anton-4/criterion.rs"}
cli_utils = { path = "../cli_utils" }
strum = "0.24.0"
strum_macros = "0.24"
# Wasmer singlepass compiler only works on x86_64.
[target.'cfg(target_arch = "x86_64")'.dev-dependencies]
wasmer = { version = "2.2.1", default-features = false, features = ["singlepass", "universal"] }
[target.'cfg(not(target_arch = "x86_64"))'.dev-dependencies]
wasmer = { version = "2.2.1", default-features = false, features = ["cranelift", "universal"] }
[[bench]]
name = "time_bench"
harness = false

View file

@ -0,0 +1,17 @@
# Running the benchmarks
Install cargo criterion:
```
cargo install cargo-criterion
```
To prevent stack overflow on the `CFold` benchmark:
```
ulimit -s unlimited
```
In the `cli` folder execute:
```
cargo criterion
```

View file

@ -0,0 +1,71 @@
use std::time::Duration;
use cli_utils::bench_utils::{
bench_cfold, bench_deriv, bench_nqueens, bench_quicksort, bench_rbtree_ck,
};
use criterion::{measurement::WallTime, BenchmarkGroup, Criterion, SamplingMode};
fn bench_group_wall_time(c: &mut Criterion) {
let mut group = c.benchmark_group("bench-group_wall-time");
// calculate statistics based on a fixed(flat) x runs
group.sampling_mode(SamplingMode::Flat);
let default_nr_of_runs = 200;
let nr_of_runs = match std::env::var("BENCH_DRY_RUN") {
Ok(val) => {
if val == "1" {
10 // minimum value allowed by criterion
} else {
default_nr_of_runs
}
}
Err(_) => default_nr_of_runs,
};
group.sample_size(nr_of_runs);
let bench_funcs: Vec<fn(Option<&mut BenchmarkGroup<WallTime>>)> = vec![
bench_nqueens, // queens 11
bench_cfold, // e = mkExpr 17 1
bench_deriv, // nest deriv 8 f
bench_rbtree_ck, // ms = makeMap 5 80000
// bench_rbtree_delete, // m = makeMap 100000
bench_quicksort, // list size 10000
];
for bench_func in bench_funcs.iter() {
bench_func(Some(&mut group))
}
group.finish();
}
// use short warm up and measurement time on dry run
fn make_config() -> Criterion {
let default_config = Criterion::default();
match std::env::var("BENCH_DRY_RUN") {
Ok(val) => {
if val == "1" {
default_config
.warm_up_time(Duration::new(1, 0))
.measurement_time(Duration::new(1, 0))
} else {
default_config
}
}
Err(_) => default_config,
}
}
fn all_benches() {
let mut criterion: Criterion<_> = make_config().configure_from_args();
bench_group_wall_time(&mut criterion);
}
fn main() {
all_benches();
Criterion::default().configure_from_args().final_summary();
}

490
crates/cli/src/build.rs Normal file
View file

@ -0,0 +1,490 @@
use bumpalo::Bump;
use roc_build::{
link::{link, preprocess_host_wasm32, rebuild_host, LinkType, LinkingStrategy},
program::{self, Problems},
};
use roc_builtins::bitcode;
use roc_load::{LoadingProblem, Threading};
use roc_mono::ir::OptLevel;
use roc_reporting::report::RenderTarget;
use roc_target::TargetInfo;
use std::time::{Duration, SystemTime};
use std::{path::PathBuf, thread::JoinHandle};
use target_lexicon::Triple;
use tempfile::Builder;
fn report_timing(buf: &mut String, label: &str, duration: Duration) {
buf.push_str(&format!(
" {:9.3} ms {}\n",
duration.as_secs_f64() * 1000.0,
label,
));
}
pub struct BuiltFile {
pub binary_path: PathBuf,
pub problems: Problems,
pub total_time: Duration,
}
#[allow(clippy::too_many_arguments)]
pub fn build_file<'a>(
arena: &'a Bump,
target: &Triple,
src_dir: PathBuf,
app_module_path: PathBuf,
opt_level: OptLevel,
emit_debug_info: bool,
emit_timings: bool,
link_type: LinkType,
linking_strategy: LinkingStrategy,
precompiled: bool,
target_valgrind: bool,
threading: Threading,
) -> Result<BuiltFile, LoadingProblem<'a>> {
let compilation_start = SystemTime::now();
let target_info = TargetInfo::from(target);
// Step 1: compile the app and generate the .o file
let subs_by_module = Default::default();
let loaded = roc_load::load_and_monomorphize(
arena,
app_module_path.clone(),
src_dir.as_path(),
subs_by_module,
target_info,
// TODO: expose this from CLI?
RenderTarget::ColorTerminal,
threading,
)?;
use target_lexicon::Architecture;
let emit_wasm = matches!(target.architecture, Architecture::Wasm32);
// TODO wasm host extension should be something else ideally
// .bc does not seem to work because
//
// > Non-Emscripten WebAssembly hasn't implemented __builtin_return_address
//
// and zig does not currently emit `.a` webassembly static libraries
let host_extension = if emit_wasm {
if matches!(opt_level, OptLevel::Development) {
"wasm"
} else {
"zig"
}
} else {
"o"
};
let app_extension = if emit_wasm {
if matches!(opt_level, OptLevel::Development) {
"wasm"
} else {
"bc"
}
} else {
"o"
};
let cwd = app_module_path.parent().unwrap();
let mut binary_path = cwd.join(&*loaded.output_path); // TODO should join ".exe" on Windows
if emit_wasm {
binary_path.set_extension("wasm");
}
let host_input_path = cwd
.join(&*loaded.platform_path)
.with_file_name("host")
.with_extension(host_extension);
// TODO this should probably be moved before load_and_monomorphize.
// To do this we will need to preprocess files just for their exported symbols.
// Also, we should no longer need to do this once we have platforms on
// a package repository, as we can then get precompiled hosts from there.
let exposed_values = loaded
.exposed_to_host
.values
.keys()
.map(|x| x.as_str(&loaded.interns).to_string())
.collect();
let exposed_closure_types = loaded
.exposed_to_host
.closure_types
.iter()
.map(|x| x.as_str(&loaded.interns).to_string())
.collect();
let preprocessed_host_path = if emit_wasm {
host_input_path.with_file_name("preprocessedhost.o")
} else {
host_input_path.with_file_name("preprocessedhost")
};
let rebuild_thread = spawn_rebuild_thread(
opt_level,
linking_strategy,
precompiled,
host_input_path.clone(),
preprocessed_host_path.clone(),
binary_path.clone(),
target,
exposed_values,
exposed_closure_types,
target_valgrind,
);
// TODO try to move as much of this linking as possible to the precompiled
// host, to minimize the amount of host-application linking required.
let app_o_file = Builder::new()
.prefix("roc_app")
.suffix(&format!(".{}", app_extension))
.tempfile()
.map_err(|err| {
todo!("TODO Gracefully handle tempfile creation error {:?}", err);
})?;
let app_o_file = app_o_file.path();
let buf = &mut String::with_capacity(1024);
let mut it = loaded.timings.iter().peekable();
while let Some((module_id, module_timing)) = it.next() {
let module_name = loaded.interns.module_name(*module_id);
buf.push_str(" ");
if module_name.is_empty() {
// the App module
buf.push_str("Application Module");
} else {
buf.push_str(module_name);
}
buf.push('\n');
report_timing(buf, "Read .roc file from disk", module_timing.read_roc_file);
report_timing(buf, "Parse header", module_timing.parse_header);
report_timing(buf, "Parse body", module_timing.parse_body);
report_timing(buf, "Canonicalize", module_timing.canonicalize);
report_timing(buf, "Constrain", module_timing.constrain);
report_timing(buf, "Solve", module_timing.solve);
report_timing(
buf,
"Find Specializations",
module_timing.find_specializations,
);
let multiple_make_specializations_passes = module_timing.make_specializations.len() > 1;
for (i, pass_time) in module_timing.make_specializations.iter().enumerate() {
let suffix = if multiple_make_specializations_passes {
format!(" (Pass {})", i)
} else {
String::new()
};
report_timing(buf, &format!("Make Specializations{}", suffix), *pass_time);
}
report_timing(buf, "Other", module_timing.other());
buf.push('\n');
report_timing(buf, "Total", module_timing.total());
if it.peek().is_some() {
buf.push('\n');
}
}
// This only needs to be mutable for report_problems. This can't be done
// inside a nested scope without causing a borrow error!
let mut loaded = loaded;
let problems = program::report_problems_monomorphized(&mut loaded);
let loaded = loaded;
enum HostRebuildTiming {
BeforeApp(u128),
ConcurrentWithApp(JoinHandle<u128>),
}
let rebuild_timing = if linking_strategy == LinkingStrategy::Additive {
let rebuild_duration = rebuild_thread.join().unwrap();
if emit_timings && !precompiled {
println!(
"Finished rebuilding and preprocessing the host in {} ms\n",
rebuild_duration
);
}
HostRebuildTiming::BeforeApp(rebuild_duration)
} else {
HostRebuildTiming::ConcurrentWithApp(rebuild_thread)
};
let code_gen_timing = program::gen_from_mono_module(
arena,
loaded,
&app_module_path,
target,
app_o_file,
opt_level,
emit_debug_info,
&preprocessed_host_path,
);
buf.push('\n');
buf.push_str(" ");
buf.push_str("Code Generation");
buf.push('\n');
report_timing(
buf,
"Generate Assembly from Mono IR",
code_gen_timing.code_gen,
);
report_timing(buf, "Emit .o file", code_gen_timing.emit_o_file);
let compilation_end = compilation_start.elapsed().unwrap();
let size = std::fs::metadata(&app_o_file)
.unwrap_or_else(|err| {
panic!(
"Could not open {:?} - which was supposed to have been generated. Error: {:?}",
app_o_file, err
);
})
.len();
if emit_timings {
println!(
"\n\nCompilation finished!\n\nHere's how long each module took to compile:\n\n{}",
buf
);
println!(
"Finished compilation and code gen in {} ms\n\nProduced a app.o file of size {:?}\n",
compilation_end.as_millis(),
size,
);
}
if let HostRebuildTiming::ConcurrentWithApp(thread) = rebuild_timing {
let rebuild_duration = thread.join().unwrap();
if emit_timings && !precompiled {
println!(
"Finished rebuilding and preprocessing the host in {} ms\n",
rebuild_duration
);
}
}
// Step 2: link the precompiled host and compiled app
let link_start = SystemTime::now();
let problems = match (linking_strategy, link_type) {
(LinkingStrategy::Surgical, _) => {
roc_linker::link_preprocessed_host(target, &host_input_path, app_o_file, &binary_path)
.map_err(|err| {
todo!(
"gracefully handle failing to surgically link with error: {:?}",
err
);
})?;
problems
}
(LinkingStrategy::Additive, _) | (LinkingStrategy::Legacy, LinkType::None) => {
// Just copy the object file to the output folder.
binary_path.set_extension(app_extension);
std::fs::copy(app_o_file, &binary_path).unwrap();
problems
}
(LinkingStrategy::Legacy, _) => {
let mut inputs = vec![
host_input_path.as_path().to_str().unwrap(),
app_o_file.to_str().unwrap(),
];
if matches!(opt_level, OptLevel::Development) {
inputs.push(bitcode::BUILTINS_HOST_OBJ_PATH);
}
let (mut child, _) = // TODO use lld
link(
target,
binary_path.clone(),
&inputs,
link_type
)
.map_err(|_| {
todo!("gracefully handle `ld` failing to spawn.");
})?;
let exit_status = child.wait().map_err(|_| {
todo!("gracefully handle error after `ld` spawned");
})?;
if exit_status.success() {
problems
} else {
let mut problems = problems;
// Add an error for `ld` failing
problems.errors += 1;
problems
}
}
};
let linking_time = link_start.elapsed().unwrap();
if emit_timings {
println!("Finished linking in {} ms\n", linking_time.as_millis());
}
let total_time = compilation_start.elapsed().unwrap();
Ok(BuiltFile {
binary_path,
problems,
total_time,
})
}
#[allow(clippy::too_many_arguments)]
fn spawn_rebuild_thread(
opt_level: OptLevel,
linking_strategy: LinkingStrategy,
precompiled: bool,
host_input_path: PathBuf,
preprocessed_host_path: PathBuf,
binary_path: PathBuf,
target: &Triple,
exported_symbols: Vec<String>,
exported_closure_types: Vec<String>,
target_valgrind: bool,
) -> std::thread::JoinHandle<u128> {
let thread_local_target = target.clone();
std::thread::spawn(move || {
if !precompiled {
println!("🔨 Rebuilding host...");
}
let rebuild_host_start = SystemTime::now();
if !precompiled {
match linking_strategy {
LinkingStrategy::Additive => {
let host_dest = rebuild_host(
opt_level,
&thread_local_target,
host_input_path.as_path(),
None,
target_valgrind,
);
preprocess_host_wasm32(host_dest.as_path(), &preprocessed_host_path);
}
LinkingStrategy::Surgical => {
roc_linker::build_and_preprocess_host(
opt_level,
&thread_local_target,
host_input_path.as_path(),
preprocessed_host_path.as_path(),
exported_symbols,
exported_closure_types,
target_valgrind,
)
.unwrap();
}
LinkingStrategy::Legacy => {
rebuild_host(
opt_level,
&thread_local_target,
host_input_path.as_path(),
None,
target_valgrind,
);
}
}
}
if linking_strategy == LinkingStrategy::Surgical {
// Copy preprocessed host to executable location.
std::fs::copy(preprocessed_host_path, binary_path.as_path()).unwrap();
}
let rebuild_host_end = rebuild_host_start.elapsed().unwrap();
rebuild_host_end.as_millis()
})
}
#[allow(clippy::too_many_arguments)]
pub fn check_file(
arena: &Bump,
src_dir: PathBuf,
roc_file_path: PathBuf,
emit_timings: bool,
threading: Threading,
) -> Result<(program::Problems, Duration), LoadingProblem> {
let compilation_start = SystemTime::now();
// only used for generating errors. We don't do code generation, so hardcoding should be fine
// we need monomorphization for when exhaustiveness checking
let target_info = TargetInfo::default_x86_64();
// Step 1: compile the app and generate the .o file
let subs_by_module = Default::default();
let mut loaded = roc_load::load_and_typecheck(
arena,
roc_file_path,
src_dir.as_path(),
subs_by_module,
target_info,
// TODO: expose this from CLI?
RenderTarget::ColorTerminal,
threading,
)?;
let buf = &mut String::with_capacity(1024);
let mut it = loaded.timings.iter().peekable();
while let Some((module_id, module_timing)) = it.next() {
let module_name = loaded.interns.module_name(*module_id);
buf.push_str(" ");
if module_name.is_empty() {
// the App module
buf.push_str("Application Module");
} else {
buf.push_str(module_name);
}
buf.push('\n');
report_timing(buf, "Read .roc file from disk", module_timing.read_roc_file);
report_timing(buf, "Parse header", module_timing.parse_header);
report_timing(buf, "Parse body", module_timing.parse_body);
report_timing(buf, "Canonicalize", module_timing.canonicalize);
report_timing(buf, "Constrain", module_timing.constrain);
report_timing(buf, "Solve", module_timing.solve);
report_timing(buf, "Other", module_timing.other());
buf.push('\n');
report_timing(buf, "Total", module_timing.total());
if it.peek().is_some() {
buf.push('\n');
}
}
let compilation_end = compilation_start.elapsed().unwrap();
if emit_timings {
println!(
"\n\nCompilation finished!\n\nHere's how long each module took to compile:\n\n{}",
buf
);
println!("Finished checking in {} ms\n", compilation_end.as_millis(),);
}
Ok((
program::report_problems_typechecked(&mut loaded),
compilation_end,
))
}

172
crates/cli/src/format.rs Normal file
View file

@ -0,0 +1,172 @@
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use crate::FormatMode;
use bumpalo::Bump;
use roc_error_macros::{internal_error, user_error};
use roc_fmt::def::fmt_defs;
use roc_fmt::module::fmt_module;
use roc_fmt::spaces::RemoveSpaces;
use roc_fmt::{Ast, Buf};
use roc_parse::{
module::{self, module_defs},
parser::{Parser, SyntaxError},
state::State,
};
fn flatten_directories(files: std::vec::Vec<PathBuf>) -> std::vec::Vec<PathBuf> {
let mut to_flatten = files;
let mut files = vec![];
while let Some(path) = to_flatten.pop() {
if path.is_dir() {
match path.read_dir() {
Ok(directory) => {
for item in directory {
match item {
Ok(file) => {
let file_path = file.path();
if file_path.is_dir() {
to_flatten.push(file_path);
} else if is_roc_file(&file_path) {
files.push(file_path);
}
}
Err(error) => internal_error!(
"There was an error while trying to read a file from a directory: {:?}",
error
),
}
}
}
Err(error) => internal_error!(
"There was an error while trying to read the contents of a directory: {:?}",
error
),
}
} else if is_roc_file(&path) {
files.push(path);
}
}
files
}
fn is_roc_file(path: &Path) -> bool {
let ext = path.extension().and_then(OsStr::to_str);
return matches!(ext, Some("roc"));
}
pub fn format(files: std::vec::Vec<PathBuf>, mode: FormatMode) -> Result<(), String> {
let files = flatten_directories(files);
for file in files {
let arena = Bump::new();
let src = std::fs::read_to_string(&file).unwrap();
let ast = arena.alloc(parse_all(&arena, &src).unwrap_or_else(|e| {
user_error!("Unexpected parse failure when parsing this formatting:\n\n{:?}\n\nParse error was:\n\n{:?}\n\n", src, e)
}));
let mut buf = Buf::new_in(&arena);
fmt_all(&mut buf, ast);
let reparsed_ast = arena.alloc(parse_all(&arena, buf.as_str()).unwrap_or_else(|e| {
let mut fail_file = file.clone();
fail_file.set_extension("roc-format-failed");
std::fs::write(&fail_file, buf.as_str()).unwrap();
internal_error!(
"Formatting bug; formatted code isn't valid\n\n\
I wrote the incorrect result to this file for debugging purposes:\n{}\n\n\
Parse error was: {:?}\n\n",
fail_file.display(),
e
);
}));
let ast_normalized = ast.remove_spaces(&arena);
let reparsed_ast_normalized = reparsed_ast.remove_spaces(&arena);
// HACK!
// We compare the debug format strings of the ASTs, because I'm finding in practice that _somewhere_ deep inside the ast,
// the PartialEq implementation is returning `false` even when the Debug-formatted impl is exactly the same.
// I don't have the patience to debug this right now, so let's leave it for another day...
// TODO: fix PartialEq impl on ast types
if format!("{:?}", ast_normalized) != format!("{:?}", reparsed_ast_normalized) {
let mut fail_file = file.clone();
fail_file.set_extension("roc-format-failed");
std::fs::write(&fail_file, buf.as_str()).unwrap();
let mut before_file = file.clone();
before_file.set_extension("roc-format-failed-ast-before");
std::fs::write(&before_file, &format!("{:#?}\n", ast)).unwrap();
let mut after_file = file.clone();
after_file.set_extension("roc-format-failed-ast-after");
std::fs::write(&after_file, &format!("{:#?}\n", reparsed_ast)).unwrap();
internal_error!(
"Formatting bug; formatting didn't reparse as the same tree\n\n\
I wrote the incorrect result to this file for debugging purposes:\n{}\n\n\
I wrote the tree before and after formatting to these files for debugging purposes:\n{}\n{}\n\n",
fail_file.display(),
before_file.display(),
after_file.display());
}
// Now verify that the resultant formatting is _stable_ - i.e. that it doesn't change again if re-formatted
let mut reformatted_buf = Buf::new_in(&arena);
fmt_all(&mut reformatted_buf, reparsed_ast);
if buf.as_str() != reformatted_buf.as_str() {
let mut unstable_1_file = file.clone();
unstable_1_file.set_extension("roc-format-unstable-1");
std::fs::write(&unstable_1_file, buf.as_str()).unwrap();
let mut unstable_2_file = file.clone();
unstable_2_file.set_extension("roc-format-unstable-2");
std::fs::write(&unstable_2_file, reformatted_buf.as_str()).unwrap();
internal_error!(
"Formatting bug; formatting is not stable. Reformatting the formatted file changed it again.\n\n\
I wrote the result of formatting to this file for debugging purposes:\n{}\n\n\
I wrote the result of double-formatting here:\n{}\n\n",
unstable_1_file.display(),
unstable_2_file.display());
}
match mode {
FormatMode::CheckOnly => {
// If we notice that this file needs to be formatted, return early
if buf.as_str() != src {
return Err("One or more files need to be reformatted.".to_string());
}
}
FormatMode::Format => {
// If all the checks above passed, actually write out the new file.
std::fs::write(&file, buf.as_str()).unwrap();
}
}
}
Ok(())
}
fn parse_all<'a>(arena: &'a Bump, src: &'a str) -> Result<Ast<'a>, SyntaxError<'a>> {
let (module, state) = module::parse_header(arena, State::new(src.as_bytes()))
.map_err(|e| SyntaxError::Header(e.problem))?;
let (_, defs, _) = module_defs().parse(arena, state).map_err(|(_, e, _)| e)?;
Ok(Ast { module, defs })
}
fn fmt_all<'a>(buf: &mut Buf<'a>, ast: &'a Ast) {
fmt_module(buf, &ast.module);
fmt_defs(buf, &ast.defs, 0);
buf.fmt_end_of_file();
}

861
crates/cli/src/lib.rs Normal file
View file

@ -0,0 +1,861 @@
#[macro_use]
extern crate const_format;
use build::BuiltFile;
use bumpalo::Bump;
use clap::{Arg, ArgMatches, Command, ValueSource};
use roc_build::link::{LinkType, LinkingStrategy};
use roc_error_macros::{internal_error, user_error};
use roc_load::{LoadingProblem, Threading};
use roc_mono::ir::OptLevel;
use std::env;
use std::ffi::{CString, OsStr};
use std::io;
use std::path::{Path, PathBuf};
use std::process;
use target_lexicon::BinaryFormat;
use target_lexicon::{
Architecture, Environment, OperatingSystem, Triple, Vendor, X86_32Architecture,
};
#[cfg(not(target_os = "linux"))]
use tempfile::TempDir;
pub mod build;
mod format;
pub use format::format;
const DEFAULT_ROC_FILENAME: &str = "main.roc";
pub const CMD_BUILD: &str = "build";
pub const CMD_RUN: &str = "run";
pub const CMD_REPL: &str = "repl";
pub const CMD_EDIT: &str = "edit";
pub const CMD_DOCS: &str = "docs";
pub const CMD_CHECK: &str = "check";
pub const CMD_VERSION: &str = "version";
pub const CMD_FORMAT: &str = "format";
pub const FLAG_DEBUG: &str = "debug";
pub const FLAG_DEV: &str = "dev";
pub const FLAG_OPTIMIZE: &str = "optimize";
pub const FLAG_MAX_THREADS: &str = "max-threads";
pub const FLAG_OPT_SIZE: &str = "opt-size";
pub const FLAG_LIB: &str = "lib";
pub const FLAG_NO_LINK: &str = "no-link";
pub const FLAG_TARGET: &str = "target";
pub const FLAG_TIME: &str = "time";
pub const FLAG_LINKER: &str = "linker";
pub const FLAG_PRECOMPILED: &str = "precompiled-host";
pub const FLAG_VALGRIND: &str = "valgrind";
pub const FLAG_CHECK: &str = "check";
pub const ROC_FILE: &str = "ROC_FILE";
pub const ROC_DIR: &str = "ROC_DIR";
pub const DIRECTORY_OR_FILES: &str = "DIRECTORY_OR_FILES";
pub const ARGS_FOR_APP: &str = "ARGS_FOR_APP";
const VERSION: &str = include_str!("../../../version.txt");
pub fn build_app<'a>() -> Command<'a> {
let flag_optimize = Arg::new(FLAG_OPTIMIZE)
.long(FLAG_OPTIMIZE)
.help("Optimize the compiled program to run faster. (Optimization takes time to complete.)")
.required(false);
let flag_max_threads = Arg::new(FLAG_MAX_THREADS)
.long(FLAG_MAX_THREADS)
.help("Limit the number of threads (and hence cores) used during compilation.")
.takes_value(true)
.validator(|s| s.parse::<usize>())
.required(false);
let flag_opt_size = Arg::new(FLAG_OPT_SIZE)
.long(FLAG_OPT_SIZE)
.help("Optimize the compiled program to have a small binary size. (Optimization takes time to complete.)")
.required(false);
let flag_dev = Arg::new(FLAG_DEV)
.long(FLAG_DEV)
.help("Make compilation finish as soon as possible, at the expense of runtime performance.")
.required(false);
let flag_debug = Arg::new(FLAG_DEBUG)
.long(FLAG_DEBUG)
.help("Store LLVM debug information in the generated program.")
.required(false);
let flag_valgrind = Arg::new(FLAG_VALGRIND)
.long(FLAG_VALGRIND)
.help("Some assembly instructions are not supported by valgrind, this flag prevents those from being output when building the host.")
.required(false);
let flag_time = Arg::new(FLAG_TIME)
.long(FLAG_TIME)
.help("Prints detailed compilation time information.")
.required(false);
let flag_linker = Arg::new(FLAG_LINKER)
.long(FLAG_LINKER)
.help("Sets which linker to use. The surgical linker is enabled by default only when building for wasm32 or x86_64 Linux, because those are the only targets it currently supports. Otherwise the legacy linker is used by default.")
.possible_values(["surgical", "legacy"])
.required(false);
let flag_precompiled = Arg::new(FLAG_PRECOMPILED)
.long(FLAG_PRECOMPILED)
.help("Assumes the host has been precompiled and skips recompiling the host. (Enabled by default when using `roc build` with a --target other than `--target host`)")
.possible_values(["true", "false"])
.required(false);
let roc_file_to_run = Arg::new(ROC_FILE)
.help("The .roc file of an app to run")
.allow_invalid_utf8(true)
.required(false)
.default_value(DEFAULT_ROC_FILENAME);
let args_for_app = Arg::new(ARGS_FOR_APP)
.help("Arguments to pass into the app being run, e.g. `roc run -- arg1 arg2`")
.allow_invalid_utf8(true)
.multiple_values(true)
.takes_value(true)
.allow_hyphen_values(true)
.last(true);
let app = Command::new("roc")
.version(concatcp!(VERSION, "\n"))
.about("Runs the given .roc file, if there are no compilation errors.\nUse one of the SUBCOMMANDS below to do something else!")
.subcommand(Command::new(CMD_BUILD)
.about("Build a binary from the given .roc file, but don't run it")
.arg(flag_optimize.clone())
.arg(flag_max_threads.clone())
.arg(flag_opt_size.clone())
.arg(flag_dev.clone())
.arg(flag_debug.clone())
.arg(flag_time.clone())
.arg(flag_linker.clone())
.arg(flag_precompiled.clone())
.arg(flag_valgrind.clone())
.arg(
Arg::new(FLAG_TARGET)
.long(FLAG_TARGET)
.help("Choose a different target")
.default_value(Target::default().as_str())
.possible_values(Target::OPTIONS)
.required(false),
)
.arg(
Arg::new(FLAG_LIB)
.long(FLAG_LIB)
.help("Build a C library instead of an executable.")
.required(false),
)
.arg(
Arg::new(FLAG_NO_LINK)
.long(FLAG_NO_LINK)
.help("Does not link. Instead just outputs the `.o` file")
.required(false),
)
.arg(
Arg::new(ROC_FILE)
.help("The .roc file to build")
.allow_invalid_utf8(true)
.required(false)
.default_value(DEFAULT_ROC_FILENAME),
)
)
.subcommand(Command::new(CMD_REPL)
.about("Launch the interactive Read Eval Print Loop (REPL)")
)
.subcommand(Command::new(CMD_RUN)
.about("Run a .roc file even if it has build errors")
.arg(flag_optimize.clone())
.arg(flag_max_threads.clone())
.arg(flag_opt_size.clone())
.arg(flag_dev.clone())
.arg(flag_debug.clone())
.arg(flag_time.clone())
.arg(flag_linker.clone())
.arg(flag_precompiled.clone())
.arg(flag_valgrind.clone())
.arg(roc_file_to_run.clone())
.arg(args_for_app.clone())
)
.subcommand(Command::new(CMD_FORMAT)
.about("Format a .roc file using standard Roc formatting")
.arg(
Arg::new(DIRECTORY_OR_FILES)
.index(1)
.multiple_values(true)
.required(false)
.allow_invalid_utf8(true))
.arg(
Arg::new(FLAG_CHECK)
.long(FLAG_CHECK)
.help("Checks that specified files are formatted. If formatting is needed, it will return a non-zero exit code.")
.required(false),
)
)
.subcommand(Command::new(CMD_VERSION)
.about(concatcp!("Print the Roc compilers version, which is currently ", VERSION)))
.subcommand(Command::new(CMD_CHECK)
.about("Check the code for problems, but doesnt build or run it")
.arg(flag_time.clone())
.arg(flag_max_threads.clone())
.arg(
Arg::new(ROC_FILE)
.help("The .roc file of an app to check")
.allow_invalid_utf8(true)
.required(false)
.default_value(DEFAULT_ROC_FILENAME),
)
)
.subcommand(
Command::new(CMD_DOCS)
.about("Generate documentation for Roc modules (Work In Progress)")
.arg(Arg::new(DIRECTORY_OR_FILES)
.multiple_values(true)
.required(false)
.help("The directory or files to build documentation for")
.allow_invalid_utf8(true)
)
)
.trailing_var_arg(true)
.arg(flag_optimize)
.arg(flag_max_threads.clone())
.arg(flag_opt_size)
.arg(flag_dev)
.arg(flag_debug)
.arg(flag_time)
.arg(flag_linker)
.arg(flag_precompiled)
.arg(flag_valgrind)
.arg(roc_file_to_run.required(false))
.arg(args_for_app);
if cfg!(feature = "editor") {
app.subcommand(
Command::new(CMD_EDIT)
.about("Launch the Roc editor (Work In Progress)")
.arg(
Arg::new(DIRECTORY_OR_FILES)
.multiple_values(true)
.required(false)
.help("(optional) The directory or files to open on launch."),
),
)
} else {
app
}
}
#[derive(Debug, PartialEq, Eq)]
pub enum BuildConfig {
BuildOnly,
BuildAndRun,
BuildAndRunIfNoErrors,
}
pub enum FormatMode {
Format,
CheckOnly,
}
pub fn build(
matches: &ArgMatches,
config: BuildConfig,
triple: Triple,
link_type: LinkType,
) -> io::Result<i32> {
use build::build_file;
use BuildConfig::*;
let arena = Bump::new();
let filename = matches.value_of_os(ROC_FILE).unwrap();
let opt_level = match (
matches.is_present(FLAG_OPTIMIZE),
matches.is_present(FLAG_OPT_SIZE),
matches.is_present(FLAG_DEV),
) {
(true, false, false) => OptLevel::Optimize,
(false, true, false) => OptLevel::Size,
(false, false, true) => OptLevel::Development,
(false, false, false) => OptLevel::Normal,
_ => user_error!("build can be only one of `--dev`, `--optimize`, or `--opt-size`"),
};
let emit_debug_info = matches.is_present(FLAG_DEBUG);
let emit_timings = matches.is_present(FLAG_TIME);
let threading = match matches
.value_of(FLAG_MAX_THREADS)
.and_then(|s| s.parse::<usize>().ok())
{
None => Threading::AllAvailable,
Some(0) => user_error!("cannot build with at most 0 threads"),
Some(1) => Threading::Single,
Some(n) => Threading::AtMost(n),
};
let wasm_dev_backend = matches!(opt_level, OptLevel::Development)
&& matches!(triple.architecture, Architecture::Wasm32);
let linking_strategy = if wasm_dev_backend {
LinkingStrategy::Additive
} else if !roc_linker::supported(&link_type, &triple)
|| matches.value_of(FLAG_LINKER) == Some("legacy")
{
LinkingStrategy::Legacy
} else {
LinkingStrategy::Surgical
};
let precompiled = if matches.is_present(FLAG_PRECOMPILED) {
matches.value_of(FLAG_PRECOMPILED) == Some("true")
} else {
// When compiling for a different target, default to assuming a precompiled host.
// Otherwise compilation would most likely fail because many toolchains assume you're compiling for the host
// We make an exception for Wasm, because cross-compiling is the norm in that case.
triple != Triple::host() && !matches!(triple.architecture, Architecture::Wasm32)
};
let path = Path::new(filename);
// Spawn the root task
let path = path.canonicalize().unwrap_or_else(|err| {
use io::ErrorKind::*;
match err.kind() {
NotFound => {
let path_string = path.to_string_lossy();
// TODO these should use roc_reporting to display nicer error messages.
match matches.value_source(ROC_FILE) {
Some(ValueSource::DefaultValue) => {
eprintln!(
"\nNo `.roc` file was specified, and the current directory does not contain a {} file to use as a default.\n\nYou can run `roc help` for more information on how to provide a .roc file.\n",
DEFAULT_ROC_FILENAME
)
}
_ => eprintln!("\nThis file was not found: {}\n\nYou can run `roc help` for more information on how to provide a .roc file.\n", path_string),
}
process::exit(1);
}
_ => {
todo!("TODO Gracefully handle opening {:?} - {:?}", path, err);
}
}
});
let src_dir = path.parent().unwrap().canonicalize().unwrap();
let target_valgrind = matches.is_present(FLAG_VALGRIND);
let res_binary_path = build_file(
&arena,
&triple,
src_dir,
path,
opt_level,
emit_debug_info,
emit_timings,
link_type,
linking_strategy,
precompiled,
target_valgrind,
threading,
);
match res_binary_path {
Ok(BuiltFile {
binary_path,
problems,
total_time,
}) => {
match config {
BuildOnly => {
// If possible, report the generated executable name relative to the current dir.
let generated_filename = binary_path
.strip_prefix(env::current_dir().unwrap())
.unwrap_or(&binary_path);
// No need to waste time freeing this memory,
// since the process is about to exit anyway.
std::mem::forget(arena);
println!(
"\x1B[{}m{}\x1B[39m {} and \x1B[{}m{}\x1B[39m {} found in {} ms while successfully building:\n\n {}",
if problems.errors == 0 {
32 // green
} else {
33 // yellow
},
problems.errors,
if problems.errors == 1 {
"error"
} else {
"errors"
},
if problems.warnings == 0 {
32 // green
} else {
33 // yellow
},
problems.warnings,
if problems.warnings == 1 {
"warning"
} else {
"warnings"
},
total_time.as_millis(),
generated_filename.to_str().unwrap()
);
// Return a nonzero exit code if there were problems
Ok(problems.exit_code())
}
BuildAndRun => {
if problems.errors > 0 || problems.warnings > 0 {
println!(
"\x1B[{}m{}\x1B[39m {} and \x1B[{}m{}\x1B[39m {} found in {} ms.\n\nRunning program anyway…\n\n\x1B[36m{}\x1B[39m",
if problems.errors == 0 {
32 // green
} else {
33 // yellow
},
problems.errors,
if problems.errors == 1 {
"error"
} else {
"errors"
},
if problems.warnings == 0 {
32 // green
} else {
33 // yellow
},
problems.warnings,
if problems.warnings == 1 {
"warning"
} else {
"warnings"
},
total_time.as_millis(),
"".repeat(80)
);
}
let args = matches.values_of_os(ARGS_FOR_APP).unwrap_or_default();
let mut bytes = std::fs::read(&binary_path).unwrap();
let x = roc_run(arena, triple, args, &mut bytes);
std::mem::forget(bytes);
x
}
BuildAndRunIfNoErrors => {
if problems.errors == 0 {
if problems.warnings > 0 {
println!(
"\x1B[32m0\x1B[39m errors and \x1B[33m{}\x1B[39m {} found in {} ms.\n\nRunning program…\n\n\x1B[36m{}\x1B[39m",
problems.warnings,
if problems.warnings == 1 {
"warning"
} else {
"warnings"
},
total_time.as_millis(),
"".repeat(80)
);
}
let args = matches.values_of_os(ARGS_FOR_APP).unwrap_or_default();
let mut bytes = std::fs::read(&binary_path).unwrap();
let x = roc_run(arena, triple, args, &mut bytes);
std::mem::forget(bytes);
x
} else {
println!(
"\x1B[{}m{}\x1B[39m {} and \x1B[{}m{}\x1B[39m {} found in {} ms.\n\nYou can run the program anyway with: \x1B[32mroc run {}\x1B[39m",
if problems.errors == 0 {
32 // green
} else {
33 // yellow
},
problems.errors,
if problems.errors == 1 {
"error"
} else {
"errors"
},
if problems.warnings == 0 {
32 // green
} else {
33 // yellow
},
problems.warnings,
if problems.warnings == 1 {
"warning"
} else {
"warnings"
},
total_time.as_millis(),
filename.to_string_lossy()
);
Ok(problems.exit_code())
}
}
}
}
Err(LoadingProblem::FormattedReport(report)) => {
print!("{}", report);
Ok(1)
}
Err(other) => {
panic!("build_file failed with error:\n{:?}", other);
}
}
}
fn roc_run<'a, I: IntoIterator<Item = &'a OsStr>>(
arena: Bump, // This should be passed an owned value, not a reference, so we can usefully mem::forget it!
triple: Triple,
args: I,
binary_bytes: &mut [u8],
) -> io::Result<i32> {
match triple.architecture {
Architecture::Wasm32 => {
let executable = roc_run_executable_file_path(binary_bytes)?;
let path = executable.as_path();
// If possible, report the generated executable name relative to the current dir.
let generated_filename = path
.strip_prefix(env::current_dir().unwrap())
.unwrap_or(path);
// No need to waste time freeing this memory,
// since the process is about to exit anyway.
std::mem::forget(arena);
if cfg!(target_family = "unix") {
use std::os::unix::ffi::OsStrExt;
run_with_wasmer(
generated_filename,
args.into_iter().map(|os_str| os_str.as_bytes()),
);
} else {
run_with_wasmer(
generated_filename,
args.into_iter().map(|os_str| {
os_str.to_str().expect(
"Roc does not currently support passing non-UTF8 arguments to Wasmer.",
)
}),
);
}
Ok(0)
}
_ => roc_run_native(arena, args, binary_bytes),
}
}
/// Run on the native OS (not on wasm)
#[cfg(target_family = "unix")]
fn roc_run_native<I: IntoIterator<Item = S>, S: AsRef<OsStr>>(
arena: Bump,
args: I,
binary_bytes: &mut [u8],
) -> std::io::Result<i32> {
use bumpalo::collections::CollectIn;
use std::os::unix::ffi::OsStrExt;
unsafe {
let executable = roc_run_executable_file_path(binary_bytes)?;
let path = executable.as_path();
let path_cstring = CString::new(path.as_os_str().as_bytes()).unwrap();
// argv is an array of pointers to strings passed to the new program
// as its command-line arguments. By convention, the first of these
// strings (i.e., argv[0]) should contain the filename associated
// with the file being executed. The argv array must be terminated
// by a NULL pointer. (Thus, in the new program, argv[argc] will be NULL.)
let c_strings: bumpalo::collections::Vec<CString> = args
.into_iter()
.map(|x| CString::new(x.as_ref().as_bytes()).unwrap())
.collect_in(&arena);
let c_string_pointers = c_strings
.iter()
.map(|x| x.as_bytes_with_nul().as_ptr().cast());
let argv: bumpalo::collections::Vec<*const libc::c_char> =
std::iter::once(path_cstring.as_ptr())
.chain(c_string_pointers)
.chain([std::ptr::null()])
.collect_in(&arena);
// envp is an array of pointers to strings, conventionally of the
// form key=value, which are passed as the environment of the new
// program. The envp array must be terminated by a NULL pointer.
let envp_cstrings: bumpalo::collections::Vec<CString> = std::env::vars_os()
.flat_map(|(k, v)| {
[
CString::new(k.as_bytes()).unwrap(),
CString::new(v.as_bytes()).unwrap(),
]
})
.collect_in(&arena);
let envp: bumpalo::collections::Vec<*const libc::c_char> = envp_cstrings
.iter()
.map(|s| s.as_ptr())
.chain([std::ptr::null()])
.collect_in(&arena);
match executable {
#[cfg(target_os = "linux")]
ExecutableFile::MemFd(fd, _) => {
if libc::fexecve(fd, argv.as_ptr(), envp.as_ptr()) != 0 {
internal_error!(
"libc::fexecve({:?}, ..., ...) failed: {:?}",
path,
errno::errno()
);
}
}
#[cfg(not(target_os = "linux"))]
ExecutableFile::OnDisk(_, _) => {
if libc::execve(path_cstring.as_ptr().cast(), argv.as_ptr(), envp.as_ptr()) != 0 {
internal_error!(
"libc::execve({:?}, ..., ...) failed: {:?}",
path,
errno::errno()
);
}
}
}
}
Ok(1)
}
#[derive(Debug)]
enum ExecutableFile {
#[cfg(target_os = "linux")]
MemFd(libc::c_int, PathBuf),
#[cfg(not(target_os = "linux"))]
OnDisk(TempDir, PathBuf),
}
impl ExecutableFile {
fn as_path(&self) -> &Path {
match self {
#[cfg(target_os = "linux")]
ExecutableFile::MemFd(_, path_buf) => path_buf.as_ref(),
#[cfg(not(target_os = "linux"))]
ExecutableFile::OnDisk(_, path_buf) => path_buf.as_ref(),
}
}
}
#[cfg(target_os = "linux")]
fn roc_run_executable_file_path(binary_bytes: &mut [u8]) -> std::io::Result<ExecutableFile> {
// on linux, we use the `memfd_create` function to create an in-memory anonymous file.
let flags = 0;
let anonymous_file_name = "roc_file_descriptor\0";
let fd = unsafe { libc::memfd_create(anonymous_file_name.as_ptr().cast(), flags) };
if fd == 0 {
internal_error!(
"libc::memfd_create({:?}, {}) failed: file descriptor is 0",
anonymous_file_name,
flags
);
}
let path = PathBuf::from(format!("/proc/self/fd/{}", fd));
std::fs::write(&path, binary_bytes)?;
Ok(ExecutableFile::MemFd(fd, path))
}
#[cfg(all(target_family = "unix", not(target_os = "linux")))]
fn roc_run_executable_file_path(binary_bytes: &mut [u8]) -> std::io::Result<ExecutableFile> {
use std::fs::OpenOptions;
use std::io::Write;
use std::os::unix::fs::OpenOptionsExt;
let temp_dir = tempfile::tempdir()?;
// We have not found a way to use a virtual file on non-Linux OSes.
// Hence we fall back to just writing the file to the file system, and using that file.
let app_path_buf = temp_dir.path().join("roc_app_binary");
let mut file = OpenOptions::new()
.create(true)
.write(true)
.mode(0o777) // create the file as executable
.open(&app_path_buf)?;
file.write_all(binary_bytes)?;
// We store the TempDir in this variant alongside the path to the executable,
// so that the TempDir doesn't get dropped until after we're done with the path.
// If we didn't do that, then the tempdir would potentially get deleted by the
// TempDir's Drop impl before the file had been executed.
Ok(ExecutableFile::OnDisk(temp_dir, app_path_buf))
}
/// Run on the native OS (not on wasm)
#[cfg(not(target_family = "unix"))]
fn roc_run_native<I: IntoIterator<Item = S>, S: AsRef<OsStr>>(
_arena: Bump, // This should be passed an owned value, not a reference, so we can usefully mem::forget it!
_args: I,
_binary_bytes: &mut [u8],
) -> io::Result<i32> {
todo!("TODO support running roc programs on non-UNIX targets");
// let mut cmd = std::process::Command::new(&binary_path);
// // Run the compiled app
// let exit_status = cmd
// .spawn()
// .unwrap_or_else(|err| panic!("Failed to run app after building it: {:?}", err))
// .wait()
// .expect("TODO gracefully handle block_on failing when `roc` spawns a subprocess for the compiled app");
// // `roc [FILE]` exits with the same status code as the app it ran.
// //
// // If you want to know whether there were compilation problems
// // via status code, use either `roc build` or `roc check` instead!
// match exit_status.code() {
// Some(code) => Ok(code),
// None => {
// todo!("TODO gracefully handle the `roc [FILE]` subprocess terminating with a signal.");
// }
// }
}
#[cfg(feature = "run-wasm32")]
fn run_with_wasmer<I: Iterator<Item = S>, S: AsRef<[u8]>>(wasm_path: &std::path::Path, args: I) {
use wasmer::{Instance, Module, Store};
let store = Store::default();
let module = Module::from_file(&store, &wasm_path).unwrap();
// First, we create the `WasiEnv`
use wasmer_wasi::WasiState;
let mut wasi_env = WasiState::new("hello").args(args).finalize().unwrap();
// Then, we get the import object related to our WASI
// and attach it to the Wasm instance.
let import_object = wasi_env.import_object(&module).unwrap();
let instance = Instance::new(&module, &import_object).unwrap();
let start = instance.exports.get_function("_start").unwrap();
use wasmer_wasi::WasiError;
match start.call(&[]) {
Ok(_) => {}
Err(e) => match e.downcast::<WasiError>() {
Ok(WasiError::Exit(0)) => {
// we run the `_start` function, so exit(0) is expected
}
other => panic!("Wasmer error: {:?}", other),
},
}
}
#[cfg(not(feature = "run-wasm32"))]
fn run_with_wasmer<I: Iterator<Item = S>, S: AsRef<[u8]>>(_wasm_path: &std::path::Path, _args: I) {
println!("Running wasm files is not supported on this target.");
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum Target {
System,
Linux32,
Linux64,
Wasm32,
}
impl Default for Target {
fn default() -> Self {
Target::System
}
}
impl Target {
const fn as_str(&self) -> &'static str {
use Target::*;
match self {
System => "system",
Linux32 => "linux32",
Linux64 => "linux64",
Wasm32 => "wasm32",
}
}
/// NOTE keep up to date!
const OPTIONS: &'static [&'static str] = &[
Target::System.as_str(),
Target::Linux32.as_str(),
Target::Linux64.as_str(),
Target::Wasm32.as_str(),
];
pub fn to_triple(self) -> Triple {
use Target::*;
match self {
System => Triple::host(),
Linux32 => Triple {
architecture: Architecture::X86_32(X86_32Architecture::I386),
vendor: Vendor::Unknown,
operating_system: OperatingSystem::Linux,
environment: Environment::Musl,
binary_format: BinaryFormat::Elf,
},
Linux64 => Triple {
architecture: Architecture::X86_64,
vendor: Vendor::Unknown,
operating_system: OperatingSystem::Linux,
environment: Environment::Musl,
binary_format: BinaryFormat::Elf,
},
Wasm32 => Triple {
architecture: Architecture::Wasm32,
vendor: Vendor::Unknown,
operating_system: OperatingSystem::Unknown,
environment: Environment::Unknown,
binary_format: BinaryFormat::Wasm,
},
}
}
}
impl From<&Target> for Triple {
fn from(target: &Target) -> Self {
target.to_triple()
}
}
impl std::fmt::Display for Target {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
impl std::str::FromStr for Target {
type Err = String;
fn from_str(string: &str) -> Result<Self, Self::Err> {
match string {
"system" => Ok(Target::System),
"linux32" => Ok(Target::Linux32),
"linux64" => Ok(Target::Linux64),
"wasm32" => Ok(Target::Wasm32),
_ => Err(format!("Roc does not know how to compile to {}", string)),
}
}
}

303
crates/cli/src/main.rs Normal file
View file

@ -0,0 +1,303 @@
use roc_build::link::LinkType;
use roc_cli::build::check_file;
use roc_cli::{
build_app, format, BuildConfig, FormatMode, Target, CMD_BUILD, CMD_CHECK, CMD_DOCS, CMD_EDIT,
CMD_FORMAT, CMD_REPL, CMD_RUN, CMD_VERSION, DIRECTORY_OR_FILES, FLAG_CHECK, FLAG_LIB,
FLAG_NO_LINK, FLAG_TARGET, FLAG_TIME, ROC_FILE,
};
use roc_docs::generate_docs_html;
use roc_error_macros::user_error;
use roc_load::{LoadingProblem, Threading};
use std::fs::{self, FileType};
use std::io;
use std::path::{Path, PathBuf};
use target_lexicon::Triple;
#[macro_use]
extern crate const_format;
#[global_allocator]
static ALLOC: mimalloc::MiMalloc = mimalloc::MiMalloc;
use std::ffi::{OsStr, OsString};
use roc_cli::build;
fn main() -> io::Result<()> {
let matches = build_app().get_matches();
let exit_code = match matches.subcommand() {
None => {
if matches.is_present(ROC_FILE) {
build(
&matches,
BuildConfig::BuildAndRunIfNoErrors,
Triple::host(),
LinkType::Executable,
)
} else {
launch_editor(None)?;
Ok(0)
}
}
Some((CMD_RUN, matches)) => {
if matches.is_present(ROC_FILE) {
build(
matches,
BuildConfig::BuildAndRun,
Triple::host(),
LinkType::Executable,
)
} else {
eprintln!("What .roc file do you want to run? Specify it at the end of the `roc run` command.");
Ok(1)
}
}
Some((CMD_BUILD, matches)) => {
let target: Target = matches.value_of_t(FLAG_TARGET).unwrap_or_default();
let link_type = match (
matches.is_present(FLAG_LIB),
matches.is_present(FLAG_NO_LINK),
) {
(true, false) => LinkType::Dylib,
(true, true) => user_error!("build can only be one of `--lib` or `--no-link`"),
(false, true) => LinkType::None,
(false, false) => LinkType::Executable,
};
Ok(build(
matches,
BuildConfig::BuildOnly,
target.to_triple(),
link_type,
)?)
}
Some((CMD_CHECK, matches)) => {
let arena = bumpalo::Bump::new();
let emit_timings = matches.is_present(FLAG_TIME);
let filename = matches.value_of_os(ROC_FILE).unwrap();
let roc_file_path = PathBuf::from(filename);
let src_dir = roc_file_path.parent().unwrap().to_owned();
let threading = match matches
.value_of(roc_cli::FLAG_MAX_THREADS)
.and_then(|s| s.parse::<usize>().ok())
{
None => Threading::AllAvailable,
Some(0) => user_error!("cannot build with at most 0 threads"),
Some(1) => Threading::Single,
Some(n) => Threading::AtMost(n),
};
match check_file(&arena, src_dir, roc_file_path, emit_timings, threading) {
Ok((problems, total_time)) => {
println!(
"\x1B[{}m{}\x1B[39m {} and \x1B[{}m{}\x1B[39m {} found in {} ms.",
if problems.errors == 0 {
32 // green
} else {
33 // yellow
},
problems.errors,
if problems.errors == 1 {
"error"
} else {
"errors"
},
if problems.warnings == 0 {
32 // green
} else {
33 // yellow
},
problems.warnings,
if problems.warnings == 1 {
"warning"
} else {
"warnings"
},
total_time.as_millis(),
);
Ok(problems.exit_code())
}
Err(LoadingProblem::FormattedReport(report)) => {
print!("{}", report);
Ok(1)
}
Err(other) => {
panic!("build_file failed with error:\n{:?}", other);
}
}
}
Some((CMD_REPL, _)) => {
{
roc_repl_cli::main()?;
// Exit 0 if the repl exited normally
Ok(0)
}
}
Some((CMD_EDIT, matches)) => {
match matches
.values_of_os(DIRECTORY_OR_FILES)
.map(|mut values| values.next())
{
Some(Some(os_str)) => {
launch_editor(Some(Path::new(os_str)))?;
}
_ => {
launch_editor(None)?;
}
}
// Exit 0 if the editor exited normally
Ok(0)
}
Some((CMD_DOCS, matches)) => {
let maybe_values = matches.values_of_os(DIRECTORY_OR_FILES);
let mut values: Vec<OsString> = Vec::new();
match maybe_values {
None => {
let mut os_string_values: Vec<OsString> = Vec::new();
read_all_roc_files(
&std::env::current_dir()?.as_os_str().to_os_string(),
&mut os_string_values,
)?;
for os_string in os_string_values {
values.push(os_string);
}
}
Some(os_values) => {
for os_str in os_values {
values.push(os_str.to_os_string());
}
}
}
let mut roc_files = Vec::new();
// Populate roc_files
for os_str in values {
let metadata = fs::metadata(os_str.clone())?;
roc_files_recursive(os_str.as_os_str(), metadata.file_type(), &mut roc_files)?;
}
generate_docs_html(roc_files);
Ok(0)
}
Some((CMD_FORMAT, matches)) => {
let maybe_values = matches.values_of_os(DIRECTORY_OR_FILES);
let mut values: Vec<OsString> = Vec::new();
match maybe_values {
None => {
let mut os_string_values: Vec<OsString> = Vec::new();
read_all_roc_files(
&std::env::current_dir()?.as_os_str().to_os_string(),
&mut os_string_values,
)?;
for os_string in os_string_values {
values.push(os_string);
}
}
Some(os_values) => {
for os_str in os_values {
values.push(os_str.to_os_string());
}
}
}
let mut roc_files = Vec::new();
// Populate roc_files
for os_str in values {
let metadata = fs::metadata(os_str.clone())?;
roc_files_recursive(os_str.as_os_str(), metadata.file_type(), &mut roc_files)?;
}
let format_mode = match matches.is_present(FLAG_CHECK) {
true => FormatMode::CheckOnly,
false => FormatMode::Format,
};
let format_exit_code = match format(roc_files, format_mode) {
Ok(_) => 0,
Err(message) => {
eprintln!("{}", message);
1
}
};
Ok(format_exit_code)
}
Some((CMD_VERSION, _)) => {
print!(
"{}",
concatcp!("roc ", include_str!("../../../version.txt"))
);
Ok(0)
}
_ => unreachable!(),
}?;
std::process::exit(exit_code);
}
fn read_all_roc_files(
dir: &OsString,
roc_file_paths: &mut Vec<OsString>,
) -> Result<(), std::io::Error> {
let entries = fs::read_dir(dir)?;
for entry in entries {
let path = entry?.path();
if path.is_dir() {
read_all_roc_files(&path.into_os_string(), roc_file_paths)?;
} else if path.extension().and_then(OsStr::to_str) == Some("roc") {
let file_path = path.into_os_string();
roc_file_paths.push(file_path);
}
}
Ok(())
}
fn roc_files_recursive<P: AsRef<Path>>(
path: P,
file_type: FileType,
roc_files: &mut Vec<PathBuf>,
) -> io::Result<()> {
if file_type.is_dir() {
for entry_res in fs::read_dir(path)? {
let entry = entry_res?;
roc_files_recursive(entry.path(), entry.file_type()?, roc_files)?;
}
} else {
roc_files.push(path.as_ref().to_path_buf());
}
Ok(())
}
#[cfg(feature = "editor")]
fn launch_editor(project_dir_path: Option<&Path>) -> io::Result<()> {
roc_editor::launch(project_dir_path)
}
#[cfg(not(feature = "editor"))]
fn launch_editor(_project_dir_path: Option<&Path>) -> io::Result<()> {
panic!("Cannot launch the editor because this build of roc did not include `feature = \"editor\"`!");
}

1123
crates/cli/tests/cli_run.rs Normal file

File diff suppressed because it is too large Load diff

7
crates/cli/tests/fixtures/.gitignore vendored Normal file
View file

@ -0,0 +1,7 @@
app
*.o
*.dSYM
dynhost
libapp.so
metadata
preprocessedhost

Some files were not shown because too many files have changed in this diff Show more