Mov gen to its own crate

This commit is contained in:
Richard Feldman 2020-03-06 18:15:38 -05:00
parent a18e023326
commit 363a7a0abd
16 changed files with 101 additions and 552 deletions

207
Cargo.lock generated
View file

@ -237,12 +237,6 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "fixedbitset"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86d4de0081402f5e88cdac65c8dcdcc73118c1a7a465e2a05f0da05843a8ea33"
[[package]] [[package]]
name = "fnv" name = "fnv"
version = "1.0.6" version = "1.0.6"
@ -255,97 +249,6 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
[[package]]
name = "futures"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c329ae8753502fb44ae4fc2b622fa2a94652c41e795143765ba0927f92ab780"
dependencies = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-channel"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0c77d04ce8edd9cb903932b608268b3fffec4163dc053b3b402bf47eac1f1a8"
dependencies = [
"futures-core",
"futures-sink",
]
[[package]]
name = "futures-core"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f25592f769825e89b92358db00d26f965761e094951ac44d3663ef25b7ac464a"
[[package]]
name = "futures-executor"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f674f3e1bcb15b37284a90cedf55afdba482ab061c407a9c0ebbd0f3109741ba"
dependencies = [
"futures-core",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-io"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a638959aa96152c7a4cddf50fcb1e3fede0583b27157c26e67d6f99904090dc6"
[[package]]
name = "futures-macro"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a5081aa3de1f7542a794a397cde100ed903b0630152d0973479018fd85423a7"
dependencies = [
"proc-macro-hack",
"proc-macro2 1.0.9",
"quote 1.0.3",
"syn 1.0.16",
]
[[package]]
name = "futures-sink"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3466821b4bc114d95b087b850a724c6f83115e929bc88f1fa98a3304a944c8a6"
[[package]]
name = "futures-task"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b0a34e53cf6cdcd0178aa573aed466b646eb3db769570841fda0c7ede375a27"
[[package]]
name = "futures-util"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22766cf25d64306bedf0384da004d05c9974ab104fcc4528f1236181c18004c5"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-macro",
"futures-sink",
"futures-task",
"memchr",
"pin-utils",
"proc-macro-hack",
"proc-macro-nested",
"slab",
]
[[package]] [[package]]
name = "gcc" name = "gcc"
version = "0.3.55" version = "0.3.55"
@ -541,12 +444,6 @@ version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1c601810575c99596d4afc46f78a678c80105117c379eb3650cf99b8a21ce5b" checksum = "b1c601810575c99596d4afc46f78a678c80105117c379eb3650cf99b8a21ce5b"
[[package]]
name = "ordermap"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a86ed3f5f244b372d6b1a00b72ef7f8876d0bc6a78a4c9985c53614041512063"
[[package]] [[package]]
name = "parking_lot" name = "parking_lot"
version = "0.10.0" version = "0.10.0"
@ -571,28 +468,12 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "petgraph"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c3659d1ee90221741f65dd128d9998311b0e40c5d3c23a62445938214abce4f"
dependencies = [
"fixedbitset",
"ordermap",
]
[[package]] [[package]]
name = "pin-project-lite" name = "pin-project-lite"
version = "0.1.4" version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "237844750cfbb86f67afe27eee600dfbbcb6188d734139b534cbfbf4f96792ae" checksum = "237844750cfbb86f67afe27eee600dfbbcb6188d734139b534cbfbf4f96792ae"
[[package]]
name = "pin-utils"
version = "0.1.0-alpha.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5894c618ce612a3fa23881b152b608bafb8c56cfc22f434a3ba3120b40f7b587"
[[package]] [[package]]
name = "pretty_assertions" name = "pretty_assertions"
version = "0.5.1" version = "0.5.1"
@ -614,12 +495,6 @@ dependencies = [
"syn 1.0.16", "syn 1.0.16",
] ]
[[package]]
name = "proc-macro-nested"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "369a6ed065f249a159e06c45752c780bda2fb53c995718f9e484d08daa9eb42e"
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "0.4.30" version = "0.4.30"
@ -856,47 +731,6 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "roc"
version = "0.1.0"
dependencies = [
"bumpalo",
"cranelift",
"cranelift-codegen",
"cranelift-module",
"cranelift-simplejit",
"futures",
"im",
"im-rc",
"indoc",
"inkwell",
"inlinable_string",
"lazy_static",
"log",
"maplit",
"petgraph",
"pretty_assertions",
"quickcheck",
"quickcheck_macros",
"roc_builtins",
"roc_can",
"roc_collections",
"roc_constrain",
"roc_fmt",
"roc_module",
"roc_mono",
"roc_parse",
"roc_problem",
"roc_region",
"roc_solve",
"roc_types",
"roc_unify",
"roc_uniq",
"target-lexicon",
"tokio",
"wyhash",
]
[[package]] [[package]]
name = "roc_builtins" name = "roc_builtins"
version = "0.1.0" version = "0.1.0"
@ -985,6 +819,41 @@ dependencies = [
"roc_types", "roc_types",
] ]
[[package]]
name = "roc_gen"
version = "0.1.0"
dependencies = [
"bumpalo",
"cranelift",
"cranelift-codegen",
"cranelift-module",
"cranelift-simplejit",
"im",
"im-rc",
"indoc",
"inkwell",
"inlinable_string",
"maplit",
"pretty_assertions",
"quickcheck",
"quickcheck_macros",
"roc_builtins",
"roc_can",
"roc_collections",
"roc_constrain",
"roc_module",
"roc_mono",
"roc_parse",
"roc_problem",
"roc_region",
"roc_solve",
"roc_types",
"roc_unify",
"roc_uniq",
"target-lexicon",
"tokio",
]
[[package]] [[package]]
name = "roc_load" name = "roc_load"
version = "0.1.0" version = "0.1.0"
@ -1217,12 +1086,6 @@ dependencies = [
"typenum", "typenum",
] ]
[[package]]
name = "slab"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8"
[[package]] [[package]]
name = "smallvec" name = "smallvec"
version = "1.2.0" version = "1.2.0"

View file

@ -1,7 +1,6 @@
[workspace] [workspace]
members = [ members = [
"compiler",
"compiler/region", "compiler/region",
"compiler/collections", "compiler/collections",
"compiler/module", "compiler/module",
@ -18,6 +17,7 @@ members = [
"compiler/fmt", "compiler/fmt",
"compiler/mono", "compiler/mono",
"compiler/load", "compiler/load",
"compiler/gen",
"vendor/ena", "vendor/ena",
"vendor/pathfinding" "vendor/pathfinding"
] ]

View file

@ -105,3 +105,40 @@ That concludes our original recursive call to `eval`, after which point we'll be
This will work the same way as `Minus` did, and will reduce down to `Int(6)`. This will work the same way as `Minus` did, and will reduce down to `Int(6)`.
## Optimization philosophy
Focus on optimizations which are only safe in the absence of side effects, and leave the rest to LLVM.
This focus may lead to some optimizations becoming transitively in scope. For example, some deforestation
examples in the MSR paper benefit from multiple rounds of interleaved deforestation, beta-reduction, and inlining.
To get those benefits, we'd have to do some inlining and beta-reduction that we could otherwise leave to LLVM's
inlining and constant propagation/folding.
Even if we're doing those things, it may still make sense to have LLVM do a pass for them as well, since
early LLVM optimization passes may unlock later opportunities for inlining and constant propagation/folding.
## Inlining
If a function is called exactly once (it's a helper function), presumably we always want to inline those.
If a function is "small enough" it's probably worth inlining too.
## Fusion
https://www.microsoft.com/en-us/research/wp-content/uploads/2016/07/deforestation-short-cut.pdf
Basic approach:
Do list stuff using `build` passing Cons Nil (like a cons list) and then do foldr/build substitution/reduction.
Afterwards, we can do a separate pass to flatten nested Cons structures into properly initialized RRBTs.
This way we get both deforestation and efficient RRBT construction. Should work for the other collection types too.
It looks like we need to do some amount of inlining and beta reductions on the Roc side, rather than
leaving all of those to LLVM.
Advanced approach:
Express operations like map and filter in terms of toStream and fromStream, to unlock more deforestation.
More info on here:
https://wiki.haskell.org/GHC_optimisations#Fusion

View file

@ -1,30 +1,23 @@
[package] [package]
name = "roc" name = "roc_gen"
version = "0.1.0" version = "0.1.0"
authors = ["Richard Feldman <oss@rtfeldman.com>"] authors = ["Richard Feldman <oss@rtfeldman.com>"]
edition = "2018" edition = "2018"
[dependencies] [dependencies]
roc_collections = { path = "./collections" } roc_collections = { path = "../collections" }
roc_region = { path = "./region" } roc_region = { path = "../region" }
roc_module = { path = "./module" } roc_module = { path = "../module" }
roc_parse = { path = "./parse" } roc_problem = { path = "../problem" }
roc_problem = { path = "./problem" } roc_types = { path = "../types" }
roc_types = { path = "./types" } roc_builtins = { path = "../builtins" }
roc_can = { path = "./can" } roc_constrain = { path = "../constrain" }
roc_builtins = { path = "./builtins" } roc_uniq = { path = "../uniq" }
roc_constrain = { path = "./constrain" } roc_unify = { path = "../unify" }
roc_uniq = { path = "./uniq" } roc_solve = { path = "../solve" }
roc_unify = { path = "./unify" } roc_mono = { path = "../mono" }
roc_solve = { path = "./solve" }
roc_fmt = { path = "./fmt" }
roc_mono = { path = "./mono" }
log = "0.4.8"
petgraph = { version = "0.4.5", optional = true }
im = "14" # im and im-rc should always have the same version! im = "14" # im and im-rc should always have the same version!
im-rc = "14" # im and im-rc should always have the same version! im-rc = "14" # im and im-rc should always have the same version!
wyhash = "0.3"
tokio = { version = "0.2", features = ["blocking", "fs", "sync", "rt-threaded"] }
bumpalo = "2.6" bumpalo = "2.6"
inlinable_string = "0.1.0" inlinable_string = "0.1.0"
# NOTE: Breaking API changes get pushed directly to this Inkwell branch, so be # NOTE: Breaking API changes get pushed directly to this Inkwell branch, so be
@ -34,8 +27,6 @@ inlinable_string = "0.1.0"
# `rev` works locally, it causes an error on GitHub Actions. (It's unclear why, # `rev` works locally, it causes an error on GitHub Actions. (It's unclear why,
# but after several hours of trying unsuccessfully to fix it, `branch` is it.) # but after several hours of trying unsuccessfully to fix it, `branch` is it.)
inkwell = { git = "https://github.com/TheDan64/inkwell", branch = "llvm8-0" } inkwell = { git = "https://github.com/TheDan64/inkwell", branch = "llvm8-0" }
futures = "0.3"
lazy_static = "1.4"
target-lexicon = "0.10" # NOTE: we must use the same version of target-lexicon as cranelift! target-lexicon = "0.10" # NOTE: we must use the same version of target-lexicon as cranelift!
cranelift = "0.59" # All cranelift crates should have the same version! cranelift = "0.59" # All cranelift crates should have the same version!
cranelift-simplejit = "0.59" # All cranelift crates should have the same version! cranelift-simplejit = "0.59" # All cranelift crates should have the same version!
@ -43,8 +34,12 @@ cranelift-module = "0.59" # All cranelift crates should have the same version
cranelift-codegen = "0.59" # All cranelift crates should have the same version! cranelift-codegen = "0.59" # All cranelift crates should have the same version!
[dev-dependencies] [dev-dependencies]
roc_can = { path = "../can" }
roc_parse = { path = "../parse" }
pretty_assertions = "0.5.1 " pretty_assertions = "0.5.1 "
maplit = "1.0.1" maplit = "1.0.1"
indoc = "0.3.3" indoc = "0.3.3"
quickcheck = "0.8" quickcheck = "0.8"
quickcheck_macros = "0.8" quickcheck_macros = "0.8"
tokio = { version = "0.2", features = ["blocking", "fs", "sync", "rt-threaded"] }
bumpalo = "2.6"

View file

@ -10,6 +10,5 @@
// and encouraging shortcuts here creates bad incentives. I would rather temporarily // and encouraging shortcuts here creates bad incentives. I would rather temporarily
// re-enable this when working on performance optimizations than have it block PRs. // re-enable this when working on performance optimizations than have it block PRs.
#![allow(clippy::large_enum_variant)] #![allow(clippy::large_enum_variant)]
pub mod crane; pub mod crane;
pub mod llvm; pub mod llvm;

View file

@ -5,7 +5,7 @@ extern crate indoc;
extern crate bumpalo; extern crate bumpalo;
extern crate inkwell; extern crate inkwell;
extern crate roc; extern crate roc_gen;
mod helpers; mod helpers;
@ -24,12 +24,12 @@ mod test_gen {
use inkwell::passes::PassManager; use inkwell::passes::PassManager;
use inkwell::types::BasicType; use inkwell::types::BasicType;
use inkwell::OptimizationLevel; use inkwell::OptimizationLevel;
use roc::crane::build::{declare_proc, define_proc_body, ScopeEntry};
use roc::crane::convert::type_from_layout;
use roc::crane::imports::define_malloc;
use roc::llvm::build::{build_proc, build_proc_header};
use roc::llvm::convert::basic_type_from_layout;
use roc_collections::all::{ImMap, MutMap}; use roc_collections::all::{ImMap, MutMap};
use roc_gen::crane::build::{declare_proc, define_proc_body, ScopeEntry};
use roc_gen::crane::convert::type_from_layout;
use roc_gen::crane::imports::define_malloc;
use roc_gen::llvm::build::{build_proc, build_proc_header};
use roc_gen::llvm::convert::basic_type_from_layout;
use roc_mono::expr::Expr; use roc_mono::expr::Expr;
use roc_mono::layout::Layout; use roc_mono::layout::Layout;
use roc_types::subs::Subs; use roc_types::subs::Subs;
@ -63,7 +63,7 @@ mod test_gen {
// Compile and add all the Procs before adding main // Compile and add all the Procs before adding main
let mut procs = MutMap::default(); let mut procs = MutMap::default();
let mut env = roc::crane::build::Env { let mut env = roc_gen::crane::build::Env {
arena: &arena, arena: &arena,
subs, subs,
interns, interns,
@ -135,7 +135,7 @@ mod test_gen {
builder.append_block_params_for_function_params(block); builder.append_block_params_for_function_params(block);
let main_body = let main_body =
roc::crane::build::build_expr(&env, &scope, &mut module, &mut builder, &mono_expr, &procs); roc_gen::crane::build::build_expr(&env, &scope, &mut module, &mut builder, &mono_expr, &procs);
builder.ins().return_(&[main_body]); builder.ins().return_(&[main_body]);
// TODO re-enable this once Switch stops making unsealed blocks, e.g. // TODO re-enable this once Switch stops making unsealed blocks, e.g.
@ -210,7 +210,7 @@ mod test_gen {
let pointer_bytes = execution_engine.get_target_data().get_pointer_byte_size(None); let pointer_bytes = execution_engine.get_target_data().get_pointer_byte_size(None);
// Compile and add all the Procs before adding main // Compile and add all the Procs before adding main
let mut env = roc::llvm::build::Env { let mut env = roc_gen::llvm::build::Env {
arena: &arena, arena: &arena,
subs, subs,
builder: &builder, builder: &builder,
@ -265,7 +265,7 @@ mod test_gen {
builder.position_at_end(basic_block); builder.position_at_end(basic_block);
let ret = roc::llvm::build::build_expr( let ret = roc_gen::llvm::build::build_expr(
&env, &env,
&ImMap::default(), &ImMap::default(),
main_fn, main_fn,
@ -346,7 +346,7 @@ mod test_gen {
let pointer_bytes = execution_engine.get_target_data().get_pointer_byte_size(None); let pointer_bytes = execution_engine.get_target_data().get_pointer_byte_size(None);
// Compile and add all the Procs before adding main // Compile and add all the Procs before adding main
let mut env = roc::llvm::build::Env { let mut env = roc_gen::llvm::build::Env {
arena: &arena, arena: &arena,
subs, subs,
builder: &builder, builder: &builder,
@ -401,7 +401,7 @@ mod test_gen {
builder.position_at_end(basic_block); builder.position_at_end(basic_block);
let ret = roc::llvm::build::build_expr( let ret = roc_gen::llvm::build::build_expr(
&env, &env,
&ImMap::default(), &ImMap::default(),
main_fn, main_fn,

View file

@ -1,36 +0,0 @@
// PHILOSOPHY
//
// Focus on optimizations which are only safe in the absence of side effects, and leave the rest to LLVM.
//
// This focus may lead to some optimizations becoming transitively in scope. For example, some deforestation
// examples in the MSR paper benefit from multiple rounds of interleaved deforestation, beta-reduction, and inlining.
// To get those benefits, we'd have to do some inlining and beta-reduction that we could otherwise leave to LLVM's
// inlining and constant propagation/folding.
//
// Even if we're doing those things, it may still make sense to have LLVM do a pass for them as well, since
// early LLVM optimization passes may unlock later opportunities for inlining and constant propagation/folding.
//
// INLINING
//
// If a function is called exactly once (it's a helper function), presumably we always want to inline those.
// If a function is "small enough" it's probably worth inlining too.
//
// FUSION
//
// https://www.microsoft.com/en-us/research/wp-content/uploads/2016/07/deforestation-short-cut.pdf
//
// Basic approach:
//
// Do list stuff using `build` passing Cons Nil (like a cons list) and then do foldr/build substitution/reduction.
// Afterwards, we can do a separate pass to flatten nested Cons structures into properly initialized RRBTs.
// This way we get both deforestation and efficient RRBT construction. Should work for the other collection types too.
//
// It looks like we need to do some amount of inlining and beta reductions on the Roc side, rather than
// leaving all of those to LLVM.
//
// Advanced approach:
//
// Express operations like map and filter in terms of toStream and fromStream, to unlock more deforestation.
// More info on here:
//
// https://wiki.haskell.org/GHC_optimisations#Fusion

View file

@ -1,309 +0,0 @@
use std::alloc::{self, Layout};
use std::fmt;
use std::mem::{self, MaybeUninit};
use std::ptr;
use std::slice;
use std::str;
/// An immutable string whose maximum length is `isize::MAX`. (For convenience,
/// it still returns its length as `usize` since it can't be negative.)
///
/// For larger strings, under the hood this is a struct which stores a
/// pointer and a usize for length (so 16 bytes on a 64-bit system).
///
/// For smaller strings (lengths 0-15 on 64-bit systems, and 0-7 on 32-bit),
/// this uses a "short string optimization" where it stores the entire string
/// in this struct and does not bother allocating on the heap at all.
pub struct RocStr(InnerStr);
/// Roc strings are optimized not to do heap allocations when they are between
/// 0-15 bytes in length on 64-bit little endian systems,
/// and 0-7 bytes on systems that are 32-bit, big endian, or both.
///
/// This optimization relies on the assumption that string lengths are always
/// less than isize::MAX as opposed to usize::MAX. It relies on this because
/// it uses the most significant bit in the most significant byte in the length
/// as a flag for whether it is a short string or a long string. This bit is
/// unused if lengths are below isize::MAX.
///
/// Roc integers are i64, so on 64-bit systems this guarantee necessarily holds
/// from the roc side. On a 32-bit system it might not though. Rust historically
/// had this guarantee, but it might get relaxed. For more on the Rust side, see
/// https://github.com/rust-lang/unsafe-code-guidelines/issues/102
///
/// Since Roc will interpret them as i64, it's important that on 64-bit systems,
/// Rust never sends Roc any length values outsize isize::MAX because they'll
/// be interpreted as negative i64s!
///
/// Anyway, this "is this a short string?" bit is in a convenient location on
/// 64-bit little endian systems. This is because of how Rust's &str is
/// laid out, and memory alignment.
///
/// Rust's &str is laid out as a slice, namely:
///
/// struct RustStr { ptr: *const [u8], length: usize }
///
/// In little endian systems, the bit for detecting short vs long length is
/// the most significant bit of the length field, which is the very last byte
/// in the struct.
///
/// This means if we detect that we are a short string, we can pass a pointer
/// to the entire struct (which is necessarily aligned already), and its first
/// contiguous N bytes represent the bytes in the string, where N is 15 on
/// 64-bit systems and 7 on 32-bit ones. The final byte is the msbyte where
/// we stored the flag, but it doesn't matter what's in that memory because the
/// str's length will be too low to encounter that anyway.
union InnerStr {
raw: [u8; 16],
long: LongStr,
}
#[derive(Copy)]
#[repr(C)]
struct LongStr {
/// It is *crucial* that we have exactly this memory layout!
/// This is the same layout that Rust uses for string slices in memory,
/// which lets us mem::transmute long strings directly into them.
///
/// https://pramode.in/2016/09/13/using-unsafe-tricks-in-rust/
bytes: MaybeUninit<*const u8>,
length: usize,
}
// The bit pattern for an empty string. (1 and then all 0s.)
// Any other bit pattern means this is not an empty string!
#[cfg(target_pointer_width = "64")]
const EMPTY_STRING: usize =
0b1000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000;
#[cfg(target_pointer_width = "32")]
const EMPTY_STRING: usize = 0b1000_0000_0000_0000;
impl RocStr {
#[inline(always)]
pub fn is_empty(&self) -> bool {
unsafe { self.0.long.length == EMPTY_STRING }
}
#[inline(always)]
pub fn empty() -> RocStr {
RocStr(InnerStr {
long: LongStr {
length: EMPTY_STRING,
// empty strings only ever have length set.
bytes: MaybeUninit::uninit(),
},
})
}
pub fn len(&self) -> usize {
let len_msbyte = self.len_msbyte();
if flagged_as_short_string(len_msbyte) {
// Drop the "is this a short string?" flag
let length: u8 = len_msbyte & 0b0111_1111;
length as usize
} else {
unsafe { self.0.long.length }
}
}
/// The most significant byte in the length. We use the last bit of this
/// byte to determine if we are a short string or a long string.
/// If this is a short string, we intentionally set that bit to 1.
#[inline(always)]
#[cfg(all(target_pointer_width = "64", target_endian = "little"))]
fn len_msbyte(&self) -> u8 {
(unsafe { mem::transmute::<usize, [u8; 8]>(self.0.long.length) })[7]
}
#[inline(always)]
#[cfg(all(target_pointer_width = "32", target_endian = "little"))]
fn len_msbyte(&self) -> u8 {
(unsafe { mem::transmute::<usize, [u8; 4]>(self.long.length) })[3]
}
#[inline(always)]
#[cfg(all(target_pointer_width = "64", target_endian = "big"))]
fn len_msbyte(&self) -> u8 {
(unsafe { mem::transmute::<usize, [u8; 8]>(self.long.length) })[0]
}
#[inline(always)]
#[cfg(all(target_pointer_width = "32", target_endian = "big"))]
fn len_msbyte(&self) -> u8 {
(unsafe { mem::transmute::<usize, [u8; 4]>(self.long.length) })[0]
}
}
#[inline(always)]
fn flagged_as_short_string(len_msbyte: u8) -> bool {
// It's a short string iff the first bit of len_msbyte is 1.
len_msbyte & 0b1000_0000 == 0b1000_0000
}
#[inline(always)]
fn with_short_string_flag_enabled(len_msbyte: u8) -> u8 {
// It's a short string iff the first bit of len_msbyte is 1.
len_msbyte | 0b1000_0000
}
impl fmt::Debug for RocStr {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// TODO do this without getting a cloned String involved
let string: String = self.clone().into();
string.fmt(f)
}
}
impl fmt::Display for RocStr {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// TODO do this without getting a cloned String involved
let string: String = self.clone().into();
string.fmt(f)
}
}
impl Clone for LongStr {
fn clone(&self) -> Self {
let length = self.length;
let layout = unsafe { Layout::from_size_align_unchecked(length, 8) };
let old_bytes_ptr = unsafe { self.bytes.assume_init() };
// Allocate memory for the new bytes. (We'll manually drop them later.)
let new_bytes_ptr = unsafe { alloc::alloc(layout) };
unsafe {
ptr::copy_nonoverlapping(old_bytes_ptr, new_bytes_ptr, length);
}
LongStr {
bytes: MaybeUninit::new(new_bytes_ptr),
length,
}
}
}
impl Into<String> for RocStr {
#[cfg(all(target_pointer_width = "64", target_endian = "little"))]
fn into(self) -> String {
let len_msbyte = self.len_msbyte();
// TODO I'm not sure this works the way we want it to. Need to review.
if flagged_as_short_string(len_msbyte) {
// Drop the "is this a short string?" flag
let length: u8 = len_msbyte & 0b0111_1111;
let bytes_ptr = unsafe { &self.0.raw } as *const u8;
// These bytes are already aligned, so we can use them directly.
let bytes_slice: &[u8] = unsafe { slice::from_raw_parts(bytes_ptr, length as usize) };
(unsafe { str::from_utf8_unchecked(bytes_slice) }).to_string()
} else {
// If it's a long string, we already have the exact
// same memory layout as a Rust &str slice.
let str_slice = unsafe { mem::transmute::<[u8; 16], &str>(self.0.raw) };
let string = str_slice.to_string();
// Drop will deallocate the bytes, which we don't want in this case.
// String is using those bytes now!
mem::forget(self);
string
}
}
}
impl From<String> for RocStr {
#[cfg(all(target_pointer_width = "64", target_endian = "little"))]
fn from(string: String) -> RocStr {
if string.is_empty() {
RocStr::empty()
} else {
let str_len = string.len();
if str_len <= 15 {
let mut buffer: [u8; 16] = [0; 16];
// Copy the raw bytes from the string into the buffer.
unsafe {
// Write into the buffer's bytes
ptr::copy_nonoverlapping(string.as_ptr(), buffer.as_ptr() as *mut u8, str_len);
}
// Set the last byte in the buffer to be the length (with flag).
buffer[15] = with_short_string_flag_enabled(string.len() as u8);
RocStr(InnerStr { raw: buffer })
} else {
panic!("TODO: use mem::forget on the string and steal its bytes!");
// let bytes_ptr = string.as_bytes().clone().as_ptr();
// let long = LongStr {
// bytes: MaybeUninit::new(bytes_ptr),
// length: str_len,
// };
// RocStr(InnerStr { long })
}
}
}
}
impl Clone for RocStr {
fn clone(&self) -> Self {
let inner = if flagged_as_short_string(self.len_msbyte()) {
InnerStr {
raw: (unsafe { self.0.raw }),
}
} else {
InnerStr {
long: (unsafe { self.0.long }),
}
};
RocStr(inner)
}
}
impl Drop for RocStr {
fn drop(&mut self) {
// If this is a LongStr, we need to deallocate its bytes.
// Otherwise we would have a memory leak!
if !flagged_as_short_string(self.len_msbyte()) {
let bytes_ptr = unsafe { self.0.long.bytes.assume_init() };
// If this was already dropped previously (most likely because the
// bytes were moved into a String), we shouldn't deallocate them.
if !bytes_ptr.is_null() {
let length = unsafe { self.0.long.length };
let layout = unsafe { Layout::from_size_align_unchecked(length, 8) };
// We don't need to call drop_in_place. We know bytes_ptr points to
// a plain u8 array, so there will for sure be no destructor to run.
unsafe {
alloc::dealloc(bytes_ptr as *mut u8, layout);
}
}
}
}
}
#[cfg(test)]
mod test_roc_str {
use super::RocStr;
#[test]
fn empty_str() {
assert!(RocStr::empty().is_empty());
assert_eq!(RocStr::empty().len(), 0);
}
#[test]
fn fmt() {
assert_eq!("".to_string(), format!("{}", RocStr::empty()));
}
}