Merge pull request #5190 from roc-lang/pluggable-glue

Pluggable glue
This commit is contained in:
Folkert de Vries 2023-03-29 23:15:57 +02:00 committed by GitHub
commit 7a77702e78
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 6111 additions and 446 deletions

6
Cargo.lock generated
View file

@ -3427,17 +3427,23 @@ dependencies = [
name = "roc_glue"
version = "0.0.1"
dependencies = [
"backtrace",
"bumpalo",
"cli_utils",
"dircpy",
"fnv",
"indexmap",
"indoc",
"libc",
"libloading",
"pretty_assertions",
"roc_build",
"roc_builtins",
"roc_can",
"roc_collections",
"roc_error_macros",
"roc_gen_llvm",
"roc_linker",
"roc_load",
"roc_module",
"roc_mono",

View file

@ -71,6 +71,7 @@ version = "0.0.1"
inkwell = { git = "https://github.com/roc-lang/inkwell", branch = "inkwell-llvm-15", features = ["llvm13-0"] }
arrayvec = "0.7.2" # update roc_std/Cargo.toml on change
backtrace = "0.3.67"
base64-url = "1.4.13"
bincode = "1.3.3"
bitflags = "1.3.2"

View file

@ -7,10 +7,12 @@ use bumpalo::Bump;
use clap::{Arg, ArgMatches, Command, ValueSource};
use roc_build::link::{LinkType, LinkingStrategy};
use roc_build::program::{
standard_load_config, BuildFileError, BuildOrdering, BuiltFile, CodeGenBackend, CodeGenOptions,
handle_error_module, handle_loading_problem, standard_load_config, BuildFileError,
BuildOrdering, BuiltFile, CodeGenBackend, CodeGenOptions, DEFAULT_ROC_FILENAME,
};
use roc_error_macros::{internal_error, user_error};
use roc_load::{ExpectMetadata, LoadingProblem, Threading};
use roc_gen_llvm::llvm::build::LlvmBackendMode;
use roc_load::{ExpectMetadata, Threading};
use roc_mono::ir::OptLevel;
use roc_packaging::cache::RocCacheDir;
use roc_packaging::tarball::Compression;
@ -33,8 +35,6 @@ use tempfile::TempDir;
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_DEV: &str = "dev";
@ -64,7 +64,8 @@ pub const FLAG_CHECK: &str = "check";
pub const FLAG_WASM_STACK_SIZE_KB: &str = "wasm-stack-size-kb";
pub const ROC_FILE: &str = "ROC_FILE";
pub const ROC_DIR: &str = "ROC_DIR";
pub const GLUE_FILE: &str = "GLUE_FILE";
pub const GLUE_DIR: &str = "GLUE_DIR";
pub const GLUE_SPEC: &str = "GLUE_SPEC";
pub const DIRECTORY_OR_FILES: &str = "DIRECTORY_OR_FILES";
pub const ARGS_FOR_APP: &str = "ARGS_FOR_APP";
@ -279,17 +280,24 @@ pub fn build_app<'a>() -> Command<'a> {
.subcommand(Command::new(CMD_GLUE)
.about("Generate glue code between a platform's Roc API and its host language")
.arg(
Arg::new(ROC_FILE)
.help("The .roc file for the platform module")
Arg::new(GLUE_SPEC)
.help("The specification for how to translate Roc types into output files.")
.allow_invalid_utf8(true)
.required(true)
)
.arg(
Arg::new(GLUE_FILE)
.help("The filename for the generated glue code\n(Currently, this must be a .rs file because only Rust glue generation is supported so far.)")
Arg::new(GLUE_DIR)
.help("The directory for the generated glue code.\nNote: The implementation can write to any file in this directory.")
.allow_invalid_utf8(true)
.required(true)
)
.arg(
Arg::new(ROC_FILE)
.help("The .roc file whose exposed types should be translated.")
.allow_invalid_utf8(true)
.required(false)
.default_value(DEFAULT_ROC_FILENAME)
)
)
.subcommand(Command::new(CMD_GEN_STUB_LIB)
.about("Generate a stubbed shared library that can be used for linking a platform binary.\nThe stubbed library has prototypes, but no function bodies.\n\nNote: This command will be removed in favor of just using `roc build` once all platforms support the surgical linker")
@ -359,7 +367,6 @@ pub fn test(_matches: &ArgMatches, _triple: Triple) -> io::Result<i32> {
#[cfg(not(windows))]
pub fn test(matches: &ArgMatches, triple: Triple) -> io::Result<i32> {
use roc_build::program::report_problems_monomorphized;
use roc_gen_llvm::llvm::build::LlvmBackendMode;
use roc_load::{ExecutionMode, LoadConfig, LoadMonomorphizedError};
use roc_packaging::cache;
use roc_target::TargetInfo;
@ -593,15 +600,6 @@ pub fn build(
// so we don't want to spend time freeing these values
let arena = ManuallyDrop::new(Bump::new());
let code_gen_backend = if matches!(triple.architecture, Architecture::Wasm32) {
CodeGenBackend::Wasm
} else {
match matches.is_present(FLAG_DEV) {
true => CodeGenBackend::Assembly,
false => CodeGenBackend::Llvm,
}
};
let opt_level = if let BuildConfig::BuildAndRunIfNoErrors = config {
OptLevel::Development
} else {
@ -617,6 +615,25 @@ pub fn build(
}
}
};
let code_gen_backend = if matches!(triple.architecture, Architecture::Wasm32) {
CodeGenBackend::Wasm
} else {
match matches.is_present(FLAG_DEV) {
true => CodeGenBackend::Assembly,
false => {
let backend_mode = match opt_level {
OptLevel::Development => LlvmBackendMode::BinaryDev,
OptLevel::Normal | OptLevel::Size | OptLevel::Optimize => {
LlvmBackendMode::Binary
}
};
CodeGenBackend::Llvm(backend_mode)
}
}
};
let emit_debug_info = matches.is_present(FLAG_DEBUG);
let emit_timings = matches.is_present(FLAG_TIME);
@ -759,48 +776,6 @@ pub fn build(
}
}
fn handle_error_module(
mut module: roc_load::LoadedModule,
total_time: std::time::Duration,
filename: &OsStr,
print_run_anyway_hint: bool,
) -> io::Result<i32> {
debug_assert!(module.total_problems() > 0);
let problems = roc_build::program::report_problems_typechecked(&mut module);
problems.print_to_stdout(total_time);
if print_run_anyway_hint {
// If you're running "main.roc" then you can just do `roc run`
// to re-run the program.
print!(".\n\nYou can run the program anyway with \x1B[32mroc run");
if filename != DEFAULT_ROC_FILENAME {
print!(" {}", &filename.to_string_lossy());
}
println!("\x1B[39m");
}
Ok(problems.exit_code())
}
fn handle_loading_problem(problem: LoadingProblem) -> io::Result<i32> {
match problem {
LoadingProblem::FormattedReport(report) => {
print!("{}", report);
Ok(1)
}
_ => {
// TODO: tighten up the types here, we should always end up with a
// formatted report from load.
print!("Failed with error: {:?}", problem);
Ok(1)
}
}
}
fn roc_run<'a, I: IntoIterator<Item = &'a OsStr>>(
arena: &Bump,
opt_level: OptLevel,
@ -1322,40 +1297,3 @@ impl std::str::FromStr for Target {
}
}
}
// These functions don't end up in the final Roc binary but Windows linker needs a definition inside the crate.
// On Windows, there seems to be less dead-code-elimination than on Linux or MacOS, or maybe it's done later.
#[cfg(windows)]
#[allow(unused_imports)]
use windows_roc_platform_functions::*;
#[cfg(windows)]
mod windows_roc_platform_functions {
use core::ffi::c_void;
/// # Safety
/// The Roc application needs this.
#[no_mangle]
pub unsafe fn roc_alloc(size: usize, _alignment: u32) -> *mut c_void {
libc::malloc(size)
}
/// # Safety
/// The Roc application needs this.
#[no_mangle]
pub unsafe fn roc_realloc(
c_ptr: *mut c_void,
new_size: usize,
_old_size: usize,
_alignment: u32,
) -> *mut c_void {
libc::realloc(c_ptr, new_size)
}
/// # Safety
/// The Roc application needs this.
#[no_mangle]
pub unsafe fn roc_dealloc(c_ptr: *mut c_void, _alignment: u32) {
libc::free(c_ptr)
}
}

View file

@ -5,7 +5,7 @@ use roc_cli::{
build_app, format, test, BuildConfig, FormatMode, Target, CMD_BUILD, CMD_CHECK, CMD_DEV,
CMD_DOCS, CMD_EDIT, CMD_FORMAT, CMD_GEN_STUB_LIB, CMD_GLUE, CMD_REPL, CMD_RUN, CMD_TEST,
CMD_VERSION, DIRECTORY_OR_FILES, FLAG_CHECK, FLAG_LIB, FLAG_NO_LINK, FLAG_TARGET, FLAG_TIME,
GLUE_FILE, ROC_FILE,
GLUE_DIR, GLUE_SPEC, ROC_FILE,
};
use roc_docs::generate_docs_html;
use roc_error_macros::user_error;
@ -88,12 +88,13 @@ fn main() -> io::Result<()> {
}
Some((CMD_GLUE, matches)) => {
let input_path = Path::new(matches.value_of_os(ROC_FILE).unwrap());
let output_path = Path::new(matches.value_of_os(GLUE_FILE).unwrap());
let output_path = Path::new(matches.value_of_os(GLUE_DIR).unwrap());
let spec_path = Path::new(matches.value_of_os(GLUE_SPEC).unwrap());
if Some("rs") == output_path.extension().and_then(OsStr::to_str) {
roc_glue::generate(input_path, output_path)
if !output_path.exists() || output_path.is_dir() {
roc_glue::generate(input_path, output_path, spec_path)
} else {
eprintln!("Currently, `roc glue` only supports generating Rust glue files (with the .rs extension). In the future, the plan is to decouple `roc glue` from any particular output format, by having it accept a second .roc file which gets executed as a plugin to generate glue code for any desired language. However, this has not yet been implemented, and for now only .rs is supported.");
eprintln!("`roc glue` must be given a directory to output into, because the glue might generate multiple files.");
Ok(1)
}

View file

@ -14,6 +14,7 @@ use std::io::Write;
use std::path::Path;
use std::path::PathBuf;
use std::process::{Command, ExitStatus, Stdio};
use std::sync::Mutex;
use tempfile::NamedTempFile;
#[derive(Debug)]
@ -87,11 +88,19 @@ pub fn build_roc_bin(extra_args: &[&str]) -> PathBuf {
roc_binary_path
}
// Since glue is always compiling the same plugin, it can not be run in parallel.
// That would lead to a race condition in writing the output shared library.
// Thus, all calls to glue in a test are made sequential.
// TODO: In the future, look into compiling the shared libary once and then caching it.
static GLUE_LOCK: Mutex<()> = Mutex::new(());
pub fn run_glue<I, S>(args: I) -> Out
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
let _guard = GLUE_LOCK.lock().unwrap();
run_roc_with_stdin(&path_to_roc_binary(), args, &[])
}

View file

@ -17,6 +17,7 @@ use roc_reporting::{
report::{RenderTarget, DEFAULT_PALETTE},
};
use roc_target::TargetInfo;
use std::ffi::OsStr;
use std::ops::Deref;
use std::{
path::{Path, PathBuf},
@ -28,6 +29,8 @@ use target_lexicon::Triple;
#[cfg(feature = "target-wasm32")]
use roc_collections::all::MutSet;
pub const DEFAULT_ROC_FILENAME: &str = "main.roc";
#[derive(Debug, Clone, Copy, Default)]
pub struct CodeGenTiming {
pub code_gen: Duration,
@ -72,7 +75,7 @@ impl Deref for CodeObject {
#[derive(Debug, Clone, Copy)]
pub enum CodeGenBackend {
Assembly,
Llvm,
Llvm(LlvmBackendMode),
Wasm,
}
@ -95,6 +98,10 @@ pub fn gen_from_mono_module<'a>(
preprocessed_host_path: &Path,
wasm_dev_stack_bytes: Option<u32>,
) -> GenFromMono<'a> {
let path = roc_file_path;
let debug = code_gen_options.emit_debug_info;
let opt = code_gen_options.opt_level;
match code_gen_options.backend {
CodeGenBackend::Assembly => gen_from_mono_module_dev(
arena,
@ -103,12 +110,18 @@ pub fn gen_from_mono_module<'a>(
preprocessed_host_path,
wasm_dev_stack_bytes,
),
CodeGenBackend::Llvm => {
gen_from_mono_module_llvm(arena, loaded, roc_file_path, target, code_gen_options)
CodeGenBackend::Llvm(backend_mode) => {
gen_from_mono_module_llvm(arena, loaded, path, target, opt, backend_mode, debug)
}
CodeGenBackend::Wasm => {
// emit wasm via the llvm backend
gen_from_mono_module_llvm(arena, loaded, roc_file_path, target, code_gen_options)
let backend_mode = match code_gen_options.opt_level {
OptLevel::Development => LlvmBackendMode::BinaryDev,
OptLevel::Normal | OptLevel::Size | OptLevel::Optimize => LlvmBackendMode::Binary,
};
gen_from_mono_module_llvm(arena, loaded, path, target, opt, backend_mode, debug)
}
}
}
@ -121,7 +134,9 @@ fn gen_from_mono_module_llvm<'a>(
mut loaded: MonomorphizedModule<'a>,
roc_file_path: &Path,
target: &target_lexicon::Triple,
code_gen_options: CodeGenOptions,
opt_level: OptLevel,
backend_mode: LlvmBackendMode,
emit_debug_info: bool,
) -> GenFromMono<'a> {
use crate::target::{self, convert_opt_level};
use inkwell::attributes::{Attribute, AttributeLoc};
@ -171,12 +186,6 @@ fn gen_from_mono_module_llvm<'a>(
}
}
let CodeGenOptions {
backend: _,
opt_level,
emit_debug_info,
} = code_gen_options;
let builder = context.create_builder();
let (dibuilder, compile_unit) = roc_gen_llvm::llvm::build::Env::new_debug_info(module);
let (mpm, _fpm) = roc_gen_llvm::llvm::build::construct_optimization_passes(module, opt_level);
@ -191,10 +200,7 @@ fn gen_from_mono_module_llvm<'a>(
interns: loaded.interns,
module,
target_info,
mode: match opt_level {
OptLevel::Development => LlvmBackendMode::BinaryDev,
OptLevel::Normal | OptLevel::Size | OptLevel::Optimize => LlvmBackendMode::Binary,
},
mode: backend_mode,
exposed_to_host: loaded
.exposed_to_host
@ -652,6 +658,48 @@ impl<'a> BuildFileError<'a> {
}
}
pub fn handle_error_module(
mut module: roc_load::LoadedModule,
total_time: std::time::Duration,
filename: &OsStr,
print_run_anyway_hint: bool,
) -> std::io::Result<i32> {
debug_assert!(module.total_problems() > 0);
let problems = report_problems_typechecked(&mut module);
problems.print_to_stdout(total_time);
if print_run_anyway_hint {
// If you're running "main.roc" then you can just do `roc run`
// to re-run the program.
print!(".\n\nYou can run the program anyway with \x1B[32mroc run");
if filename != DEFAULT_ROC_FILENAME {
print!(" {}", &filename.to_string_lossy());
}
println!("\x1B[39m");
}
Ok(problems.exit_code())
}
pub fn handle_loading_problem(problem: LoadingProblem) -> std::io::Result<i32> {
match problem {
LoadingProblem::FormattedReport(report) => {
print!("{}", report);
Ok(1)
}
_ => {
// TODO: tighten up the types here, we should always end up with a
// formatted report from load.
print!("Failed with error: {:?}", problem);
Ok(1)
}
}
}
pub fn standard_load_config(
target: &Triple,
order: BuildOrdering,
@ -1188,7 +1236,7 @@ pub fn build_str_test<'a>(
let triple = target_lexicon::Triple::host();
let code_gen_options = CodeGenOptions {
backend: CodeGenBackend::Llvm,
backend: CodeGenBackend::Llvm(LlvmBackendMode::Binary),
opt_level: OptLevel::Normal,
emit_debug_info: false,
};

View file

@ -200,6 +200,7 @@ pub enum LlvmBackendMode {
BinaryDev,
/// Creates a test wrapper around the main roc function to catch and report panics.
/// Provides a testing implementation of primitives (roc_alloc, roc_panic, etc)
BinaryGlue,
GenTest,
WasmGenTest,
CliTest,
@ -210,6 +211,7 @@ impl LlvmBackendMode {
match self {
LlvmBackendMode::Binary => true,
LlvmBackendMode::BinaryDev => true,
LlvmBackendMode::BinaryGlue => false,
LlvmBackendMode::GenTest => false,
LlvmBackendMode::WasmGenTest => true,
LlvmBackendMode::CliTest => false,
@ -221,6 +223,7 @@ impl LlvmBackendMode {
match self {
LlvmBackendMode::Binary => false,
LlvmBackendMode::BinaryDev => false,
LlvmBackendMode::BinaryGlue => false,
LlvmBackendMode::GenTest => true,
LlvmBackendMode::WasmGenTest => true,
LlvmBackendMode::CliTest => true,
@ -231,6 +234,7 @@ impl LlvmBackendMode {
match self {
LlvmBackendMode::Binary => false,
LlvmBackendMode::BinaryDev => true,
LlvmBackendMode::BinaryGlue => false,
LlvmBackendMode::GenTest => false,
LlvmBackendMode::WasmGenTest => false,
LlvmBackendMode::CliTest => true,
@ -4116,7 +4120,7 @@ fn expose_function_to_host_help_c_abi<'a, 'ctx, 'env>(
)
}
LlvmBackendMode::Binary | LlvmBackendMode::BinaryDev => {}
LlvmBackendMode::Binary | LlvmBackendMode::BinaryDev | LlvmBackendMode::BinaryGlue => {}
}
// a generic version that writes the result into a passed *u8 pointer
@ -4169,7 +4173,7 @@ fn expose_function_to_host_help_c_abi<'a, 'ctx, 'env>(
roc_call_result_type(env, roc_function.get_type().get_return_type().unwrap()).into()
}
LlvmBackendMode::Binary | LlvmBackendMode::BinaryDev => {
LlvmBackendMode::Binary | LlvmBackendMode::BinaryDev | LlvmBackendMode::BinaryGlue => {
basic_type_from_layout(env, layout_interner, return_layout)
}
};
@ -5250,7 +5254,7 @@ fn build_proc<'a, 'ctx, 'env>(
GenTest | WasmGenTest | CliTest => {
/* no host, or exposing types is not supported */
}
Binary | BinaryDev => {
Binary | BinaryDev | BinaryGlue => {
for (alias_name, hels) in aliases.iter() {
let ident_string = proc.name.name().as_str(&env.interns);
let fn_name: String = format!("{}_{}", ident_string, hels.id.0);

View file

@ -197,6 +197,11 @@ pub fn add_sjlj_roc_panic(env: &Env<'_, '_, '_>) {
let mut params = fn_val.get_param_iter();
let roc_str_arg = params.next().unwrap();
// normally, roc_panic is marked as external so it can be provided by the host. But when we
// define it here in LLVM IR, we never want it to be linked by the host (that would
// overwrite which implementation is used.
fn_val.set_linkage(Linkage::Internal);
let tag_id_arg = params.next().unwrap();
debug_assert!(params.next().is_none());

View file

@ -253,6 +253,7 @@ fn create_llvm_module<'a>(
let (main_fn_name, main_fn) = match config.mode {
LlvmBackendMode::Binary => unreachable!(),
LlvmBackendMode::BinaryDev => unreachable!(),
LlvmBackendMode::BinaryGlue => unreachable!(),
LlvmBackendMode::CliTest => unreachable!(),
LlvmBackendMode::WasmGenTest => roc_gen_llvm::llvm::build::build_wasm_test_wrapper(
&env,

3
crates/glue/.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
*.so
*.dylib
*.dll

View file

@ -8,10 +8,13 @@ license.workspace = true
version.workspace = true
[dependencies]
roc_build = { path = "../compiler/build" }
roc_builtins = { path = "../compiler/builtins" }
roc_can = { path = "../compiler/can" }
roc_collections = { path = "../compiler/collections" }
roc_error_macros = { path = "../error_macros" }
roc_gen_llvm= { path = "../compiler/gen_llvm" }
roc_linker = { path = "../linker"}
roc_load = { path = "../compiler/load" }
roc_module = { path = "../compiler/module" }
roc_mono = { path = "../compiler/mono" }
@ -22,9 +25,12 @@ roc_target = { path = "../compiler/roc_target" }
roc_tracing = { path = "../tracing" }
roc_types = { path = "../compiler/types" }
backtrace.workspace = true
bumpalo.workspace = true
fnv.workspace = true
indexmap.workspace = true
libc.workspace = true
libloading.workspace = true
strum.workspace = true
strum_macros.workspace = true
target-lexicon.workspace = true

View file

@ -1,17 +1,19 @@
platform "roc-lang/rbt"
requires {} { makeGlue : Str -> Types }
platform "roc-lang/glue"
requires {} { makeGlue : List Types -> Result (List File) Str }
exposes []
packages {}
imports []
provides [makeGlueForHost]
makeGlueForHost : Str -> Types
makeGlueForHost = makeGlue
makeGlueForHost : List Types -> Result (List File) Str
makeGlueForHost = \x -> makeGlue x
File : { name : Str, content : Str }
# TODO move into separate Target.roc interface once glue works across interfaces.
Target : {
architecture: Architecture,
operatingSystem: OperatingSystem,
architecture : Architecture,
operatingSystem : OperatingSystem,
}
Architecture : [
@ -28,22 +30,36 @@ OperatingSystem : [
Wasi,
]
TypeId := Nat
# TODO change this to an opaque type once glue supports abilities.
TypeId : Nat
# has [
# Eq {
# isEq: isEqTypeId,
# },
# Hash {
# hash: hashTypeId,
# }
# ]
# isEqTypeId = \@TypeId lhs, @TypeId rhs -> lhs == rhs
# hashTypeId = \hasher, @TypeId id -> Hash.hash hasher id
# TODO: switch AssocList uses to Dict once roc_std is updated.
Tuple1 : [T Str TypeId]
Tuple2 : [T TypeId (List TypeId)]
Types := {
Types : {
# These are all indexed by TypeId
types: List RocType,
sizes: List U32,
aligns: List U32,
types : List RocType,
sizes : List U32,
aligns : List U32,
# Needed to check for duplicates
typesByName: Dict Str TypeId,
typesByName : List Tuple1,
## 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: Dict TypeId (List TypeId),
target: Target,
deps : List Tuple2,
target : Target,
}
RocType : [
@ -57,25 +73,24 @@ RocType : [
RocBox TypeId,
TagUnion RocTagUnion,
EmptyTagUnion,
Struct {
name: Str,
fields: List { name: Str, type: TypeId }
},
TagUnionPayload {
name: Str,
fields: List { discriminant: Nat, type: TypeId },
},
Struct
{
name : Str,
fields : RocStructFields,
},
TagUnionPayload
{
name : Str,
fields : RocStructFields,
},
## 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,
Function {
name: Str,
args: List TypeId,
ret: TypeId,
},
Function RocFn,
# A zero-sized type, such as an empty record or a single-tag union with no payload
Unit,
Unsized,
]
RocNum : [
@ -95,61 +110,86 @@ RocNum : [
]
RocTagUnion : [
Enumeration {
name: Str,
tags: List Str,
size: U32,
},
Enumeration
{
name : Str,
tags : List Str,
size : U32,
},
## A non-recursive tag union
## e.g. `Result a e : [Ok a, Err e]`
NonRecursive {
name: Str,
tags: List { name : Str, payload : [Some TypeId, None] },
discriminantSize: U32,
discriminantOffset: U32,
},
NonRecursive
{
name : Str,
tags : List { name : Str, payload : [Some TypeId, None] },
discriminantSize : U32,
discriminantOffset : U32,
},
## A recursive tag union (general case)
## e.g. `Expr : [Sym Str, Add Expr Expr]`
Recursive {
name: Str,
tags: List { name : Str, payload : [Some TypeId, None] },
discriminantSize: U32,
discriminantOffset: U32,
},
Recursive
{
name : Str,
tags : List { name : Str, payload : [Some TypeId, None] },
discriminantSize : U32,
discriminantOffset : U32,
},
## 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: Str,
indexOfNullTag: U16,
tags: List { name : Str, payload : [Some TypeId, None] },
discriminantSize: U32,
discriminantOffset: U32,
},
NullableWrapped
{
name : Str,
indexOfNullTag : U16,
tags : List { name : Str, payload : [Some TypeId, None] },
discriminantSize : U32,
discriminantOffset : U32,
},
## Optimization: No need to store a tag ID (the payload is "unwrapped")
## e.g. `RoseTree a : [Tree a (List (RoseTree a))]`
NonNullableUnwrapped {
name: Str,
tagName: Str,
payload: TypeId, # These always have a payload.
},
NonNullableUnwrapped
{
name : Str,
tagName : Str,
payload : TypeId, # These always have a payload.
},
## Optimization: No need to store a tag ID (the payload is "unwrapped")
## e.g. `[Foo Str Bool]`
SingleTagStruct {
name: Str,
tagName: Str,
payloadFields: List TypeId,
},
SingleTagStruct
{
name : Str,
tagName : Str,
payload: RocSingleTagPayload,
},
## 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: Str,
nullTag: Str,
nonNullTag: Str,
nonNullPayload: TypeId,
whichTagIsNull: [FirstTagIsNull, SecondTagIsNull],
},
NullableUnwrapped
{
name : Str,
nullTag : Str,
nonNullTag : Str,
nonNullPayload : TypeId,
whichTagIsNull : [FirstTagIsNull, SecondTagIsNull],
},
]
RocStructFields : [
HasNoClosure (List { name: Str, id: TypeId }),
HasClosure (List { name: Str, id: TypeId, accessors: { getter: Str } }),
]
RocSingleTagPayload: [
HasClosure (List { name: Str, id: TypeId }),
HasNoClosure (List { id: TypeId }),
]
RocFn : {
functionName: Str,
externName: Str,
args: List TypeId,
lambdaSet: TypeId,
ret: TypeId,
}

View file

@ -0,0 +1,944 @@
app "rust-glue"
packages { pf: "RocType.roc" }
imports []
provides [makeGlue] to pf
makeGlue = \types ->
modFileContent =
List.walk types "" \content, { target } ->
archStr = archName target.architecture
Str.concat
content
"""
#[cfg(target_arch = "\(archStr)")]
mod \(archStr);
#[cfg(target_arch = "\(archStr)")]
pub use \(archStr)::*;
"""
types
|> List.map typesWithDict
|> List.map convertTypesToFile
|> List.append { name: "mod.rs", content: modFileContent }
|> Ok
convertTypesToFile = \types ->
content =
walkWithIndex types.types fileHeader \buf, id, type ->
when type is
Struct { name, fields } ->
generateStruct buf types id name fields Public
TagUnionPayload { name, fields } ->
generateStruct buf types id name (nameTagUnionPayloadFields fields) Private
TagUnion (Enumeration { name, tags, size }) ->
generateEnumeration buf types type name tags size
TagUnion (NonRecursive { name, tags, discriminantSize, discriminantOffset }) ->
if !(List.isEmpty tags) then
generateNonRecursiveTagUnion buf types id name tags discriminantSize discriminantOffset None
else
buf
TagUnion (Recursive { name, tags, discriminantSize, discriminantOffset }) ->
if !(List.isEmpty tags) then
generateRecursiveTagUnion buf types id name tags discriminantSize discriminantOffset None
else
buf
TagUnion (NullableWrapped { name, indexOfNullTag, tags, discriminantSize, discriminantOffset }) ->
generateRecursiveTagUnion buf types id name tags discriminantSize discriminantOffset (Some indexOfNullTag)
TagUnion (NullableUnwrapped { name, nullTag, nonNullTag, nonNullPayload, whichTagIsNull }) ->
generateNullableUnwrapped buf types id name nullTag nonNullTag nonNullPayload whichTagIsNull
TagUnion (SingleTagStruct { name, tagName, payload }) ->
generateSingleTagStruct buf types name tagName payload
TagUnion (NonNullableUnwrapped { name, tagName, payload }) ->
generateRecursiveTagUnion buf types id name [{ name: tagName, payload: Some payload }] 0 0 None
Function _ ->
# TODO: actually generate glue functions.
buf
RecursivePointer _ ->
# This is recursively pointing to a type that should already have been added,
# so no extra work needs to happen.
buf
Unit
| Unsized
| EmptyTagUnion
| Num _
| Bool
| RocResult _ _
| RocStr
| RocDict _ _
| RocSet _
| RocList _
| RocBox _ ->
# These types don't need to be declared in Rust.
# TODO: Eventually we want to generate roc_std. So these types will need to be emitted.
buf
archStr = archName types.target.architecture
{
name: "\(archStr).rs",
content,
}
generateStruct = \buf, types, id, name, structFields, visibility ->
escapedName = escapeKW name
repr =
length =
when structFields is
HasClosure fields -> List.len fields
HasNoClosure fields -> List.len fields
if length <= 1 then
"transparent"
else
"C"
pub =
when visibility is
Public -> "pub"
Private -> ""
structType = getType types id
buf
|> generateDeriveStr types structType IncludeDebug
|> Str.concat "#[repr(\(repr))]\n\(pub) struct \(escapedName) {\n"
|> generateStructFields types Public structFields
|> Str.concat "}\n\n"
generateStructFields = \buf, types, visibility, structFields ->
when structFields is
HasNoClosure fields ->
List.walk fields buf (generateStructFieldWithoutClosure types visibility)
HasClosure _ ->
Str.concat buf "// TODO: Struct fields with closures"
generateStructFieldWithoutClosure = \types, visibility ->
\accum, { name: fieldName, id } ->
typeStr = typeName types id
escapedFieldName = escapeKW fieldName
pub =
when visibility is
Public -> "pub"
Private -> ""
Str.concat accum "\(indent)\(pub) \(escapedFieldName): \(typeStr),\n"
nameTagUnionPayloadFields = \payloadFields ->
# Tag union payloads have numbered fields, so we prefix them
# with an "f" because Rust doesn't allow struct fields to be numbers.
when payloadFields is
HasNoClosure fields ->
renamedFields = List.map fields \{ name, id } -> { name: "f\(name)", id }
HasNoClosure renamedFields
HasClosure fields ->
renamedFields = List.map fields \{ name, id, accessors } -> { name: "f\(name)", id, accessors }
HasClosure renamedFields
generateEnumeration = \buf, types, enumType, name, tags, tagBytes ->
escapedName = escapeKW name
reprBits = tagBytes * 8 |> Num.toStr
buf
|> generateDeriveStr types enumType ExcludeDebug
|> Str.concat "#[repr(u\(reprBits))]\npub enum \(escapedName) {\n"
|> \b -> walkWithIndex tags b generateEnumTags
|>
# Enums require a custom debug impl to ensure naming is identical on all platforms.
Str.concat
"""
}
impl core::fmt::Debug for \(escapedName) {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
"""
|> \b -> List.walk tags b (generateEnumTagsDebug name)
|> Str.concat "\(indent)\(indent)}\n\(indent)}\n}\n\n"
generateEnumTags = \accum, index, name ->
indexStr = Num.toStr index
Str.concat accum "\(indent)\(name) = \(indexStr),\n"
generateEnumTagsDebug = \name ->
\accum, tagName ->
Str.concat accum "\(indent)\(indent)\(indent)Self::\(tagName) => f.write_str(\"\(name)::\(tagName)\"),\n"
generateNonRecursiveTagUnion = \buf, types, id, name, tags, discriminantSize, discriminantOffset, _nullTagIndex ->
escapedName = escapeKW name
discriminantName = "discriminant_\(escapedName)"
discriminantOffsetStr = Num.toStr discriminantOffset
tagNames = List.map tags \{ name: n } -> n
# self = "self"
selfMut = "self"
# other = "other"
unionName = escapedName
buf
|> generateDiscriminant types discriminantName tagNames discriminantSize
|> Str.concat "#[repr(C)]\npub union \(unionName) {\n"
|> \b -> List.walk tags b (generateUnionField types)
|> generateTagUnionSizer types id tags
|> Str.concat
"""
}
impl \(escapedName) {
\(discriminantDocComment)
pub fn discriminant(&self) -> \(discriminantName) {
unsafe {
let bytes = core::mem::transmute::<&Self, &[u8; core::mem::size_of::<Self>()]>(self);
core::mem::transmute::<u8, \(discriminantName)>(*bytes.as_ptr().add(\(discriminantOffsetStr)))
}
}
/// Internal helper
fn set_discriminant(&mut self, discriminant: \(discriminantName)) {
let discriminant_ptr: *mut \(discriminantName) = (self as *mut \(escapedName)).cast();
unsafe {
*(discriminant_ptr.add(\(discriminantOffsetStr))) = discriminant;
}
}
}
"""
|> Str.concat "// TODO: NonRecursive TagUnion constructor impls\n\n"
|> \b ->
type = getType types id
if cannotDeriveCopy types type then
# A custom drop impl is only needed when we can't derive copy.
b
|> Str.concat
"""
impl Drop for \(escapedName) {
fn drop(&mut self) {
// Drop the payloads
"""
|> generateTagUnionDropPayload types selfMut tags discriminantName discriminantSize 2
|> Str.concat
"""
}
}
"""
else
b
generateRecursiveTagUnion = \buf, types, id, name, tags, discriminantSize, _discriminantOffset, _nullTagIndex ->
escapedName = escapeKW name
discriminantName = "discriminant_\(escapedName)"
tagNames = List.map tags \{ name: n } -> n
# self = "(&*self.union_pointer())"
# selfMut = "(&mut *self.union_pointer())"
# other = "(&*other.union_pointer())"
unionName = "union_\(escapedName)"
buf
|> generateDiscriminant types discriminantName tagNames discriminantSize
|> Str.concat
"""
#[repr(transparent)]
pub struct \(escapedName) {
pointer: *mut \(unionName),
}
#[repr(C)]
union \(unionName) {
"""
|> \b -> List.walk tags b (generateUnionField types)
|> generateTagUnionSizer types id tags
|> Str.concat "}\n\n"
|> Str.concat "// TODO: Recursive TagUnion impls\n\n"
generateTagUnionDropPayload = \buf, types, selfMut, tags, discriminantName, discriminantSize, indents ->
if discriminantSize == 0 then
when List.first tags is
Ok { name } ->
# There's only one tag, so there's no discriminant and no need to match;
# just drop the pointer.
buf
|> writeIndents indents
|> Str.concat "unsafe { core::mem::ManuallyDrop::drop(&mut core::ptr::read(self.pointer).\(name)); }"
Err ListWasEmpty ->
crash "unreachable"
else
buf
|> writeTagImpls tags discriminantName indents \name, payload ->
when payload is
Some id if cannotDeriveCopy types (getType types id) ->
"unsafe {{ core::mem::ManuallyDrop::drop(&mut \(selfMut).\(name)) }},"
_ ->
# If it had no payload, or if the payload had no pointers,
# there's nothing to clean up, so do `=> {}` for the branch.
"{}"
writeIndents = \buf, indents ->
if indents <= 0 then
buf
else
buf
|> Str.concat indent
|> writeIndents (indents - 1)
writeTagImpls = \buf, tags, discriminantName, indents, f ->
buf
|> writeIndents indents
|> Str.concat "match self.discriminant() {\n"
|> \b -> List.walk tags b \accum, { name, payload } ->
branchStr = f name payload
accum
|> writeIndents (indents + 1)
|> Str.concat "\(discriminantName)::\(name) => \(branchStr)\n"
|> writeIndents indents
|> Str.concat "}\n"
generateTagUnionSizer = \buf, types, id, tags ->
if List.len tags > 1 then
# When there's a discriminant (so, multiple tags) and there is
# no alignment padding after the largest variant,
# the compiler will make extra room for the discriminant.
# We need that to be reflected in the overall size of the enum,
# so add an extra variant with the appropriate size.
#
# (Do this even if theoretically shouldn't be necessary, since
# there's no runtime cost and it more explicitly syncs the
# union's size with what we think it should be.)
size = getSizeRoundedToAlignment types id
sizeStr = Num.toStr size
Str.concat buf "\(indent)_sizer: [u8; \(sizeStr)],\n"
else
buf
generateDiscriminant = \buf, types, name, tags, size ->
if size > 0 then
enumType =
TagUnion
(
Enumeration {
name,
tags,
size,
}
)
buf
|> generateEnumeration types enumType name tags size
else
buf
generateUnionField = \types ->
\accum, { name: fieldName, payload } ->
when payload is
Some id ->
typeStr = typeName types id
escapedFieldName = escapeKW fieldName
type = getType types id
fullTypeStr =
if cannotDeriveCopy types type then
# types with pointers need ManuallyDrop
# because rust unions don't (and can't)
# know how to drop them automatically!
"core::mem::ManuallyDrop<\(typeStr)>"
else
typeStr
Str.concat accum "\(indent)\(escapedFieldName): \(fullTypeStr),\n"
None ->
# If there's no payload, we don't need a discriminant for it.
accum
generateNullableUnwrapped = \buf, _types, _id, _name, _nullTag, _nonNullTag, _nonNullPayload, _whichTagIsNull ->
Str.concat buf "// TODO: TagUnion NullableUnwrapped\n\n"
generateSingleTagStruct = \buf, types, name, tagName, payload ->
# Store single-tag unions as structs rather than enums,
# because they have only one alternative. However, still
# offer the usual tag union APIs.
escapedName = escapeKW name
repr =
length =
when payload is
HasClosure fields -> List.len fields
HasNoClosure fields -> List.len fields
if length <= 1 then
"transparent"
else
"C"
when payload is
HasNoClosure fields ->
asStructFields =
List.mapWithIndex fields \{ id }, index ->
indexStr = Num.toStr index
{ name: "f\(indexStr)", id }
|> HasNoClosure
asStructType =
Struct {
name,
fields: asStructFields,
}
buf
|> generateDeriveStr types asStructType ExcludeDebug
|> Str.concat "#[repr(\(repr))]\npub struct \(escapedName) "
|> \b ->
if List.isEmpty fields then
generateZeroElementSingleTagStruct b escapedName tagName
else
generateMultiElementSingleTagStruct b types escapedName tagName fields asStructFields
HasClosure _ ->
Str.concat buf "\\TODO: SingleTagStruct with closures"
generateMultiElementSingleTagStruct = \buf, types, name, tagName, payloadFields, asStructFields ->
buf
|> Str.concat "{\n"
|> generateStructFields types Private asStructFields
|> Str.concat "}\n\n"
|> Str.concat
"""
impl \(name) {
"""
|> \b ->
fieldTypes =
payloadFields
|> List.map \{ id } ->
typeName types id
args =
fieldTypes
|> List.mapWithIndex \fieldTypeName, index ->
indexStr = Num.toStr index
"f\(indexStr): \(fieldTypeName)"
fields =
payloadFields
|> List.mapWithIndex \_, index ->
indexStr = Num.toStr index
"f\(indexStr)"
fieldAccesses =
fields
|> List.map \field ->
"self.\(field)"
{
b,
args,
fields,
fieldTypes,
fieldAccesses,
}
|> \{ b, args, fields, fieldTypes, fieldAccesses } ->
argsStr = Str.joinWith args ", "
fieldsStr = Str.joinWith fields "\n\(indent)\(indent)\(indent)"
{
b: Str.concat
b
"""
\(indent)/// A tag named ``\(tagName)``, with the given payload.
\(indent)pub fn \(tagName)(\(argsStr)) -> Self {
\(indent) Self {
\(indent) \(fieldsStr)
\(indent) }
\(indent)}
""",
fieldTypes,
fieldAccesses,
}
|> \{ b, fieldTypes, fieldAccesses } ->
retType = asRustTuple fieldTypes
retExpr = asRustTuple fieldAccesses
{
b: Str.concat
b
"""
\(indent)/// Since `\(name)` only has one tag (namely, `\(tagName)`),
\(indent)/// convert it to `\(tagName)`'s payload.
\(indent)pub fn into_\(tagName)(self) -> \(retType) {
\(indent) \(retExpr)
\(indent)}
""",
fieldTypes,
fieldAccesses,
}
|> \{ b, fieldTypes, fieldAccesses } ->
retType =
fieldTypes
|> List.map \ft -> "&\(ft)"
|> asRustTuple
retExpr =
fieldAccesses
|> List.map \fa -> "&\(fa)"
|> asRustTuple
Str.concat
b
"""
\(indent)/// Since `\(name)` only has one tag (namely, `\(tagName)`),
\(indent)/// convert it to `\(tagName)`'s payload.
\(indent)pub fn as_\(tagName)(&self) -> \(retType) {
\(indent) \(retExpr)
\(indent)}
"""
|> Str.concat
"""
}
impl core::fmt::Debug for \(name) {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_tuple("\(name)::\(tagName)")
"""
|> \b ->
payloadFields
|> List.mapWithIndex \_, index ->
indexStr = Num.toStr index
"\(indent)\(indent)\(indent)\(indent).field(&self.f\(indexStr))\n"
|> List.walk b Str.concat
|> Str.concat
"""
.finish()
}
}
"""
asRustTuple = \list ->
# If there is 1 element in the list we just return it
# Otherwise, we make a proper tuple string.
joined = Str.joinWith list ", "
if List.len list == 1 then
joined
else
"(\(joined))"
generateZeroElementSingleTagStruct = \buf, name, tagName ->
# A single tag with no payload is a zero-sized unit type, so
# represent it as a zero-sized struct (e.g. "struct Foo()").
buf
|> Str.concat "();\n\n"
|> Str.concat
"""
impl \(name) {
/// A tag named \(tagName), which has no payload.
pub const \(tagName): Self = Self();
/// Other `into_` methods return a payload, but since \(tagName) tag
/// has no payload, this does nothing and is only here for completeness.
pub fn into_\(tagName)(self) {
()
}
/// Other `as_` methods return a payload, but since \(tagName) tag
/// has no payload, this does nothing and is only here for completeness.
pub fn as_\(tagName)(&self) {
()
}
}
impl core::fmt::Debug for \(name) {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str("\(name)::\(tagName)")
}
}
"""
generateDeriveStr = \buf, types, type, includeDebug ->
buf
|> Str.concat "#[derive(Clone, "
|> \b ->
if !(cannotDeriveCopy types type) then
Str.concat b "Copy, "
else
b
|> \b ->
if !(cannotDeriveDefault types type) then
Str.concat b "Default, "
else
b
|> \b ->
when includeDebug is
IncludeDebug ->
Str.concat b "Debug, "
ExcludeDebug ->
b
|> \b ->
if !(hasFloat types type) then
Str.concat b "Eq, Ord, Hash, "
else
b
|> Str.concat "PartialEq, PartialOrd)]\n"
cannotDeriveCopy = \types, type ->
when type is
Unit | Unsized | EmptyTagUnion | Bool | Num _ | TagUnion (Enumeration _) | Function _ -> Bool.false
RocStr | RocList _ | RocDict _ _ | RocSet _ | RocBox _ | TagUnion (NullableUnwrapped _) | TagUnion (NullableWrapped _) | TagUnion (Recursive _) | TagUnion (NonNullableUnwrapped _) | RecursivePointer _ -> Bool.true
TagUnion (SingleTagStruct { payload: HasNoClosure fields }) ->
List.any fields \{ id } -> cannotDeriveCopy types (getType types id)
TagUnion (SingleTagStruct { payload: HasClosure fields }) ->
List.any fields \{ id } -> cannotDeriveCopy types (getType types id)
TagUnion (NonRecursive { tags }) ->
List.any tags \{ payload } ->
when payload is
Some id -> cannotDeriveCopy types (getType types id)
None -> Bool.false
RocResult okId errId ->
cannotDeriveCopy types (getType types okId)
|| cannotDeriveCopy types (getType types errId)
Struct { fields: HasNoClosure fields } | TagUnionPayload { fields: HasNoClosure fields } ->
List.any fields \{ id } -> cannotDeriveCopy types (getType types id)
Struct { fields: HasClosure fields } | TagUnionPayload { fields: HasClosure fields } ->
List.any fields \{ id } -> cannotDeriveCopy types (getType types id)
cannotDeriveDefault = \types, type ->
when type is
Unit | Unsized | EmptyTagUnion | TagUnion _ | RocResult _ _ | RecursivePointer _ | Function _ -> Bool.true
RocStr | Bool | Num _ | Struct { fields: HasClosure _ } | TagUnionPayload { fields: HasClosure _ } -> Bool.false
RocList id | RocSet id | RocBox id ->
cannotDeriveDefault types (getType types id)
RocDict keyId valId ->
cannotDeriveCopy types (getType types keyId)
|| cannotDeriveCopy types (getType types valId)
Struct { fields: HasNoClosure fields } | TagUnionPayload { fields: HasNoClosure fields } ->
List.any fields \{ id } -> cannotDeriveDefault types (getType types id)
hasFloat = \types, type ->
hasFloatHelp types type (Set.empty {})
hasFloatHelp = \types, type, doNotRecurse ->
# TODO: is doNotRecurse problematic? Do we need an updated doNotRecurse for calls up the tree?
# I think there is a change it really only matters for RecursivePointer, so it may be fine.
# Otherwise we need to deal with threading through updates to doNotRecurse
when type is
Num kind ->
when kind is
F32 | F64 -> Bool.true
_ -> Bool.false
Unit | Unsized | EmptyTagUnion | RocStr | Bool | TagUnion (Enumeration _) | Function _ -> Bool.false
RocList id | RocSet id | RocBox id ->
hasFloatHelp types (getType types id) doNotRecurse
RocDict id0 id1 | RocResult id0 id1 ->
hasFloatHelp types (getType types id0) doNotRecurse
|| hasFloatHelp types (getType types id1) doNotRecurse
Struct { fields: HasNoClosure fields } | TagUnionPayload { fields: HasNoClosure fields } ->
List.any fields \{ id } -> hasFloatHelp types (getType types id) doNotRecurse
Struct { fields: HasClosure fields } | TagUnionPayload { fields: HasClosure fields } ->
List.any fields \{ id } -> hasFloatHelp types (getType types id) doNotRecurse
TagUnion (SingleTagStruct { payload: HasNoClosure fields }) ->
List.any fields \{ id } -> hasFloatHelp types (getType types id) doNotRecurse
TagUnion (SingleTagStruct { payload: HasClosure fields }) ->
List.any fields \{ id } -> hasFloatHelp types (getType types id) doNotRecurse
TagUnion (Recursive { tags }) ->
List.any tags \{ payload } ->
when payload is
Some id -> hasFloatHelp types (getType types id) doNotRecurse
None -> Bool.false
TagUnion (NonRecursive { tags }) ->
List.any tags \{ payload } ->
when payload is
Some id -> hasFloatHelp types (getType types id) doNotRecurse
None -> Bool.false
TagUnion (NullableWrapped { tags }) ->
List.any tags \{ payload } ->
when payload is
Some id -> hasFloatHelp types (getType types id) doNotRecurse
None -> Bool.false
TagUnion (NonNullableUnwrapped { payload }) ->
if Set.contains doNotRecurse payload then
Bool.false
else
nextDoNotRecurse = Set.insert doNotRecurse payload
hasFloatHelp types (getType types payload) nextDoNotRecurse
TagUnion (NullableUnwrapped { nonNullPayload }) ->
if Set.contains doNotRecurse nonNullPayload then
Bool.false
else
nextDoNotRecurse = Set.insert doNotRecurse nonNullPayload
hasFloatHelp types (getType types nonNullPayload) nextDoNotRecurse
RecursivePointer payload ->
if Set.contains doNotRecurse payload then
Bool.false
else
nextDoNotRecurse = Set.insert doNotRecurse payload
hasFloatHelp types (getType types payload) nextDoNotRecurse
typeName = \types, id ->
when getType types id is
Unit -> "()"
Unsized -> "roc_std::RocList<u8>"
EmptyTagUnion -> "std::convert::Infallible"
RocStr -> "roc_std::RocStr"
Bool -> "bool"
Num U8 -> "u8"
Num U16 -> "u16"
Num U32 -> "u32"
Num U64 -> "u64"
Num U128 -> "u128"
Num I8 -> "i8"
Num I16 -> "i16"
Num I32 -> "i32"
Num I64 -> "i64"
Num I128 -> "i128"
Num F32 -> "f32"
Num F64 -> "f64"
Num Dec -> "roc_std:RocDec"
RocDict key value ->
keyName = typeName types key
valueName = typeName types value
"roc_std::RocDict<\(keyName), \(valueName)>"
RocSet elem ->
elemName = typeName types elem
"roc_std::RocSet<\(elemName)>"
RocList elem ->
elemName = typeName types elem
"roc_std::RocList<\(elemName)>"
RocBox elem ->
elemName = typeName types elem
"roc_std::RocBox<\(elemName)>"
RocResult ok err ->
okName = typeName types ok
errName = typeName types err
"roc_std::RocResult<\(okName), \(errName)>"
RecursivePointer content ->
typeName types content
Struct { name } -> escapeKW name
TagUnionPayload { name } -> escapeKW name
TagUnion (NonRecursive { name }) -> escapeKW name
TagUnion (Recursive { name }) -> escapeKW name
TagUnion (Enumeration { name }) -> escapeKW name
TagUnion (NullableWrapped { name }) -> escapeKW name
TagUnion (NullableUnwrapped { name }) -> escapeKW name
TagUnion (NonNullableUnwrapped { name }) -> escapeKW name
TagUnion (SingleTagStruct { name }) -> escapeKW name
Function { functionName } -> escapeKW functionName
getType = \types, id ->
when List.get types.types id is
Ok type -> type
Err _ -> crash "unreachable"
getSizeRoundedToAlignment = \types, id ->
alignment = getAlignment types id
getSizeIgnoringAlignment types id
|> roundUpToAlignment alignment
getSizeIgnoringAlignment = \types, id ->
when List.get types.sizes id is
Ok size -> size
Err _ -> crash "unreachable"
getAlignment = \types, id ->
when List.get types.aligns id is
Ok align -> align
Err _ -> crash "unreachable"
roundUpToAlignment = \width, alignment ->
when alignment is
0 -> width
1 -> width
_ ->
if width % alignment > 0 then
width + alignment - (width % alignment)
else
width
walkWithIndex = \list, originalState, f ->
stateWithId =
List.walk list { id: 0nat, state: originalState } \{ id, state }, elem ->
nextState = f state id elem
{ id: id + 1, state: nextState }
stateWithId.state
archName = \arch ->
when arch is
Aarch32 ->
"arm"
Aarch64 ->
"aarch64"
Wasm32 ->
"wasm32"
X86x32 ->
"x86"
X86x64 ->
"x86_64"
fileHeader =
"""
// ⚠️ GENERATED CODE ⚠️ - this entire file was generated by the `roc glue` CLI command
#![allow(unused_unsafe)]
#![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)]
#![allow(clippy::redundant_static_lifetimes)]
#![allow(clippy::unused_unit)]
#![allow(clippy::missing_safety_doc)]
#![allow(clippy::let_and_return)]
#![allow(clippy::missing_safety_doc)]
#![allow(clippy::redundant_static_lifetimes)]
#![allow(clippy::needless_borrow)]
#![allow(clippy::clone_on_copy)]
"""
indent = " "
discriminantDocComment = "/// Returns which variant this tag union holds. Note that this never includes a payload!"
reservedKeywords = Set.fromList [
"try",
"abstract",
"become",
"box",
"do",
"final",
"macro",
"override",
"priv",
"typeof",
"unsized",
"virtual",
"yield",
"async",
"await",
"dyn",
"as",
"break",
"const",
"continue",
"crate",
"else",
"enum",
"extern",
"false",
"fn",
"for",
"if",
"impl",
"in",
"let",
"loop",
"match",
"mod",
"move",
"mut",
"pub",
"ref",
"return",
"self",
"Self",
"static",
"struct",
"super",
"trait",
"true",
"type",
"unsafe",
"use",
"where",
"while",
]
escapeKW = \input ->
# use a raw identifier for this, to prevent a syntax error due to using a reserved keyword.
# https://doc.rust-lang.org/rust-by-example/compatibility/raw_identifiers.html
# another design would be to add an underscore after it; this is an experiment!
if Set.contains reservedKeywords input then
"r#\(input)"
else
input
# This is a temporary helper until roc_std::roc_dict is update.
# after that point, Dict will be passed in directly.
typesWithDict = \{ types, sizes, aligns, typesByName, deps, target } -> {
types,
sizes,
aligns,
typesByName: Dict.fromList typesByName,
deps: Dict.fromList deps,
target,
}

View file

@ -4,6 +4,8 @@
//! the plan is to support any language via a plugin model.
pub mod enums;
pub mod load;
pub mod roc_helpers;
pub mod roc_type;
pub mod rust_glue;
pub mod structs;
pub mod types;

View file

@ -1,9 +1,18 @@
use crate::rust_glue;
use crate::roc_type;
use crate::types::Types;
use bumpalo::Bump;
use libloading::Library;
use roc_build::{
link::{LinkType, LinkingStrategy},
program::{
build_file, handle_error_module, handle_loading_problem, standard_load_config,
BuildFileError, BuildOrdering, BuiltFile, CodeGenBackend, CodeGenOptions,
},
};
use roc_collections::MutMap;
use roc_gen_llvm::llvm::build::LlvmBackendMode;
use roc_load::{ExecutionMode, LoadConfig, LoadedModule, LoadingProblem, Threading};
use roc_mono::ir::{generate_glue_procs, GlueProc};
use roc_mono::ir::{generate_glue_procs, GlueProc, OptLevel};
use roc_mono::layout::{GlobalLayoutInterner, LayoutCache, LayoutInterner};
use roc_packaging::cache::{self, RocCacheDir};
use roc_reporting::report::{RenderTarget, DEFAULT_PALETTE};
@ -11,7 +20,8 @@ use roc_target::{Architecture, TargetInfo};
use roc_types::subs::{Subs, Variable};
use std::fs::File;
use std::io::{self, ErrorKind, Write};
use std::path::{Path, PathBuf};
use std::mem::ManuallyDrop;
use std::path::{Component, Path, PathBuf};
use std::process;
use strum::IntoEnumIterator;
use target_lexicon::Triple;
@ -24,44 +34,165 @@ impl IgnoreErrors {
const NONE: Self = IgnoreErrors { can: false };
}
pub fn generate(input_path: &Path, output_path: &Path) -> io::Result<i32> {
pub fn generate(input_path: &Path, output_path: &Path, spec_path: &Path) -> io::Result<i32> {
// TODO: Add verification around the paths. Make sure they heav the correct file extension and what not.
match load_types(
input_path.to_path_buf(),
Threading::AllAvailable,
IgnoreErrors::NONE,
) {
Ok(types_and_targets) => {
let mut file = File::create(output_path).unwrap_or_else(|err| {
eprintln!(
"Unable to create output file {} - {:?}",
output_path.display(),
err
);
Ok(types) => {
// TODO: we should to modify the app file first before loading it.
// Somehow it has to point to the correct platform file which may not exist on the target machine.
let triple = Triple::host();
process::exit(1);
});
let code_gen_options = CodeGenOptions {
// Maybe eventually use dev here or add flag for this.
backend: CodeGenBackend::Llvm(LlvmBackendMode::BinaryGlue),
opt_level: OptLevel::Development,
emit_debug_info: false,
};
let mut buf = std::str::from_utf8(rust_glue::HEADER).unwrap().to_string();
let body = rust_glue::emit(&types_and_targets);
buf.push_str(&body);
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()
let load_config = standard_load_config(
&triple,
BuildOrdering::BuildIfChecks,
Threading::AllAvailable,
);
Ok(0)
let arena = ManuallyDrop::new(Bump::new());
let link_type = LinkType::Dylib;
let linking_strategy = if roc_linker::supported(link_type, &triple) {
LinkingStrategy::Surgical
} else {
LinkingStrategy::Legacy
};
let res_binary_path = build_file(
&arena,
&triple,
spec_path.to_path_buf(),
code_gen_options,
false,
link_type,
linking_strategy,
true,
None,
RocCacheDir::Persistent(cache::roc_cache_dir().as_path()),
load_config,
);
match res_binary_path {
Ok(BuiltFile {
binary_path,
problems,
total_time,
expect_metadata: _,
}) => {
// TODO: Should binary_path be update to deal with extensions?
use target_lexicon::OperatingSystem;
let lib_path = match triple.operating_system {
OperatingSystem::Windows => binary_path.with_extension("dll"),
OperatingSystem::Darwin | OperatingSystem::MacOSX { .. } => {
binary_path.with_extension("dylib")
}
_ => binary_path.with_extension("so.1.0"),
};
// TODO: Should glue try and run with errors, especially type errors.
// My gut feeling is no or that we should add a flag for it.
// Given glue will generally be run by random end users, I think it should default to full correctness.
debug_assert_eq!(
problems.errors, 0,
"if there are errors, they should have been returned as an error variant"
);
if problems.warnings > 0 {
problems.print_to_stdout(total_time);
println!(
".\n\nRunning program…\n\n\x1B[36m{}\x1B[39m",
"".repeat(80)
);
}
let lib = unsafe { Library::new(lib_path) }.unwrap();
type MakeGlue = unsafe extern "C" fn(
*mut roc_std::RocResult<roc_std::RocList<roc_type::File>, roc_std::RocStr>,
&roc_std::RocList<roc_type::Types>,
);
let make_glue: libloading::Symbol<MakeGlue> = unsafe {
lib.get("roc__makeGlueForHost_1_exposed_generic".as_bytes())
.unwrap_or_else(|_| panic!("Unable to load glue function"))
};
let roc_types: roc_std::RocList<roc_type::Types> =
types.iter().map(|x| x.into()).collect();
let mut files = roc_std::RocResult::err(roc_std::RocStr::empty());
unsafe { make_glue(&mut files, &roc_types) };
// Roc will free data passed into it. So forget that data.
std::mem::forget(roc_types);
let files: Result<roc_std::RocList<roc_type::File>, roc_std::RocStr> =
files.into();
let files = files.unwrap_or_else(|err| {
eprintln!("Glue generation failed: {}", err);
process::exit(1);
});
for roc_type::File { name, content } in &files {
let valid_name = PathBuf::from(name.as_str())
.components()
.all(|comp| matches!(comp, Component::CurDir | Component::Normal(_)));
if !valid_name {
eprintln!("File name was invalid: {}", &name);
process::exit(1);
}
let full_path = output_path.join(name.as_str());
if let Some(dir_path) = full_path.parent() {
std::fs::create_dir_all(dir_path).unwrap_or_else(|err| {
eprintln!(
"Unable to create output directory {} - {:?}",
dir_path.display(),
err
);
process::exit(1);
});
}
let mut file = File::create(&full_path).unwrap_or_else(|err| {
eprintln!(
"Unable to create output file {} - {:?}",
full_path.display(),
err
);
process::exit(1);
});
file.write_all(content.as_bytes()).unwrap_or_else(|err| {
eprintln!(
"Unable to write bindings to output file {} - {:?}",
full_path.display(),
err
);
process::exit(1);
});
}
println!(
"🎉 Generated type declarations in:\n\n\t{}",
output_path.display()
);
Ok(0)
}
Err(BuildFileError::ErrorModule { module, total_time }) => {
handle_error_module(module, total_time, spec_path.as_os_str(), true)
}
Err(BuildFileError::LoadingProblem(problem)) => handle_loading_problem(problem),
}
}
Err(err) => match err.kind() {
ErrorKind::NotFound => {
@ -198,7 +329,7 @@ pub fn load_types(
full_file_path: PathBuf,
threading: Threading,
ignore_errors: IgnoreErrors,
) -> Result<Vec<(Types, TargetInfo)>, io::Error> {
) -> Result<Vec<Types>, io::Error> {
let target_info = (&Triple::host()).into();
let arena = &Bump::new();
let LoadedModule {
@ -258,7 +389,7 @@ pub fn load_types(
let operating_system = target_info.operating_system;
let architectures = Architecture::iter();
let mut types_and_targets = Vec::with_capacity(architectures.len());
let mut arch_types = Vec::with_capacity(architectures.len());
let layout_interner = GlobalLayoutInterner::with_capacity(128, target_info);
@ -332,8 +463,8 @@ pub fn load_types(
target_info,
);
types_and_targets.push((types, target_info));
arch_types.push(types);
}
Ok(types_and_targets)
Ok(arch_types)
}

View file

@ -0,0 +1,234 @@
use libc::{c_char, c_int, c_void, size_t};
#[cfg(unix)]
use libc::{c_uint, mode_t, off_t, pid_t};
use roc_std::RocStr;
// These are required to ensure rust adds these functions to the final binary even though they are never used.
#[used]
pub static ROC_ALLOC: unsafe extern "C" fn(usize, u32) -> *mut c_void = roc_alloc;
#[used]
pub static ROC_REALLOC: unsafe extern "C" fn(*mut c_void, usize, usize, u32) -> *mut c_void =
roc_realloc;
#[used]
pub static ROC_DEALLOC: unsafe extern "C" fn(*mut c_void, u32) = roc_dealloc;
#[used]
pub static ROC_PANIC: unsafe extern "C" fn(&RocStr, u32) = roc_panic;
#[cfg(unix)]
#[used]
pub static ROC_GETPPID: unsafe extern "C" fn() -> pid_t = roc_getppid;
#[cfg(unix)]
#[used]
pub static ROC_MMAP: unsafe extern "C" fn(
*mut c_void,
size_t,
c_int,
c_int,
c_int,
off_t,
) -> *mut c_void = roc_mmap;
#[cfg(unix)]
#[used]
pub static ROC_SHM_OPEN: unsafe extern "C" fn(*const c_char, c_int, mode_t) -> c_int = roc_shm_open;
#[used]
pub static ROC_MEMCPY: unsafe extern "C" fn(*mut c_void, *mut c_void, usize) -> *mut c_void =
roc_memcpy;
#[used]
pub static ROC_MEMSET: unsafe extern "C" fn(*mut c_void, i32, usize) -> *mut c_void = roc_memset;
/// # Safety
/// This just delegates to libc::malloc, so it's equally safe.
#[no_mangle]
pub unsafe extern "C" fn roc_alloc(size: usize, _alignment: u32) -> *mut c_void {
libc::malloc(size)
}
/// # Safety
/// This just delegates to libc::realloc, so it's equally safe.
#[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 {
libc::realloc(c_ptr, new_size)
}
/// # Safety
/// This just delegates to libc::free, so it's equally safe.
#[no_mangle]
pub unsafe extern "C" fn roc_dealloc(c_ptr: *mut c_void, _alignment: u32) {
libc::free(c_ptr)
}
#[no_mangle]
pub extern "C" fn roc_panic(msg: &RocStr, tag_id: u32) {
match tag_id {
0 => {
eprintln!("Roc crashed with:\n\n\t{}\n", msg.as_str());
print_backtrace();
std::process::exit(1);
}
1 => {
eprintln!("The program crashed with:\n\n\t{}\n", msg.as_str());
print_backtrace();
std::process::exit(1);
}
code => {
eprintln!("Roc crashed with error code:\n\n\t{}\n", code);
print_backtrace();
std::process::exit(1);
}
}
}
#[cfg(unix)]
#[no_mangle]
pub extern "C" fn roc_getppid() -> pid_t {
unsafe { libc::getppid() }
}
/// # Safety
/// This just delegates to libc::mmap, and so is equally safe.
#[cfg(unix)]
#[no_mangle]
pub unsafe extern "C" fn roc_mmap(
addr: *mut c_void,
len: size_t,
prot: c_int,
flags: c_int,
fd: c_int,
offset: off_t,
) -> *mut c_void {
libc::mmap(addr, len, prot, flags, fd, offset)
}
/// # Safety
/// This just delegates to libc::shm_open, and so is equally safe.
#[cfg(unix)]
#[no_mangle]
pub unsafe extern "C" fn roc_shm_open(name: *const c_char, oflag: c_int, mode: mode_t) -> c_int {
libc::shm_open(name, oflag, mode as c_uint)
}
fn print_backtrace() {
eprintln!("Here is the call stack that led to the crash:\n");
let mut entries = Vec::new();
#[derive(Default)]
struct Entry {
pub fn_name: String,
pub filename: Option<String>,
pub line: Option<u32>,
pub col: Option<u32>,
}
backtrace::trace(|frame| {
backtrace::resolve_frame(frame, |symbol| {
if let Some(fn_name) = symbol.name() {
let fn_name = fn_name.to_string();
if should_show_in_backtrace(&fn_name) {
let mut entry: Entry = Entry {
fn_name: format_fn_name(&fn_name),
..Default::default()
};
if let Some(path) = symbol.filename() {
entry.filename = Some(path.to_string_lossy().into_owned());
};
entry.line = symbol.lineno();
entry.col = symbol.colno();
entries.push(entry);
}
} else {
entries.push(Entry {
fn_name: "???".to_string(),
..Default::default()
});
}
});
true // keep going to the next frame
});
for entry in entries {
eprintln!("\t{}", entry.fn_name);
if let Some(filename) = entry.filename {
eprintln!("\t\t{filename}");
}
}
eprintln!("\nOptimizations can make this list inaccurate! If it looks wrong, try running without `--optimize` and with `--linker=legacy`\n");
}
fn should_show_in_backtrace(fn_name: &str) -> bool {
let is_from_rust = fn_name.contains("::");
let is_host_fn = fn_name.starts_with("roc_panic")
|| fn_name.starts_with("_Effect_effect")
|| fn_name.starts_with("_roc__")
|| fn_name.starts_with("rust_main")
|| fn_name == "_main";
!is_from_rust && !is_host_fn
}
fn format_fn_name(fn_name: &str) -> String {
// e.g. convert "_Num_sub_a0c29024d3ec6e3a16e414af99885fbb44fa6182331a70ab4ca0886f93bad5"
// to ["Num", "sub", "a0c29024d3ec6e3a16e414af99885fbb44fa6182331a70ab4ca0886f93bad5"]
let mut pieces_iter = fn_name.split('_');
if let (_, Some(module_name), Some(name)) =
(pieces_iter.next(), pieces_iter.next(), pieces_iter.next())
{
display_roc_fn(module_name, name)
} else {
"???".to_string()
}
}
fn display_roc_fn(module_name: &str, fn_name: &str) -> String {
let module_name = if module_name == "#UserApp" {
"app"
} else {
module_name
};
let fn_name = if fn_name.parse::<u64>().is_ok() {
"(anonymous function)"
} else {
fn_name
};
format!("\u{001B}[36m{module_name}\u{001B}[39m.{fn_name}")
}
/// # Safety
/// This just delegates to libc::memcpy, so it's equally safe.
#[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)
}
/// # Safety
/// This just delegates to libc::memset, so it's equally safe.
#[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)
}

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
use crate::types::{
Accessors, RocFn, RocNum, RocSingleTagPayload, RocStructFields, RocTagUnion, RocType, TypeId,
Types,
Accessors, File, RocFn, RocNum, RocSingleTagPayload, RocStructFields, RocTagUnion, RocType,
TypeId, Types,
};
use indexmap::IndexMap;
use roc_target::{Architecture, TargetInfo};
@ -60,13 +60,13 @@ fn add_decl(impls: &mut Impls, opt_impl: Impl, target_info: TargetInfo, body: St
targets.push(target_info);
}
pub fn emit(types_and_targets: &[(Types, TargetInfo)]) -> String {
let mut buf = String::new();
pub fn emit(types: &[Types]) -> Vec<File> {
let mut buf = std::str::from_utf8(HEADER).unwrap().to_string();
let mut impls: Impls = IndexMap::default();
for (types, target_info) in types_and_targets {
for types in types {
for id in types.sorted_ids() {
add_type(*target_info, id, types, &mut impls);
add_type(types.target(), id, types, &mut impls);
}
}
@ -138,7 +138,10 @@ pub fn emit(types_and_targets: &[(Types, TargetInfo)]) -> String {
}
}
buf
vec![crate::types::File {
name: "mod.rs".to_string(),
content: buf,
}]
}
fn add_type(target_info: TargetInfo, id: TypeId, types: &Types, impls: &mut Impls) {

View file

@ -1,4 +1,5 @@
use crate::enums::Enums;
use crate::roc_type;
use crate::structs::Structs;
use bumpalo::Bump;
use fnv::FnvHashMap;
@ -18,13 +19,20 @@ use roc_mono::{
InLayout, Layout, LayoutCache, LayoutInterner, TLLayoutInterner, UnionLayout,
},
};
use roc_target::TargetInfo;
use roc_target::{Architecture, OperatingSystem, TargetInfo};
use roc_types::{
subs::{Content, FlatType, GetSubsSlice, Label, Subs, UnionLabels, Variable},
types::{AliasKind, RecordField},
};
use std::convert::From;
use std::fmt::Display;
#[derive(Debug, PartialEq, Eq)]
pub struct File {
pub name: String,
pub content: String,
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct TypeId(usize);
@ -38,6 +46,8 @@ impl TypeId {
const MAX: Self = Self(Self::PENDING.0 - 1);
}
// TODO: remove this and instead generate directly into roc_type::Types
// Probably want to fix roc_std::RocDict and update roc_type::Types to use it first.
#[derive(Debug, Clone)]
pub struct Types {
// These are all indexed by TypeId
@ -54,11 +64,13 @@ pub struct Types {
/// 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) -> Self {
pub fn with_capacity(cap: usize, target_info: TargetInfo) -> Self {
Self {
target: target_info,
types: Vec::with_capacity(cap),
types_by_name: FnvHashMap::with_capacity_and_hasher(10, Default::default()),
entry_points: Vec::new(),
@ -78,7 +90,7 @@ impl Types {
layout_cache: LayoutCache<'a>,
target: TargetInfo,
) -> Self {
let mut types = Self::with_capacity(variables.size_hint().0);
let mut types = Self::with_capacity(variables.size_hint().0, target);
let mut env = Env::new(
arena,
subs,
@ -596,6 +608,292 @@ impl Types {
} => unreachable!("Cyclic type definitions: {:?}", nodes_in_cycle),
}
}
pub fn target(&self) -> TargetInfo {
self.target
}
}
impl From<&Types> for roc_type::Types {
fn from(types: &Types) -> Self {
let deps = types
.deps
.iter()
.map(|(k, v)| roc_type::Tuple2::T(k.0 as _, v.iter().map(|x| x.0 as _).collect()))
.collect();
let types_by_name = types
.types_by_name
.iter()
.map(|(k, v)| roc_type::Tuple1::T(k.as_str().into(), v.0 as _))
.collect();
roc_type::Types {
aligns: types.aligns.as_slice().into(),
deps,
sizes: types.sizes.as_slice().into(),
types: types.types.iter().map(|t| t.into()).collect(),
typesByName: types_by_name,
target: types.target.into(),
}
}
}
impl From<&RocType> for roc_type::RocType {
fn from(rc: &RocType) -> Self {
match rc {
RocType::RocStr => roc_type::RocType::RocStr,
RocType::Bool => roc_type::RocType::Bool,
RocType::RocResult(ok, err) => roc_type::RocType::RocResult(ok.0 as _, err.0 as _),
RocType::Num(num_type) => roc_type::RocType::Num(num_type.into()),
RocType::RocList(elem) => roc_type::RocType::RocList(elem.0 as _),
RocType::RocDict(k, v) => roc_type::RocType::RocDict(k.0 as _, v.0 as _),
RocType::RocSet(elem) => roc_type::RocType::RocSet(elem.0 as _),
RocType::RocBox(elem) => roc_type::RocType::RocBox(elem.0 as _),
RocType::TagUnion(union) => roc_type::RocType::TagUnion(union.into()),
RocType::EmptyTagUnion => roc_type::RocType::EmptyTagUnion,
RocType::Struct { name, fields } => roc_type::RocType::Struct(roc_type::R1 {
fields: fields.into(),
name: name.as_str().into(),
}),
RocType::TagUnionPayload { name, fields } => {
roc_type::RocType::TagUnionPayload(roc_type::R1 {
fields: fields.into(),
name: name.as_str().into(),
})
}
RocType::RecursivePointer(elem) => roc_type::RocType::RecursivePointer(elem.0 as _),
RocType::Function(RocFn {
function_name,
extern_name,
args,
lambda_set,
ret,
}) => roc_type::RocType::Function(roc_type::RocFn {
args: args.iter().map(|arg| arg.0 as _).collect(),
functionName: function_name.as_str().into(),
externName: extern_name.as_str().into(),
ret: ret.0 as _,
lambdaSet: lambda_set.0 as _,
}),
RocType::Unit => roc_type::RocType::Unit,
RocType::Unsized => roc_type::RocType::Unsized,
}
}
}
impl From<&RocNum> for roc_type::RocNum {
fn from(rn: &RocNum) -> Self {
match rn {
RocNum::I8 => roc_type::RocNum::I8,
RocNum::U8 => roc_type::RocNum::U8,
RocNum::I16 => roc_type::RocNum::I16,
RocNum::U16 => roc_type::RocNum::U16,
RocNum::I32 => roc_type::RocNum::I32,
RocNum::U32 => roc_type::RocNum::U32,
RocNum::I64 => roc_type::RocNum::I64,
RocNum::U64 => roc_type::RocNum::U64,
RocNum::I128 => roc_type::RocNum::I128,
RocNum::U128 => roc_type::RocNum::U128,
RocNum::F32 => roc_type::RocNum::F32,
RocNum::F64 => roc_type::RocNum::F64,
RocNum::Dec => roc_type::RocNum::Dec,
}
}
}
impl From<&RocTagUnion> for roc_type::RocTagUnion {
fn from(rtu: &RocTagUnion) -> Self {
match rtu {
RocTagUnion::Enumeration { name, tags, size } => {
roc_type::RocTagUnion::Enumeration(roc_type::R5 {
name: name.as_str().into(),
tags: tags.iter().map(|name| name.as_str().into()).collect(),
size: *size,
})
}
RocTagUnion::NonRecursive {
name,
tags,
discriminant_size,
discriminant_offset,
} => roc_type::RocTagUnion::NonRecursive(roc_type::R7 {
name: name.as_str().into(),
tags: tags
.iter()
.map(|(name, payload)| roc_type::R8 {
name: name.as_str().into(),
payload: payload.into(),
})
.collect(),
discriminantSize: *discriminant_size,
discriminantOffset: *discriminant_offset,
}),
RocTagUnion::Recursive {
name,
tags,
discriminant_size,
discriminant_offset,
} => roc_type::RocTagUnion::Recursive(roc_type::R7 {
name: name.as_str().into(),
tags: tags
.iter()
.map(|(name, payload)| roc_type::R8 {
name: name.as_str().into(),
payload: payload.into(),
})
.collect(),
discriminantSize: *discriminant_size,
discriminantOffset: *discriminant_offset,
}),
RocTagUnion::NonNullableUnwrapped {
name,
tag_name,
payload,
} => roc_type::RocTagUnion::NonNullableUnwrapped(roc_type::R6 {
name: name.as_str().into(),
tagName: tag_name.as_str().into(),
payload: payload.0 as _,
}),
RocTagUnion::SingleTagStruct {
name,
tag_name,
payload,
} => roc_type::RocTagUnion::SingleTagStruct(roc_type::R14 {
name: name.as_str().into(),
tagName: tag_name.as_str().into(),
payload: payload.into(),
}),
RocTagUnion::NullableWrapped {
name,
index_of_null_tag,
tags,
discriminant_size,
discriminant_offset,
} => roc_type::RocTagUnion::NullableWrapped(roc_type::R10 {
name: name.as_str().into(),
indexOfNullTag: *index_of_null_tag,
tags: tags
.iter()
.map(|(name, payload)| roc_type::R8 {
name: name.as_str().into(),
payload: payload.into(),
})
.collect(),
discriminantSize: *discriminant_size,
discriminantOffset: *discriminant_offset,
}),
RocTagUnion::NullableUnwrapped {
name,
null_tag,
non_null_tag,
non_null_payload,
null_represents_first_tag,
} => roc_type::RocTagUnion::NullableUnwrapped(roc_type::R9 {
name: name.as_str().into(),
nonNullPayload: non_null_payload.0 as _,
nonNullTag: non_null_tag.as_str().into(),
nullTag: null_tag.as_str().into(),
whichTagIsNull: if *null_represents_first_tag {
roc_type::U2::FirstTagIsNull
} else {
roc_type::U2::SecondTagIsNull
},
}),
}
}
}
impl From<&RocStructFields> for roc_type::RocStructFields {
fn from(struct_fields: &RocStructFields) -> Self {
match struct_fields {
RocStructFields::HasNoClosure { fields } => roc_type::RocStructFields::HasNoClosure(
fields
.iter()
.map(|(name, id)| roc_type::R4 {
name: name.as_str().into(),
id: id.0 as _,
})
.collect(),
),
RocStructFields::HasClosure { fields } => roc_type::RocStructFields::HasClosure(
fields
.iter()
.map(|(name, id, accessors)| roc_type::R2 {
name: name.as_str().into(),
id: id.0 as _,
accessors: roc_type::R3 {
getter: accessors.getter.as_str().into(),
},
})
.collect(),
),
}
}
}
impl From<&RocSingleTagPayload> for roc_type::RocSingleTagPayload {
fn from(struct_fields: &RocSingleTagPayload) -> Self {
match struct_fields {
RocSingleTagPayload::HasNoClosure { payload_fields } => {
roc_type::RocSingleTagPayload::HasNoClosure(
payload_fields
.iter()
.map(|id| roc_type::R16 { id: id.0 as _ })
.collect(),
)
}
RocSingleTagPayload::HasClosure { payload_getters } => {
roc_type::RocSingleTagPayload::HasClosure(
payload_getters
.iter()
.map(|(id, name)| roc_type::R4 {
id: id.0 as _,
name: name.as_str().into(),
})
.collect(),
)
}
}
}
}
impl From<&Option<TypeId>> for roc_type::U1 {
fn from(opt: &Option<TypeId>) -> Self {
match opt {
Some(x) => roc_type::U1::Some(x.0 as _),
None => roc_type::U1::None,
}
}
}
impl From<TargetInfo> for roc_type::Target {
fn from(target: TargetInfo) -> Self {
roc_type::Target {
architecture: target.architecture.into(),
operatingSystem: target.operating_system.into(),
}
}
}
impl From<Architecture> for roc_type::Architecture {
fn from(arch: Architecture) -> Self {
match arch {
Architecture::Aarch32 => roc_type::Architecture::Aarch32,
Architecture::Aarch64 => roc_type::Architecture::Aarch64,
Architecture::Wasm32 => roc_type::Architecture::Wasm32,
Architecture::X86_32 => roc_type::Architecture::X86x32,
Architecture::X86_64 => roc_type::Architecture::X86x64,
}
}
}
impl From<OperatingSystem> for roc_type::OperatingSystem {
fn from(os: OperatingSystem) -> Self {
match os {
OperatingSystem::Windows => roc_type::OperatingSystem::Windows,
OperatingSystem::Unix => roc_type::OperatingSystem::Unix,
OperatingSystem::Wasi => roc_type::OperatingSystem::Wasi,
}
}
}
enum RocTypeOrPending<'a> {

View file

@ -2,7 +2,7 @@ Cargo.lock
Cargo.toml
build.rs
host.c
test_glue.rs
test_glue
roc_externs.rs
main.rs
app

View file

@ -9,6 +9,8 @@ mod helpers;
#[cfg(test)]
mod test_gen_rs {
use crate::helpers::generate_bindings;
use roc_glue::rust_glue::HEADER;
use roc_glue::types::File;
#[test]
fn basic_record_aliased() {
@ -18,30 +20,33 @@ mod test_gen_rs {
main : MyRcd
main = { a: 1u64, b: 2i128 }
"#
"#
);
let full_header = std::str::from_utf8(HEADER).unwrap().to_string() + "\n";
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, Hash, Ord, PartialEq, PartialOrd)]
#[repr(C)]
pub struct MyRcd {
pub b: roc_std::I128,
pub a: u64,
}
"#
)
generate_bindings(module),
vec![File {
name: "mod.rs".to_string(),
content: full_header
+ 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, Hash, Ord, PartialEq, PartialOrd)]
#[repr(C)]
pub struct MyRcd {
pub b: roc_std::I128,
pub a: u64,
}
"#
)
}]
);
}
@ -55,55 +60,58 @@ mod test_gen_rs {
main : Outer
main = { x: { a: 5, b: 24 }, y: "foo", z: [1, 2] }
"#
"#
);
let full_header = std::str::from_utf8(HEADER).unwrap().to_string() + "\n";
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>,
}
generate_bindings(module),
vec![File {
name: "mod.rs".to_string(),
content: full_header
+ 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 = "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,
}
"#
)
#[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,
}
"#
)
}]
);
}
@ -111,27 +119,30 @@ mod test_gen_rs {
fn record_anonymous() {
let module = "main = { a: 1u64, b: 2u128 }";
let full_header = std::str::from_utf8(HEADER).unwrap().to_string() + "\n";
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, Hash, Ord, PartialEq, PartialOrd)]
#[repr(C)]
pub struct R1 {
pub b: roc_std::U128,
pub a: u64,
}
"#
)
generate_bindings(module),
vec![File {
name: "mod.rs".to_string(),
content: full_header
+ 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, Hash, Ord, PartialEq, PartialOrd)]
#[repr(C)]
pub struct R1 {
pub b: roc_std::U128,
pub a: u64,
}
"#
)
}]
);
}
@ -139,52 +150,55 @@ mod test_gen_rs {
fn nested_record_anonymous() {
let module = r#"main = { x: { a: 5u16, b: 24f32 }, y: "foo", z: [1u8, 2] }"#;
let full_header = std::str::from_utf8(HEADER).unwrap().to_string() + "\n";
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>,
}
generate_bindings(module),
vec![File {
name: "mod.rs".to_string(),
content: full_header
+ 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 = "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,
}
"#
)
#[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,
}
"#
)
}]
);
}
@ -196,41 +210,44 @@ mod test_gen_rs {
main : Enumeration
main = Foo
"#
"#
);
let full_header = std::str::from_utf8(HEADER).unwrap().to_string() + "\n";
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, Hash, Ord, PartialEq, PartialOrd)]
#[repr(u8)]
pub enum Enumeration {
Bar = 0,
Blah = 1,
Foo = 2,
}
generate_bindings(module),
vec![File {
name: "mod.rs".to_string(),
content: full_header
+ indoc!(
r#"
#[cfg(any(
target_arch = "arm",
target_arch = "aarch64",
target_arch = "wasm32",
target_arch = "x86",
target_arch = "x86_64"
))]
#[derive(Clone, Copy, Eq, Hash, Ord, 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"),
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

@ -7,7 +7,7 @@ use std::io::Write;
use std::path::PathBuf;
#[allow(dead_code)]
pub fn generate_bindings(decl_src: &str) -> String {
pub fn generate_bindings(decl_src: &str) -> Vec<roc_glue::types::File> {
use tempfile::tempdir;
let mut src = indoc!(
@ -25,7 +25,7 @@ pub fn generate_bindings(decl_src: &str) -> String {
src.push_str(decl_src);
let pairs = {
let types = {
let dir = tempdir().expect("Unable to create tempdir");
let filename = PathBuf::from("platform.roc");
let file_path = dir.path().join(filename);
@ -45,7 +45,7 @@ pub fn generate_bindings(decl_src: &str) -> String {
result.expect("had problems loading")
};
rust_glue::emit(&pairs)
rust_glue::emit(&types)
}
#[allow(dead_code)]

View file

@ -55,6 +55,7 @@ mod glue_cli_run {
)*
#[test]
#[ignore]
fn all_fixtures_have_tests() {
use roc_collections::VecSet;
@ -73,51 +74,51 @@ mod glue_cli_run {
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_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)
// "#),
single_tag_union:"single-tag-union" => indoc!(r#"
tag_union was: SingleTagUnion::OneTag
"#),
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_wrapped:"nullable-wrapped" => indoc!(r#"
tag_union was: StrFingerTree::More("foo", StrFingerTree::More("bar", StrFingerTree::Empty))
`More "small str" (Single "other str")` is: StrFingerTree::More("small str", StrFingerTree::Single("other str"))
`More "small str" Empty` is: StrFingerTree::More("small str", StrFingerTree::Empty)
`Single "small str"` is: StrFingerTree::Single("small str")
`Empty` is: StrFingerTree::Empty
"#),
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
"#),
nonnullable_unwrapped:"nonnullable-unwrapped" => indoc!(r#"
tag_union was: StrRoseTree::Tree(ManuallyDrop { value: StrRoseTree_Tree { f0: "root", f1: [StrRoseTree::Tree(ManuallyDrop { value: StrRoseTree_Tree { f0: "leaf1", f1: [] } }), StrRoseTree::Tree(ManuallyDrop { value: StrRoseTree_Tree { f0: "leaf2", f1: [] } })] } })
Tree "foo" [] is: StrRoseTree::Tree(ManuallyDrop { value: StrRoseTree_Tree { f0: "foo", f1: [] } })
"#),
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: [] }) }
"#),
// 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_wrapped:"nullable-wrapped" => indoc!(r#"
// tag_union was: StrFingerTree::More("foo", StrFingerTree::More("bar", StrFingerTree::Empty))
// `More "small str" (Single "other str")` is: StrFingerTree::More("small str", StrFingerTree::Single("other str"))
// `More "small str" Empty` is: StrFingerTree::More("small str", StrFingerTree::Empty)
// `Single "small str"` is: StrFingerTree::Single("small str")
// `Empty` is: StrFingerTree::Empty
// "#),
// 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
// "#),
// nonnullable_unwrapped:"nonnullable-unwrapped" => indoc!(r#"
// tag_union was: StrRoseTree::Tree(ManuallyDrop { value: StrRoseTree_Tree { f0: "root", f1: [StrRoseTree::Tree(ManuallyDrop { value: StrRoseTree_Tree { f0: "leaf1", f1: [] } }), StrRoseTree::Tree(ManuallyDrop { value: StrRoseTree_Tree { f0: "leaf2", f1: [] } })] } })
// Tree "foo" [] is: StrRoseTree::Tree(ManuallyDrop { value: StrRoseTree_Tree { f0: "foo", f1: [] } })
// "#),
// 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: [] }) }
// "#),
multiple_modules:"multiple-modules" => indoc!(r#"
combined was: Combined { s1: DepStr1::S("hello"), s2: DepStr2::R("world") }
"#),
@ -158,7 +159,7 @@ mod glue_cli_run {
args: I,
) -> Out {
let platform_module_path = platform_dir.join("platform.roc");
let glue_file = platform_dir.join("src").join("test_glue.rs");
let glue_dir = platform_dir.join("src").join("test_glue");
let fixture_templates_dir = platform_dir
.parent()
.unwrap()
@ -173,18 +174,27 @@ mod glue_cli_run {
.unwrap();
// Delete the glue file to make sure we're actually regenerating it!
if glue_file.exists() {
fs::remove_file(&glue_file)
.expect("Unable to remove test_glue.rs in order to regenerate it in the test");
if glue_dir.exists() {
fs::remove_dir_all(&glue_dir)
.expect("Unable to remove test_glue dir in order to regenerate it in the test");
}
// Generate a fresh test_glue.rs for this platform
let rust_glue_spec = fixture_templates_dir
.parent()
.unwrap()
.parent()
.unwrap()
.join("src")
.join("RustGlue.roc");
// Generate a fresh test_glue for this platform
let glue_out = run_glue(
// converting these all to String avoids lifetime issues
std::iter::once("glue".to_string()).chain(
args.into_iter().map(|arg| arg.to_string()).chain([
rust_glue_spec.to_str().unwrap().to_string(),
glue_dir.to_str().unwrap().to_string(),
platform_module_path.to_str().unwrap().to_string(),
glue_file.to_str().unwrap().to_string(),
]),
),
);
@ -202,7 +212,7 @@ mod glue_cli_run {
}
fn run_app<'a, I: IntoIterator<Item = &'a str>>(app_file: &'a Path, args: I) -> Out {
// Generate test_glue.rs for this platform
// Generate test_glue for this platform
let compile_out = run_roc(
// converting these all to String avoids lifetime issues
args.into_iter()