Merge pull request #3769 from roc-lang/expect-fx-codegen

expect fx codegen
This commit is contained in:
Folkert de Vries 2022-08-24 19:48:42 +02:00 committed by GitHub
commit c06e5cfa32
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 773 additions and 191 deletions

1
Cargo.lock generated
View file

@ -4009,6 +4009,7 @@ dependencies = [
"roc_std", "roc_std",
"roc_target", "roc_target",
"roc_types", "roc_types",
"signal-hook",
"strip-ansi-escapes", "strip-ansi-escapes",
"target-lexicon", "target-lexicon",
"tempfile", "tempfile",

View file

@ -8,12 +8,10 @@ use roc_build::link::{LinkType, LinkingStrategy};
use roc_collections::VecMap; use roc_collections::VecMap;
use roc_error_macros::{internal_error, user_error}; use roc_error_macros::{internal_error, user_error};
use roc_gen_llvm::llvm::build::LlvmBackendMode; use roc_gen_llvm::llvm::build::LlvmBackendMode;
use roc_gen_llvm::run_roc::RocCallResult;
use roc_gen_llvm::run_roc_dylib;
use roc_load::{ExecutionMode, Expectations, LoadConfig, LoadingProblem, Threading}; use roc_load::{ExecutionMode, Expectations, LoadConfig, LoadingProblem, Threading};
use roc_module::symbol::{Interns, ModuleId}; use roc_module::symbol::{Interns, ModuleId};
use roc_mono::ir::OptLevel; use roc_mono::ir::OptLevel;
use roc_repl_expect::run::{expect_mono_module_to_dylib, roc_dev_expect}; use roc_repl_expect::run::expect_mono_module_to_dylib;
use roc_target::TargetInfo; use roc_target::TargetInfo;
use std::env; use std::env;
use std::ffi::{CString, OsStr}; use std::ffi::{CString, OsStr};
@ -320,8 +318,6 @@ pub enum FormatMode {
CheckOnly, CheckOnly,
} }
const SHM_SIZE: i64 = 1024;
pub fn test(matches: &ArgMatches, triple: Triple) -> io::Result<i32> { pub fn test(matches: &ArgMatches, triple: Triple) -> io::Result<i32> {
let start_time = Instant::now(); let start_time = Instant::now();
let arena = Bump::new(); let arena = Bump::new();
@ -407,13 +403,6 @@ pub fn test(matches: &ArgMatches, triple: Triple) -> io::Result<i32> {
let mut writer = std::io::stdout(); let mut writer = std::io::stdout();
let mut shared_buffer = vec![0u8; SHM_SIZE as usize];
let set_shared_buffer = run_roc_dylib!(lib, "set_shared_buffer", (*mut u8, usize), ());
let mut result = RocCallResult::default();
let slice = (shared_buffer.as_mut_ptr(), shared_buffer.len());
unsafe { set_shared_buffer(slice, &mut result) };
let (failed, passed) = roc_repl_expect::run::run_expects( let (failed, passed) = roc_repl_expect::run::run_expects(
&mut writer, &mut writer,
roc_reporting::report::RenderTarget::ColorTerminal, roc_reporting::report::RenderTarget::ColorTerminal,
@ -421,7 +410,6 @@ pub fn test(matches: &ArgMatches, triple: Triple) -> io::Result<i32> {
interns, interns,
&lib, &lib,
&mut expectations, &mut expectations,
shared_buffer.as_mut_ptr(),
expects, expects,
) )
.unwrap(); .unwrap();
@ -953,94 +941,13 @@ impl ExecutableFile {
// with Expect // with Expect
#[cfg(target_family = "unix")] #[cfg(target_family = "unix")]
unsafe fn roc_run_native_debug( unsafe fn roc_run_native_debug(
executable: ExecutableFile, _executable: ExecutableFile,
argv: &[*const c_char], _argv: &[*const c_char],
envp: &[*const c_char], _envp: &[*const c_char],
mut expectations: VecMap<ModuleId, Expectations>, _expectations: VecMap<ModuleId, Expectations>,
interns: Interns, _interns: Interns,
) { ) {
use signal_hook::{consts::signal::SIGCHLD, consts::signal::SIGUSR1, iterator::Signals}; todo!()
let mut signals = Signals::new(&[SIGCHLD, SIGUSR1]).unwrap();
match libc::fork() {
0 => {
// we are the child
if executable.execve(argv, envp) < 0 {
// Get the current value of errno
let e = errno::errno();
// Extract the error code as an i32
let code = e.0;
// Display a human-friendly error message
println!("💥 Error {}: {}", code, e);
}
}
-1 => {
// something failed
// Get the current value of errno
let e = errno::errno();
// Extract the error code as an i32
let code = e.0;
// Display a human-friendly error message
println!("Error {}: {}", code, e);
process::exit(1)
}
1.. => {
let name = "/roc_expect_buffer"; // IMPORTANT: shared memory object names must begin with / and contain no other slashes!
let cstring = CString::new(name).unwrap();
let arena = &bumpalo::Bump::new();
let interns = arena.alloc(interns);
for sig in &mut signals {
match sig {
SIGCHLD => {
// clean up
libc::shm_unlink(cstring.as_ptr().cast());
// done!
process::exit(0);
}
SIGUSR1 => {
// this is the signal we use for an expect failure. Let's see what the child told us
let shared_fd =
libc::shm_open(cstring.as_ptr().cast(), libc::O_RDONLY, 0o666);
libc::ftruncate(shared_fd, SHM_SIZE);
let shared_ptr = libc::mmap(
std::ptr::null_mut(),
SHM_SIZE as usize,
libc::PROT_READ,
libc::MAP_SHARED,
shared_fd,
0,
);
let shared_memory_ptr: *mut u8 = shared_ptr.cast();
roc_dev_expect(
&mut std::io::stdout(),
arena,
&mut expectations,
interns,
shared_memory_ptr,
)
.unwrap();
}
_ => println!("received signal {}", sig),
}
}
}
_ => unreachable!(),
}
} }
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]

View file

@ -463,6 +463,7 @@ fn stmt_spec<'a>(
builder.add_choice(block, &cases) builder.add_choice(block, &cases)
} }
Expect { remainder, .. } => stmt_spec(builder, env, block, layout, remainder), Expect { remainder, .. } => stmt_spec(builder, env, block, layout, remainder),
ExpectFx { remainder, .. } => stmt_spec(builder, env, block, layout, remainder),
Ret(symbol) => Ok(env.symbols[symbol]), Ret(symbol) => Ok(env.symbols[symbol]),
Refcounting(modify_rc, continuation) => match modify_rc { Refcounting(modify_rc, continuation) => match modify_rc {
ModifyRc::Inc(symbol, _) => { ModifyRc::Inc(symbol, _) => {

View file

@ -1,10 +1,5 @@
const std = @import("std"); const std = @import("std");
extern fn shm_open(name: *const i8, oflag: c_int, mode: c_uint) c_int;
extern fn mmap(addr: ?*anyopaque, length: c_uint, prot: c_int, flags: c_int, fd: c_int, offset: c_uint) *anyopaque;
extern fn kill(pid: c_int, sig: c_int) c_int;
extern fn getppid() c_int;
const SIGUSR1: c_int = 10; const SIGUSR1: c_int = 10;
const O_RDWR: c_int = 2; const O_RDWR: c_int = 2;
@ -26,9 +21,3 @@ pub fn setSharedBuffer(ptr: [*]u8, length: usize) callconv(.C) usize {
pub fn expectFailedStart() callconv(.C) [*]u8 { pub fn expectFailedStart() callconv(.C) [*]u8 {
return SHARED_BUFFER.ptr; return SHARED_BUFFER.ptr;
} }
pub fn expectFailedFinalize() callconv(.C) void {
const parent_pid = getppid();
_ = kill(parent_pid, SIGUSR1);
}

View file

@ -168,7 +168,6 @@ comptime {
if (builtin.target.cpu.arch != .wasm32) { if (builtin.target.cpu.arch != .wasm32) {
exportUtilsFn(expect.expectFailedStart, "expect_failed_start"); exportUtilsFn(expect.expectFailedStart, "expect_failed_start");
exportUtilsFn(expect.expectFailedFinalize, "expect_failed_finalize");
// sets the buffer used for expect failures // sets the buffer used for expect failures
@export(expect.setSharedBuffer, .{ .name = "set_shared_buffer", .linkage = .Weak }); @export(expect.setSharedBuffer, .{ .name = "set_shared_buffer", .linkage = .Weak });

View file

@ -613,6 +613,16 @@ fn deep_copy_expr_help<C: CopyEnv>(env: &mut C, copied: &mut Vec<Variable>, expr
lookups_in_cond: lookups_in_cond.to_vec(), lookups_in_cond: lookups_in_cond.to_vec(),
}, },
ExpectFx {
loc_condition,
loc_continuation,
lookups_in_cond,
} => ExpectFx {
loc_condition: Box::new(loc_condition.map(|e| go_help!(e))),
loc_continuation: Box::new(loc_continuation.map(|e| go_help!(e))),
lookups_in_cond: lookups_in_cond.to_vec(),
},
TypedHole(v) => TypedHole(sub!(*v)), TypedHole(v) => TypedHole(sub!(*v)),
RuntimeError(err) => RuntimeError(err.clone()), RuntimeError(err) => RuntimeError(err.clone()),

View file

@ -231,6 +231,13 @@ pub enum Expr {
lookups_in_cond: Vec<(Symbol, Variable)>, lookups_in_cond: Vec<(Symbol, Variable)>,
}, },
// not parsed, but is generated when lowering toplevel effectful expects
ExpectFx {
loc_condition: Box<Loc<Expr>>,
loc_continuation: Box<Loc<Expr>>,
lookups_in_cond: Vec<(Symbol, Variable)>,
},
/// Rendered as empty box in editor /// Rendered as empty box in editor
TypedHole(Variable), TypedHole(Variable),
@ -277,6 +284,7 @@ impl Expr {
Category::OpaqueWrap(opaque_name) Category::OpaqueWrap(opaque_name)
} }
Self::Expect { .. } => Category::Expect, Self::Expect { .. } => Category::Expect,
Self::ExpectFx { .. } => Category::Expect,
// these nodes place no constraints on the expression's type // these nodes place no constraints on the expression's type
Self::TypedHole(_) | Self::RuntimeError(..) => Category::Unknown, Self::TypedHole(_) | Self::RuntimeError(..) => Category::Unknown,
@ -1772,6 +1780,28 @@ pub fn inline_calls(var_store: &mut VarStore, scope: &mut Scope, expr: Expr) ->
} }
} }
ExpectFx {
loc_condition,
loc_continuation,
lookups_in_cond,
} => {
let loc_condition = Loc {
region: loc_condition.region,
value: inline_calls(var_store, scope, loc_condition.value),
};
let loc_continuation = Loc {
region: loc_continuation.region,
value: inline_calls(var_store, scope, loc_continuation.value),
};
ExpectFx {
loc_condition: Box::new(loc_condition),
loc_continuation: Box::new(loc_continuation),
lookups_in_cond,
}
}
LetRec(defs, loc_expr, mark) => { LetRec(defs, loc_expr, mark) => {
let mut new_defs = Vec::with_capacity(defs.len()); let mut new_defs = Vec::with_capacity(defs.len());
@ -2327,7 +2357,7 @@ impl Declarations {
) -> usize { ) -> usize {
let index = self.declarations.len(); let index = self.declarations.len();
self.declarations.push(DeclarationTag::Expectation); self.declarations.push(DeclarationTag::ExpectationFx);
self.variables.push(Variable::BOOL); self.variables.push(Variable::BOOL);
self.symbols.push(Loc::at(preceding_comment, name)); self.symbols.push(Loc::at(preceding_comment, name));
self.annotations.push(None); self.annotations.push(None);
@ -2513,13 +2543,13 @@ impl Declarations {
} }
Expectation => { Expectation => {
let loc_expr = let loc_expr =
toplevel_expect_to_inline_expect(self.expressions[index].clone()); toplevel_expect_to_inline_expect_pure(self.expressions[index].clone());
collector.visit_expr(&loc_expr.value, loc_expr.region, var); collector.visit_expr(&loc_expr.value, loc_expr.region, var);
} }
ExpectationFx => { ExpectationFx => {
let loc_expr = let loc_expr =
toplevel_expect_to_inline_expect(self.expressions[index].clone()); toplevel_expect_to_inline_expect_fx(self.expressions[index].clone());
collector.visit_expr(&loc_expr.value, loc_expr.region, var); collector.visit_expr(&loc_expr.value, loc_expr.region, var);
} }
@ -2658,6 +2688,9 @@ fn get_lookup_symbols(expr: &Expr, var_store: &mut VarStore) -> Vec<(Symbol, Var
} }
Expr::Expect { Expr::Expect {
loc_continuation, .. loc_continuation, ..
}
| Expr::ExpectFx {
loc_continuation, ..
} => { } => {
stack.push(&(*loc_continuation).value); stack.push(&(*loc_continuation).value);
@ -2705,7 +2738,15 @@ fn get_lookup_symbols(expr: &Expr, var_store: &mut VarStore) -> Vec<(Symbol, Var
/// This is supposed to happen just before monomorphization: /// This is supposed to happen just before monomorphization:
/// all type errors and such are generated from the user source, /// all type errors and such are generated from the user source,
/// but this transformation means that we don't need special codegen for toplevel expects /// but this transformation means that we don't need special codegen for toplevel expects
pub fn toplevel_expect_to_inline_expect(mut loc_expr: Loc<Expr>) -> Loc<Expr> { pub fn toplevel_expect_to_inline_expect_pure(loc_expr: Loc<Expr>) -> Loc<Expr> {
toplevel_expect_to_inline_expect_help(loc_expr, false)
}
pub fn toplevel_expect_to_inline_expect_fx(loc_expr: Loc<Expr>) -> Loc<Expr> {
toplevel_expect_to_inline_expect_help(loc_expr, true)
}
fn toplevel_expect_to_inline_expect_help(mut loc_expr: Loc<Expr>, has_effects: bool) -> Loc<Expr> {
enum StoredDef { enum StoredDef {
NonRecursive(Region, Box<Def>), NonRecursive(Region, Box<Def>),
Recursive(Region, Vec<Def>, IllegalCycleMark), Recursive(Region, Vec<Def>, IllegalCycleMark),
@ -2735,10 +2776,18 @@ pub fn toplevel_expect_to_inline_expect(mut loc_expr: Loc<Expr>) -> Loc<Expr> {
} }
let expect_region = loc_expr.region; let expect_region = loc_expr.region;
let expect = Expr::Expect { let expect = if has_effects {
loc_condition: Box::new(loc_expr), Expr::ExpectFx {
loc_continuation: Box::new(Loc::at_zero(Expr::EmptyRecord)), loc_condition: Box::new(loc_expr),
lookups_in_cond, loc_continuation: Box::new(Loc::at_zero(Expr::EmptyRecord)),
lookups_in_cond,
}
} else {
Expr::Expect {
loc_condition: Box::new(loc_expr),
loc_continuation: Box::new(Loc::at_zero(Expr::EmptyRecord)),
lookups_in_cond,
}
}; };
let mut loc_expr = Loc::at(expect_region, expect); let mut loc_expr = Loc::at(expect_region, expect);
@ -2764,14 +2813,21 @@ struct ExpectCollector {
impl crate::traverse::Visitor for ExpectCollector { impl crate::traverse::Visitor for ExpectCollector {
fn visit_expr(&mut self, expr: &Expr, _region: Region, var: Variable) { fn visit_expr(&mut self, expr: &Expr, _region: Region, var: Variable) {
if let Expr::Expect { match expr {
lookups_in_cond, Expr::Expect {
loc_condition, lookups_in_cond,
.. loc_condition,
} = expr ..
{ }
self.expects | Expr::ExpectFx {
.insert(loc_condition.region, lookups_in_cond.to_vec()); lookups_in_cond,
loc_condition,
..
} => {
self.expects
.insert(loc_condition.region, lookups_in_cond.to_vec());
}
_ => (),
} }
walk_expr(self, expr, var) walk_expr(self, expr, var)

View file

@ -964,6 +964,23 @@ fn fix_values_captured_in_closure_expr(
); );
} }
ExpectFx {
loc_condition,
loc_continuation,
lookups_in_cond: _,
} => {
fix_values_captured_in_closure_expr(
&mut loc_condition.value,
no_capture_symbols,
closure_captures,
);
fix_values_captured_in_closure_expr(
&mut loc_continuation.value,
no_capture_symbols,
closure_captures,
);
}
Closure(ClosureData { Closure(ClosureData {
captured_symbols, captured_symbols,
name, name,

View file

@ -276,6 +276,19 @@ pub fn walk_expr<V: Visitor>(visitor: &mut V, expr: &Expr, var: Variable) {
Variable::NULL, Variable::NULL,
); );
} }
Expr::ExpectFx {
loc_condition,
loc_continuation,
lookups_in_cond: _,
} => {
// TODO: what type does an expect have? bool
visitor.visit_expr(&loc_condition.value, loc_condition.region, Variable::NULL);
visitor.visit_expr(
&loc_continuation.value,
loc_continuation.region,
Variable::NULL,
);
}
Expr::TypedHole(_) => { /* terminal */ } Expr::TypedHole(_) => { /* terminal */ }
Expr::RuntimeError(..) => { /* terminal */ } Expr::RuntimeError(..) => { /* terminal */ }
} }

View file

@ -520,6 +520,53 @@ pub fn constrain_expr(
constraints.exists_many(vars, all_constraints) constraints.exists_many(vars, all_constraints)
} }
ExpectFx {
loc_condition,
loc_continuation,
lookups_in_cond,
} => {
let expect_bool = |region| {
let bool_type = Type::Variable(Variable::BOOL);
Expected::ForReason(Reason::ExpectCondition, bool_type, region)
};
let cond_con = constrain_expr(
constraints,
env,
loc_condition.region,
&loc_condition.value,
expect_bool(loc_condition.region),
);
let continuation_con = constrain_expr(
constraints,
env,
loc_continuation.region,
&loc_continuation.value,
expected,
);
// + 2 for cond_con and continuation_con
let mut all_constraints = Vec::with_capacity(lookups_in_cond.len() + 2);
all_constraints.push(cond_con);
all_constraints.push(continuation_con);
let mut vars = Vec::with_capacity(lookups_in_cond.len());
for (symbol, var) in lookups_in_cond.iter() {
vars.push(*var);
all_constraints.push(constraints.lookup(
*symbol,
NoExpectation(Type::Variable(*var)),
Region::zero(),
));
}
constraints.exists_many(vars, all_constraints)
}
If { If {
cond_var, cond_var,
branch_var, branch_var,

View file

@ -1004,7 +1004,8 @@ trait Backend<'a> {
} }
} }
Stmt::Expect { .. } => todo!("expect is not implemented in the wasm backend"), Stmt::Expect { .. } => todo!("expect is not implemented in the dev backend"),
Stmt::ExpectFx { .. } => todo!("expect-fx is not implemented in the dev backend"),
Stmt::RuntimeError(_) => {} Stmt::RuntimeError(_) => {}
} }

View file

@ -199,10 +199,6 @@ impl LlvmBackendMode {
LlvmBackendMode::CliTest => true, LlvmBackendMode::CliTest => true,
} }
} }
fn runs_expects_in_separate_process(self) -> bool {
false
}
} }
pub struct Env<'a, 'ctx, 'env> { pub struct Env<'a, 'ctx, 'env> {
@ -2802,15 +2798,67 @@ pub fn build_exp_stmt<'a, 'ctx, 'env>(
lookups, lookups,
); );
// NOTE: signals to the parent process that an expect failed bd.build_unconditional_branch(then_block);
if env.mode.runs_expects_in_separate_process() { }
let func = env roc_target::PtrWidth::Bytes4 => {
.module // temporary WASM implementation
.get_function(bitcode::UTILS_EXPECT_FAILED_FINALIZE) throw_exception(env, "An expectation failed!");
.unwrap(); }
}
} else {
bd.position_at_end(throw_block);
bd.build_unconditional_branch(then_block);
}
bd.build_call(func, &[], "call_expect_finalize_failed"); bd.position_at_end(then_block);
}
build_exp_stmt(
env,
layout_ids,
func_spec_solutions,
scope,
parent,
remainder,
)
}
ExpectFx {
condition: cond_symbol,
region,
lookups,
layouts: _,
remainder,
} => {
let bd = env.builder;
let context = env.context;
let (cond, _cond_layout) = load_symbol_and_layout(scope, cond_symbol);
let condition = bd.build_int_compare(
IntPredicate::EQ,
cond.into_int_value(),
context.bool_type().const_int(1, false),
"is_true",
);
let then_block = context.append_basic_block(parent, "then_block");
let throw_block = context.append_basic_block(parent, "throw_block");
bd.build_conditional_branch(condition, then_block, throw_block);
if env.mode.runs_expects() {
bd.position_at_end(throw_block);
match env.target_info.ptr_width() {
roc_target::PtrWidth::Bytes8 => {
clone_to_shared_memory(
env,
scope,
layout_ids,
*cond_symbol,
*region,
lookups,
);
bd.build_unconditional_branch(then_block); bd.build_unconditional_branch(then_block);
} }

View file

@ -708,6 +708,7 @@ impl<'a> WasmBackend<'a> {
Stmt::Refcounting(modify, following) => self.stmt_refcounting(modify, following), Stmt::Refcounting(modify, following) => self.stmt_refcounting(modify, following),
Stmt::Expect { .. } => todo!("expect is not implemented in the wasm backend"), Stmt::Expect { .. } => todo!("expect is not implemented in the wasm backend"),
Stmt::ExpectFx { .. } => todo!("expect-fx is not implemented in the wasm backend"),
Stmt::RuntimeError(msg) => self.stmt_runtime_error(msg), Stmt::RuntimeError(msg) => self.stmt_runtime_error(msg),
} }

View file

@ -707,6 +707,12 @@ struct LateSpecializationsModule<'a> {
procs_base: ProcsBase<'a>, procs_base: ProcsBase<'a>,
} }
#[derive(Debug, Default)]
pub struct ToplevelExpects {
pub pure: VecMap<Symbol, Region>,
pub fx: VecMap<Symbol, Region>,
}
#[derive(Debug)] #[derive(Debug)]
pub struct MonomorphizedModule<'a> { pub struct MonomorphizedModule<'a> {
pub module_id: ModuleId, pub module_id: ModuleId,
@ -716,7 +722,7 @@ pub struct MonomorphizedModule<'a> {
pub can_problems: MutMap<ModuleId, Vec<roc_problem::can::Problem>>, pub can_problems: MutMap<ModuleId, Vec<roc_problem::can::Problem>>,
pub type_problems: MutMap<ModuleId, Vec<TypeError>>, pub type_problems: MutMap<ModuleId, Vec<TypeError>>,
pub procedures: MutMap<(Symbol, ProcLayout<'a>), Proc<'a>>, pub procedures: MutMap<(Symbol, ProcLayout<'a>), Proc<'a>>,
pub toplevel_expects: VecMap<Symbol, Region>, pub toplevel_expects: ToplevelExpects,
pub entry_point: EntryPoint<'a>, pub entry_point: EntryPoint<'a>,
pub exposed_to_host: ExposedToHost, pub exposed_to_host: ExposedToHost,
pub sources: MutMap<ModuleId, (PathBuf, Box<str>)>, pub sources: MutMap<ModuleId, (PathBuf, Box<str>)>,
@ -821,7 +827,7 @@ enum Msg<'a> {
solved_subs: Solved<Subs>, solved_subs: Solved<Subs>,
module_timing: ModuleTiming, module_timing: ModuleTiming,
abilities_store: AbilitiesStore, abilities_store: AbilitiesStore,
toplevel_expects: VecMap<Symbol, Region>, toplevel_expects: ToplevelExpects,
}, },
MadeSpecializations { MadeSpecializations {
module_id: ModuleId, module_id: ModuleId,
@ -908,7 +914,7 @@ struct State<'a> {
pub module_cache: ModuleCache<'a>, pub module_cache: ModuleCache<'a>,
pub dependencies: Dependencies<'a>, pub dependencies: Dependencies<'a>,
pub procedures: MutMap<(Symbol, ProcLayout<'a>), Proc<'a>>, pub procedures: MutMap<(Symbol, ProcLayout<'a>), Proc<'a>>,
pub toplevel_expects: VecMap<Symbol, Region>, pub toplevel_expects: ToplevelExpects,
pub exposed_to_host: ExposedToHost, pub exposed_to_host: ExposedToHost,
/// This is the "final" list of IdentIds, after canonicalization and constraint gen /// This is the "final" list of IdentIds, after canonicalization and constraint gen
@ -981,7 +987,7 @@ impl<'a> State<'a> {
module_cache: ModuleCache::default(), module_cache: ModuleCache::default(),
dependencies, dependencies,
procedures: MutMap::default(), procedures: MutMap::default(),
toplevel_expects: VecMap::default(), toplevel_expects: ToplevelExpects::default(),
exposed_to_host: ExposedToHost::default(), exposed_to_host: ExposedToHost::default(),
exposed_types, exposed_types,
arc_modules, arc_modules,
@ -2527,7 +2533,8 @@ fn update<'a>(
let subs = solved_subs.into_inner(); let subs = solved_subs.into_inner();
state.toplevel_expects.extend(toplevel_expects); state.toplevel_expects.pure.extend(toplevel_expects.pure);
state.toplevel_expects.fx.extend(toplevel_expects.fx);
state state
.module_cache .module_cache
@ -4818,7 +4825,7 @@ fn build_pending_specializations<'a>(
let find_specializations_start = Instant::now(); let find_specializations_start = Instant::now();
let mut module_thunks = bumpalo::collections::Vec::new_in(arena); let mut module_thunks = bumpalo::collections::Vec::new_in(arena);
let mut toplevel_expects = VecMap::default(); let mut toplevel_expects = ToplevelExpects::default();
let mut procs_base = ProcsBase { let mut procs_base = ProcsBase {
partial_procs: BumpMap::default(), partial_procs: BumpMap::default(),
@ -5111,7 +5118,7 @@ fn build_pending_specializations<'a>(
); );
} }
let body = roc_can::expr::toplevel_expect_to_inline_expect(body); let body = roc_can::expr::toplevel_expect_to_inline_expect_pure(body);
let proc = PartialProc { let proc = PartialProc {
annotation: expr_var, annotation: expr_var,
@ -5131,7 +5138,7 @@ fn build_pending_specializations<'a>(
let expr_region = declarations.expressions[index].region; let expr_region = declarations.expressions[index].region;
let region = Region::span_across(&name_region, &expr_region); let region = Region::span_across(&name_region, &expr_region);
toplevel_expects.insert(symbol, region); toplevel_expects.pure.insert(symbol, region);
procs_base.partial_procs.insert(symbol, proc); procs_base.partial_procs.insert(symbol, proc);
} }
ExpectationFx => { ExpectationFx => {
@ -5187,7 +5194,7 @@ fn build_pending_specializations<'a>(
); );
} }
let body = roc_can::expr::toplevel_expect_to_inline_expect(body); let body = roc_can::expr::toplevel_expect_to_inline_expect_fx(body);
let proc = PartialProc { let proc = PartialProc {
annotation: expr_var, annotation: expr_var,
@ -5207,7 +5214,7 @@ fn build_pending_specializations<'a>(
let expr_region = declarations.expressions[index].region; let expr_region = declarations.expressions[index].region;
let region = Region::span_across(&name_region, &expr_region); let region = Region::span_across(&name_region, &expr_region);
toplevel_expects.insert(symbol, region); toplevel_expects.fx.insert(symbol, region);
procs_base.partial_procs.insert(symbol, proc); procs_base.partial_procs.insert(symbol, proc);
} }
} }

View file

@ -309,6 +309,7 @@ impl<'a> ParamMap<'a> {
} }
Expect { remainder, .. } => stack.push(remainder), Expect { remainder, .. } => stack.push(remainder),
ExpectFx { remainder, .. } => stack.push(remainder),
Switch { Switch {
branches, branches,
@ -820,6 +821,10 @@ impl<'a> BorrowInfState<'a> {
self.collect_stmt(param_map, remainder); self.collect_stmt(param_map, remainder);
} }
ExpectFx { remainder, .. } => {
self.collect_stmt(param_map, remainder);
}
Refcounting(_, _) => unreachable!("these have not been introduced yet"), Refcounting(_, _) => unreachable!("these have not been introduced yet"),
Ret(_) | RuntimeError(_) => { Ret(_) | RuntimeError(_) => {
@ -988,6 +993,7 @@ fn call_info_stmt<'a>(arena: &'a Bump, stmt: &Stmt<'a>, info: &mut CallInfo<'a>)
} }
Expect { remainder, .. } => stack.push(remainder), Expect { remainder, .. } => stack.push(remainder),
ExpectFx { remainder, .. } => stack.push(remainder),
Refcounting(_, _) => unreachable!("these have not been introduced yet"), Refcounting(_, _) => unreachable!("these have not been introduced yet"),

View file

@ -119,6 +119,17 @@ pub fn occurring_variables(stmt: &Stmt<'_>) -> (MutSet<Symbol>, MutSet<Symbol>)
stack.push(remainder); stack.push(remainder);
} }
ExpectFx {
condition,
remainder,
lookups,
..
} => {
result.insert(*condition);
result.extend(lookups.iter().copied());
stack.push(remainder);
}
Jump(_, arguments) => { Jump(_, arguments) => {
result.extend(arguments.iter().copied()); result.extend(arguments.iter().copied());
} }
@ -671,8 +682,8 @@ impl<'a> Context<'a> {
match ownership { match ownership {
DataBorrowedFunctionOwns => { DataBorrowedFunctionOwns => {
// the data is borrowed; // the data is borrowed; increment it to own the values so the function
// increment it to own the values so the function can use them // can use them
let rc = Stmt::Refcounting(ModifyRc::Inc(argument, 1), stmt); let rc = Stmt::Refcounting(ModifyRc::Inc(argument, 1), stmt);
stmt = self.arena.alloc(rc); stmt = self.arena.alloc(rc);
@ -690,8 +701,8 @@ impl<'a> Context<'a> {
}}; }};
} }
// incrementing/consuming the closure (if needed) is done by the zig implementation. // incrementing/consuming the closure (if needed) is done by the zig implementation. We
// We don't want to touch the RC on the roc side, so treat these as borrowed. // don't want to touch the RC on the roc side, so treat these as borrowed.
const FUNCTION: bool = BORROWED; const FUNCTION: bool = BORROWED;
const CLOSURE_DATA: bool = BORROWED; const CLOSURE_DATA: bool = BORROWED;
@ -753,9 +764,9 @@ impl<'a> Context<'a> {
handle_ownerships_pre!(Stmt::Let(z, v, l, b), ownerships) handle_ownerships_pre!(Stmt::Let(z, v, l, b), ownerships)
} }
ListSortWith { xs } => { ListSortWith { xs } => {
// NOTE: we may apply the function to the same argument multiple times. // NOTE: we may apply the function to the same argument multiple times. for that to
// for that to be valid, the function must borrow its argument. This is not // be valid, the function must borrow its argument. This is not enforced at the
// enforced at the moment // moment
// //
// we also don't check that both arguments have the same ownership characteristics // we also don't check that both arguments have the same ownership characteristics
let ownerships = [(xs, function_ps[0])]; let ownerships = [(xs, function_ps[0])];
@ -768,7 +779,8 @@ impl<'a> Context<'a> {
match ownership { match ownership {
DataOwnedFunctionOwns => { DataOwnedFunctionOwns => {
// if non-unique, elements have been consumed, must still consume the list itself // if non-unique, elements have been consumed, must still consume the
// list itself
let rc = Stmt::Refcounting(ModifyRc::DecRef(xs), b); let rc = Stmt::Refcounting(ModifyRc::DecRef(xs), b);
let condition_stmt = branch_on_list_uniqueness( let condition_stmt = branch_on_list_uniqueness(
@ -914,8 +926,7 @@ impl<'a> Context<'a> {
} }
EmptyArray | Literal(_) | Reset { .. } | RuntimeErrorFunction(_) => { EmptyArray | Literal(_) | Reset { .. } | RuntimeErrorFunction(_) => {
// EmptyArray is always stack-allocated // EmptyArray is always stack-allocated function pointers are persistent
// function pointers are persistent
self.arena.alloc(Stmt::Let(z, v, l, b)) self.arena.alloc(Stmt::Let(z, v, l, b))
} }
}; };
@ -924,8 +935,7 @@ impl<'a> Context<'a> {
} }
fn update_var_info(&self, symbol: Symbol, layout: &Layout<'a>, expr: &Expr<'a>) -> Self { fn update_var_info(&self, symbol: Symbol, layout: &Layout<'a>, expr: &Expr<'a>) -> Self {
// is this value a constant? // is this value a constant? TODO do function pointers also fall into this category?
// TODO do function pointers also fall into this category?
let persistent = false; let persistent = false;
// must this value be consumed? // must this value be consumed?
@ -979,9 +989,7 @@ impl<'a> Context<'a> {
// Add `dec` instructions for parameters that are // Add `dec` instructions for parameters that are
// //
// - references // - references - not alive in `b` - not borrow.
// - not alive in `b`
// - not borrow.
// //
// That is, we must make sure these parameters are consumed. // That is, we must make sure these parameters are consumed.
fn add_dec_for_dead_params( fn add_dec_for_dead_params(
@ -1021,9 +1029,9 @@ impl<'a> Context<'a> {
) -> (&'a Stmt<'a>, LiveVarSet) { ) -> (&'a Stmt<'a>, LiveVarSet) {
use Stmt::*; use Stmt::*;
// let-chains can be very long, especially for large (list) literals // let-chains can be very long, especially for large (list) literals in (rust) debug mode,
// in (rust) debug mode, this function can overflow the stack for such values // this function can overflow the stack for such values so we have to write an explicit
// so we have to write an explicit loop. // loop.
{ {
let mut cont = stmt; let mut cont = stmt;
let mut triples = Vec::new_in(self.arena); let mut triples = Vec::new_in(self.arena);
@ -1199,6 +1207,30 @@ impl<'a> Context<'a> {
(expect, b_live_vars) (expect, b_live_vars)
} }
ExpectFx {
remainder,
condition,
region,
lookups,
layouts,
} => {
let (b, mut b_live_vars) = self.visit_stmt(codegen, remainder);
let expect = self.arena.alloc(Stmt::ExpectFx {
condition: *condition,
region: *region,
lookups,
layouts,
remainder: b,
});
let expect = self.add_inc_before_consume_all(lookups, expect, &b_live_vars);
b_live_vars.extend(lookups.iter().copied());
(expect, b_live_vars)
}
RuntimeError(_) | Refcounting(_, _) => (stmt, MutSet::default()), RuntimeError(_) | Refcounting(_, _) => (stmt, MutSet::default()),
} }
} }
@ -1313,6 +1345,17 @@ pub fn collect_stmt(
collect_stmt(remainder, jp_live_vars, vars) collect_stmt(remainder, jp_live_vars, vars)
} }
ExpectFx {
condition,
remainder,
lookups,
..
} => {
vars.insert(*condition);
vars.extend(lookups.iter().copied());
collect_stmt(remainder, jp_live_vars, vars)
}
Join { Join {
id: j, id: j,
parameters, parameters,

View file

@ -1513,6 +1513,14 @@ pub enum Stmt<'a> {
/// what happens after the expect /// what happens after the expect
remainder: &'a Stmt<'a>, remainder: &'a Stmt<'a>,
}, },
ExpectFx {
condition: Symbol,
region: Region,
lookups: &'a [Symbol],
layouts: &'a [Layout<'a>],
/// what happens after the expect
remainder: &'a Stmt<'a>,
},
/// a join point `join f <params> = <continuation> in remainder` /// a join point `join f <params> = <continuation> in remainder`
Join { Join {
id: JoinPointId, id: JoinPointId,
@ -2100,6 +2108,17 @@ impl<'a> Stmt<'a> {
.append(alloc.hardline()) .append(alloc.hardline())
.append(remainder.to_doc(alloc)), .append(remainder.to_doc(alloc)),
ExpectFx {
condition,
remainder,
..
} => alloc
.text("expect-fx ")
.append(symbol_to_doc(alloc, *condition))
.append(";")
.append(alloc.hardline())
.append(remainder.to_doc(alloc)),
Ret(symbol) => alloc Ret(symbol) => alloc
.text("ret ") .text("ret ")
.append(symbol_to_doc(alloc, *symbol)) .append(symbol_to_doc(alloc, *symbol))
@ -4163,6 +4182,7 @@ pub fn with_hole<'a>(
EmptyRecord => let_empty_struct(assigned, hole), EmptyRecord => let_empty_struct(assigned, hole),
Expect { .. } => unreachable!("I think this is unreachable"), Expect { .. } => unreachable!("I think this is unreachable"),
ExpectFx { .. } => unreachable!("I think this is unreachable"),
If { If {
cond_var, cond_var,
@ -6133,6 +6153,45 @@ pub fn from_can<'a>(
stmt stmt
} }
ExpectFx {
loc_condition,
loc_continuation,
lookups_in_cond,
} => {
let rest = from_can(env, variable, loc_continuation.value, procs, layout_cache);
let cond_symbol = env.unique_symbol();
let lookups = Vec::from_iter_in(lookups_in_cond.iter().map(|t| t.0), env.arena);
let mut layouts = Vec::with_capacity_in(lookups_in_cond.len(), env.arena);
for (_, var) in lookups_in_cond {
let res_layout = layout_cache.from_var(env.arena, var, env.subs);
let layout = return_on_layout_error!(env, res_layout, "Expect");
layouts.push(layout);
}
let mut stmt = Stmt::ExpectFx {
condition: cond_symbol,
region: loc_condition.region,
lookups: lookups.into_bump_slice(),
layouts: layouts.into_bump_slice(),
remainder: env.arena.alloc(rest),
};
stmt = with_hole(
env,
loc_condition.value,
variable,
procs,
layout_cache,
cond_symbol,
env.arena.alloc(stmt),
);
stmt
}
LetRec(defs, cont, _cycle_mark) => { LetRec(defs, cont, _cycle_mark) => {
// because Roc is strict, only functions can be recursive! // because Roc is strict, only functions can be recursive!
for def in defs.into_iter() { for def in defs.into_iter() {
@ -6497,6 +6556,32 @@ fn substitute_in_stmt_help<'a>(
Some(arena.alloc(expect)) Some(arena.alloc(expect))
} }
ExpectFx {
condition,
region,
lookups,
layouts,
remainder,
} => {
let new_remainder =
substitute_in_stmt_help(arena, remainder, subs).unwrap_or(remainder);
let new_lookups = Vec::from_iter_in(
lookups.iter().map(|s| substitute(subs, *s).unwrap_or(*s)),
arena,
);
let expect = ExpectFx {
condition: substitute(subs, *condition).unwrap_or(*condition),
region: *region,
lookups: new_lookups.into_bump_slice(),
layouts,
remainder: new_remainder,
};
Some(arena.alloc(expect))
}
Jump(id, args) => { Jump(id, args) => {
let mut did_change = false; let mut did_change = false;
let new_args = Vec::from_iter_in( let new_args = Vec::from_iter_in(

View file

@ -216,6 +216,31 @@ fn function_s<'a, 'i>(
} }
} }
ExpectFx {
condition,
region,
lookups,
layouts,
remainder,
} => {
let continuation: &Stmt = *remainder;
let new_continuation = function_s(env, w, c, continuation);
if std::ptr::eq(continuation, new_continuation) || continuation == new_continuation {
stmt
} else {
let new_refcounting = ExpectFx {
condition: *condition,
region: *region,
lookups,
layouts,
remainder: new_continuation,
};
arena.alloc(new_refcounting)
}
}
Ret(_) | Jump(_, _) | RuntimeError(_) => stmt, Ret(_) | Jump(_, _) | RuntimeError(_) => stmt,
} }
} }
@ -446,6 +471,39 @@ fn function_d_main<'a, 'i>(
(arena.alloc(refcounting), found) (arena.alloc(refcounting), found)
} }
} }
ExpectFx {
condition,
region,
lookups,
layouts,
remainder,
} => {
let (b, found) = function_d_main(env, x, c, remainder);
if found || *condition != x {
let refcounting = ExpectFx {
condition: *condition,
region: *region,
lookups,
layouts,
remainder: b,
};
(arena.alloc(refcounting), found)
} else {
let b = try_function_s(env, x, c, b);
let refcounting = ExpectFx {
condition: *condition,
region: *region,
lookups,
layouts,
remainder: b,
};
(arena.alloc(refcounting), found)
}
}
Join { Join {
id, id,
parameters, parameters,
@ -618,6 +676,26 @@ fn function_r<'a, 'i>(env: &mut Env<'a, 'i>, stmt: &'a Stmt<'a>) -> &'a Stmt<'a>
arena.alloc(expect) arena.alloc(expect)
} }
ExpectFx {
condition,
region,
lookups,
layouts,
remainder,
} => {
let b = function_r(env, remainder);
let expect = ExpectFx {
condition: *condition,
region: *region,
lookups,
layouts,
remainder: b,
};
arena.alloc(expect)
}
Ret(_) | Jump(_, _) | RuntimeError(_) => { Ret(_) | Jump(_, _) | RuntimeError(_) => {
// terminals // terminals
stmt stmt
@ -653,6 +731,11 @@ fn has_live_var<'a>(jp_live_vars: &JPLiveVarMap, stmt: &'a Stmt<'a>, needle: Sym
remainder, remainder,
.. ..
} => *condition == needle || has_live_var(jp_live_vars, remainder, needle), } => *condition == needle || has_live_var(jp_live_vars, remainder, needle),
ExpectFx {
condition,
remainder,
..
} => *condition == needle || has_live_var(jp_live_vars, remainder, needle),
Join { Join {
id, id,
parameters, parameters,

View file

@ -273,6 +273,30 @@ fn insert_jumps<'a>(
None => None, None => None,
}, },
ExpectFx {
condition,
region,
lookups,
layouts,
remainder,
} => match insert_jumps(
arena,
remainder,
goal_id,
needle,
needle_arguments,
needle_result,
) {
Some(cont) => Some(arena.alloc(ExpectFx {
condition: *condition,
region: *region,
lookups,
layouts,
remainder: cont,
})),
None => None,
},
Ret(_) => None, Ret(_) => None,
Jump(_, _) => None, Jump(_, _) => None,
RuntimeError(_) => None, RuntimeError(_) => None,

View file

@ -636,7 +636,7 @@ fn parse_defs_end<'a>(
let parse_expect_vanilla = let parse_expect_vanilla =
crate::parser::keyword_e(crate::keyword::EXPECT, EExpect::Expect); crate::parser::keyword_e(crate::keyword::EXPECT, EExpect::Expect);
let parse_expect_fx = crate::parser::keyword_e(crate::keyword::EXPECT_FX, EExpect::Expect); let parse_expect_fx = crate::parser::keyword_e(crate::keyword::EXPECT_FX, EExpect::Expect);
let parse_expect = either!(parse_expect_vanilla, parse_expect_fx); let parse_expect = either!(parse_expect_fx, parse_expect_vanilla);
match space0_after_e( match space0_after_e(
crate::pattern::loc_pattern_help(min_indent), crate::pattern::loc_pattern_help(min_indent),
@ -682,11 +682,11 @@ fn parse_defs_end<'a>(
let preceding_comment = Region::new(spaces_before_current_start, start); let preceding_comment = Region::new(spaces_before_current_start, start);
let value_def = match expect_flavor { let value_def = match expect_flavor {
Either::First(_) => ValueDef::Expect { Either::Second(_) => ValueDef::Expect {
condition: arena.alloc(loc_def_expr), condition: arena.alloc(loc_def_expr),
preceding_comment, preceding_comment,
}, },
Either::Second(_) => ValueDef::ExpectFx { Either::First(_) => ValueDef::ExpectFx {
condition: arena.alloc(loc_def_expr), condition: arena.alloc(loc_def_expr),
preceding_comment, preceding_comment,
}, },

View file

@ -257,6 +257,7 @@ fn expr<'a>(c: &Ctx, p: EPrec, f: &'a Arena<'a>, e: &'a Expr) -> DocBuilder<'a,
ZeroArgumentTag { .. } => todo!(), ZeroArgumentTag { .. } => todo!(),
OpaqueRef { .. } => todo!(), OpaqueRef { .. } => todo!(),
Expect { .. } => todo!(), Expect { .. } => todo!(),
ExpectFx { .. } => todo!(),
TypedHole(_) => todo!(), TypedHole(_) => todo!(),
RuntimeError(_) => todo!(), RuntimeError(_) => todo!(),
} }

View file

@ -26,10 +26,11 @@ roc_build = { path = "../compiler/build" }
libloading = "0.7.1" libloading = "0.7.1"
inkwell = { path = "../vendor/inkwell" } inkwell = { path = "../vendor/inkwell" }
signal-hook = "0.3.14"
libc = "0.2.106"
[dev-dependencies] [dev-dependencies]
test_gen = { path = "../compiler/test_gen" } test_gen = { path = "../compiler/test_gen" }
libc = "0.2.106"
roc_build = { path = "../compiler/build", features = ["target-aarch64", "target-x86_64"] } roc_build = { path = "../compiler/build", features = ["target-aarch64", "target-x86_64"] }
tempfile = "3.2.0" tempfile = "3.2.0"
indoc = "1.0.7" indoc = "1.0.7"

View file

@ -141,6 +141,7 @@ mod test {
const BUFFER_SIZE: usize = 1024; const BUFFER_SIZE: usize = 1024;
let mut shared_buffer = [0u8; BUFFER_SIZE]; let mut shared_buffer = [0u8; BUFFER_SIZE];
let mut memory = crate::run::ExpectMemory::from_slice(&mut shared_buffer);
// communicate the mmapped name to zig/roc // communicate the mmapped name to zig/roc
let set_shared_buffer = run_roc_dylib!(lib, "set_shared_buffer", (*mut u8, usize), ()); let set_shared_buffer = run_roc_dylib!(lib, "set_shared_buffer", (*mut u8, usize), ());
@ -148,15 +149,15 @@ mod test {
unsafe { set_shared_buffer((shared_buffer.as_mut_ptr(), BUFFER_SIZE), &mut result) }; unsafe { set_shared_buffer((shared_buffer.as_mut_ptr(), BUFFER_SIZE), &mut result) };
let mut writer = Vec::with_capacity(1024); let mut writer = Vec::with_capacity(1024);
let (_failed, _passed) = crate::run::run_expects( let (_failed, _passed) = crate::run::run_expects_with_memory(
&mut writer, &mut writer,
RenderTarget::ColorTerminal, RenderTarget::ColorTerminal,
arena, arena,
interns, interns,
&lib, &lib,
&mut expectations, &mut expectations,
shared_buffer.as_mut_ptr(),
expects, expects,
&mut memory,
) )
.unwrap(); .unwrap();

View file

@ -1,9 +1,15 @@
use std::os::unix::process::parent_id;
use bumpalo::collections::Vec as BumpVec; use bumpalo::collections::Vec as BumpVec;
use bumpalo::Bump; use bumpalo::Bump;
use inkwell::context::Context; use inkwell::context::Context;
use roc_build::link::llvm_module_to_dylib; use roc_build::link::llvm_module_to_dylib;
use roc_collections::{MutSet, VecMap}; use roc_collections::{MutSet, VecMap};
use roc_gen_llvm::llvm::{build::LlvmBackendMode, externs::add_default_roc_externs}; use roc_gen_llvm::{
llvm::{build::LlvmBackendMode, externs::add_default_roc_externs},
run_roc::RocCallResult,
run_roc_dylib,
};
use roc_load::{EntryPoint, Expectations, MonomorphizedModule}; use roc_load::{EntryPoint, Expectations, MonomorphizedModule};
use roc_module::symbol::{Interns, ModuleId, Symbol}; use roc_module::symbol::{Interns, ModuleId, Symbol};
use roc_mono::ir::OptLevel; use roc_mono::ir::OptLevel;
@ -12,6 +18,67 @@ use roc_reporting::{error::expect::Renderer, report::RenderTarget};
use roc_target::TargetInfo; use roc_target::TargetInfo;
use target_lexicon::Triple; use target_lexicon::Triple;
pub(crate) struct ExpectMemory<'a> {
ptr: *mut u8,
length: usize,
shm_name: Option<std::ffi::CString>,
_marker: std::marker::PhantomData<&'a ()>,
}
impl<'a> ExpectMemory<'a> {
const SHM_SIZE: usize = 1024;
#[cfg(test)]
pub(crate) fn from_slice(slice: &mut [u8]) -> Self {
Self {
ptr: slice.as_mut_ptr(),
length: slice.len(),
shm_name: None,
_marker: std::marker::PhantomData,
}
}
fn create_or_reuse_mmap(shm_name: &str) -> Self {
let cstring = std::ffi::CString::new(shm_name).unwrap();
Self::mmap_help(cstring, libc::O_RDWR | libc::O_CREAT)
}
fn reuse_mmap(&mut self) -> Option<Self> {
let shm_name = self.shm_name.as_ref()?.clone();
Some(Self::mmap_help(shm_name, libc::O_RDWR))
}
fn mmap_help(cstring: std::ffi::CString, shm_flags: i32) -> Self {
let ptr = unsafe {
let shared_fd = libc::shm_open(cstring.as_ptr().cast(), shm_flags, 0o666);
libc::ftruncate(shared_fd, Self::SHM_SIZE as _);
libc::mmap(
std::ptr::null_mut(),
Self::SHM_SIZE,
libc::PROT_WRITE | libc::PROT_READ,
libc::MAP_SHARED,
shared_fd,
0,
)
};
Self {
ptr: ptr.cast(),
length: Self::SHM_SIZE,
shm_name: Some(cstring),
_marker: std::marker::PhantomData,
}
}
fn set_shared_buffer(&mut self, lib: &libloading::Library) {
let set_shared_buffer = run_roc_dylib!(lib, "set_shared_buffer", (*mut u8, usize), ());
let mut result = RocCallResult::default();
unsafe { set_shared_buffer((self.ptr, self.length), &mut result) };
}
}
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub fn run_expects<W: std::io::Write>( pub fn run_expects<W: std::io::Write>(
writer: &mut W, writer: &mut W,
@ -20,21 +87,66 @@ pub fn run_expects<W: std::io::Write>(
interns: &Interns, interns: &Interns,
lib: &libloading::Library, lib: &libloading::Library,
expectations: &mut VecMap<ModuleId, Expectations>, expectations: &mut VecMap<ModuleId, Expectations>,
shared_ptr: *mut u8, expects: ExpectFunctions<'_>,
expects: bumpalo::collections::Vec<'_, ToplevelExpect<'_>>, ) -> std::io::Result<(usize, usize)> {
let shm_name = format!("/roc_expect_buffer_{}", std::process::id());
let mut memory = ExpectMemory::create_or_reuse_mmap(&shm_name);
run_expects_with_memory(
writer,
render_target,
arena,
interns,
lib,
expectations,
expects,
&mut memory,
)
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn run_expects_with_memory<W: std::io::Write>(
writer: &mut W,
render_target: RenderTarget,
arena: &Bump,
interns: &Interns,
lib: &libloading::Library,
expectations: &mut VecMap<ModuleId, Expectations>,
expects: ExpectFunctions<'_>,
memory: &mut ExpectMemory,
) -> std::io::Result<(usize, usize)> { ) -> std::io::Result<(usize, usize)> {
let mut failed = 0; let mut failed = 0;
let mut passed = 0; let mut passed = 0;
for expect in expects { for expect in expects.fx {
let result = run_expect( let result = run_expect_fx(
writer, writer,
render_target, render_target,
arena, arena,
interns, interns,
lib, lib,
expectations, expectations,
shared_ptr, memory,
expect,
)?;
match result {
true => passed += 1,
false => failed += 1,
}
}
memory.set_shared_buffer(lib);
for expect in expects.pure {
let result = run_expect_pure(
writer,
render_target,
arena,
interns,
lib,
expectations,
memory,
expect, expect,
)?; )?;
@ -48,23 +160,23 @@ pub fn run_expects<W: std::io::Write>(
} }
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
fn run_expect<W: std::io::Write>( fn run_expect_pure<W: std::io::Write>(
writer: &mut W, writer: &mut W,
render_target: RenderTarget, render_target: RenderTarget,
arena: &Bump, arena: &Bump,
interns: &Interns, interns: &Interns,
lib: &libloading::Library, lib: &libloading::Library,
expectations: &mut VecMap<ModuleId, Expectations>, expectations: &mut VecMap<ModuleId, Expectations>,
shared_ptr: *mut u8, shared_memory: &mut ExpectMemory,
expect: ToplevelExpect<'_>, expect: ToplevelExpect<'_>,
) -> std::io::Result<bool> { ) -> std::io::Result<bool> {
use roc_gen_llvm::try_run_jit_function; use roc_gen_llvm::try_run_jit_function;
let sequence = ExpectSequence::new(shared_ptr.cast()); let sequence = ExpectSequence::new(shared_memory.ptr.cast());
let result: Result<(), String> = try_run_jit_function!(lib, expect.name, (), |v: ()| v); let result: Result<(), String> = try_run_jit_function!(lib, expect.name, (), |v: ()| v);
let shared_memory_ptr: *const u8 = shared_ptr.cast(); let shared_memory_ptr: *const u8 = shared_memory.ptr.cast();
if result.is_err() || sequence.count_failures() > 0 { if result.is_err() || sequence.count_failures() > 0 {
let module_id = expect.symbol.module_id(); let module_id = expect.symbol.module_id();
@ -103,6 +215,104 @@ fn run_expect<W: std::io::Write>(
} }
} }
#[allow(clippy::too_many_arguments)]
fn run_expect_fx<W: std::io::Write>(
writer: &mut W,
render_target: RenderTarget,
arena: &Bump,
interns: &Interns,
lib: &libloading::Library,
expectations: &mut VecMap<ModuleId, Expectations>,
parent_memory: &mut ExpectMemory,
expect: ToplevelExpect<'_>,
) -> std::io::Result<bool> {
use signal_hook::{consts::signal::SIGCHLD, consts::signal::SIGUSR1, iterator::Signals};
let mut signals = Signals::new(&[SIGCHLD, SIGUSR1]).unwrap();
match unsafe { libc::fork() } {
0 => unsafe {
// we are the child
use roc_gen_llvm::try_run_jit_function;
let mut child_memory = parent_memory.reuse_mmap().unwrap();
let sequence = ExpectSequence::new(child_memory.ptr);
child_memory.set_shared_buffer(lib);
let result: Result<(), String> = try_run_jit_function!(lib, expect.name, (), |v: ()| v);
if let Err(msg) = result {
panic!("roc panic {}", msg);
}
if sequence.count_failures() > 0 {
libc::kill(parent_id() as _, SIGUSR1);
}
std::process::exit(0)
},
-1 => {
// something failed
// Display a human-friendly error message
println!("Error {:?}", std::io::Error::last_os_error());
std::process::exit(1)
}
1.. => {
let mut has_succeeded = true;
for sig in &mut signals {
match sig {
SIGCHLD => {
// done!
return Ok(has_succeeded);
}
SIGUSR1 => {
// this is the signal we use for an expect failure. Let's see what the child told us
has_succeeded = false;
let frame =
ExpectFrame::at_offset(parent_memory.ptr, ExpectSequence::START_OFFSET);
let module_id = frame.module_id;
let data = expectations.get_mut(&module_id).unwrap();
let filename = data.path.to_owned();
let source = std::fs::read_to_string(&data.path).unwrap();
let renderer = Renderer::new(
arena,
interns,
render_target,
module_id,
filename,
&source,
);
render_expect_failure(
writer,
&renderer,
arena,
None,
expectations,
interns,
parent_memory.ptr,
ExpectSequence::START_OFFSET,
)?;
}
_ => println!("received signal {}", sig),
}
}
Ok(true)
}
_ => unreachable!(),
}
}
pub fn roc_dev_expect( pub fn roc_dev_expect(
writer: &mut impl std::io::Write, writer: &mut impl std::io::Write,
arena: &Bump, arena: &Bump,
@ -205,8 +415,8 @@ impl ExpectSequence {
fn new(ptr: *mut u8) -> Self { fn new(ptr: *mut u8) -> Self {
unsafe { unsafe {
let ptr = ptr as *mut usize; let ptr = ptr as *mut usize;
*ptr.add(Self::COUNT_INDEX) = 0; std::ptr::write_unaligned(ptr.add(Self::COUNT_INDEX), 0);
*ptr.add(Self::OFFSET_INDEX) = Self::START_OFFSET; std::ptr::write_unaligned(ptr.add(Self::OFFSET_INDEX), Self::START_OFFSET);
} }
Self { Self {
@ -251,13 +461,19 @@ pub struct ToplevelExpect<'a> {
pub region: Region, pub region: Region,
} }
#[derive(Debug)]
pub struct ExpectFunctions<'a> {
pub pure: BumpVec<'a, ToplevelExpect<'a>>,
pub fx: BumpVec<'a, ToplevelExpect<'a>>,
}
pub fn expect_mono_module_to_dylib<'a>( pub fn expect_mono_module_to_dylib<'a>(
arena: &'a Bump, arena: &'a Bump,
target: Triple, target: Triple,
loaded: MonomorphizedModule<'a>, loaded: MonomorphizedModule<'a>,
opt_level: OptLevel, opt_level: OptLevel,
mode: LlvmBackendMode, mode: LlvmBackendMode,
) -> Result<(libloading::Library, BumpVec<'a, ToplevelExpect<'a>>), libloading::Error> { ) -> Result<(libloading::Library, ExpectFunctions<'a>), libloading::Error> {
let target_info = TargetInfo::from(&target); let target_info = TargetInfo::from(&target);
let MonomorphizedModule { let MonomorphizedModule {
@ -306,18 +522,25 @@ pub fn expect_mono_module_to_dylib<'a>(
EntryPoint::Test => None, EntryPoint::Test => None,
}; };
let capacity = toplevel_expects.pure.len() + toplevel_expects.fx.len();
let mut expect_symbols = BumpVec::with_capacity_in(capacity, env.arena);
expect_symbols.extend(toplevel_expects.pure.keys().copied());
expect_symbols.extend(toplevel_expects.fx.keys().copied());
let expect_names = roc_gen_llvm::llvm::build::build_procedures_expose_expects( let expect_names = roc_gen_llvm::llvm::build::build_procedures_expose_expects(
&env, &env,
opt_level, opt_level,
toplevel_expects.unzip_slices().0, &expect_symbols,
procedures, procedures,
opt_entry_point, opt_entry_point,
); );
let expects = bumpalo::collections::Vec::from_iter_in( let expects_fx = bumpalo::collections::Vec::from_iter_in(
toplevel_expects toplevel_expects
.fx
.into_iter() .into_iter()
.zip(expect_names.into_iter()) .zip(expect_names.iter().skip(toplevel_expects.pure.len()))
.map(|((symbol, region), name)| ToplevelExpect { .map(|((symbol, region), name)| ToplevelExpect {
symbol, symbol,
region, region,
@ -326,6 +549,24 @@ pub fn expect_mono_module_to_dylib<'a>(
env.arena, env.arena,
); );
let expects_pure = bumpalo::collections::Vec::from_iter_in(
toplevel_expects
.pure
.into_iter()
.zip(expect_names.iter())
.map(|((symbol, region), name)| ToplevelExpect {
symbol,
region,
name,
}),
env.arena,
);
let expects = ExpectFunctions {
pure: expects_pure,
fx: expects_fx,
};
env.dibuilder.finalize(); env.dibuilder.finalize();
// we don't use the debug info, and it causes weird errors. // we don't use the debug info, and it causes weird errors.