Merge branch 'trunk' into dict

This commit is contained in:
Chadtech 2021-01-31 15:46:22 -05:00 committed by GitHub
commit 69fcbf70ea
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
77 changed files with 4818 additions and 862 deletions

View file

@ -1,30 +0,0 @@
on: [pull_request]
name: Benchmarks
env:
CARGO_HOME: /root/.cargo
RUSTUP_HOME: /root/.rustup
BENCH_RESOURCE_PATH: /downloads/resources/
CRITERION_HOME: /criterion/
jobs:
benchmark:
name: benchmark
runs-on: self-hosted
container: vulpix/ubuntu-20-04-benchmarks
timeout-minutes: 60
steps:
- name: add cargo to PATH
run: echo "/root/.cargo/bin" >> $GITHUB_PATH
- name: check toolchain
run: rustup show
- uses: actions/checkout@v2
- name: Expose needed modules
run: cd editor && ./prep_benchmarks.sh
- name: cargo bench
run: cargo bench

View file

@ -9,6 +9,7 @@ env:
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
SCCACHE_S3_USE_SSL: ${{ secrets.SCCACHE_S3_USE_SSL }} SCCACHE_S3_USE_SSL: ${{ secrets.SCCACHE_S3_USE_SSL }}
SCCACHE_REGION: ${{ secrets.SCCACHE_REGION }} SCCACHE_REGION: ${{ secrets.SCCACHE_REGION }}
RUST_BACKTRACE: 1
jobs: jobs:
test: test:

243
Cargo.lock generated
View file

@ -383,6 +383,16 @@ dependencies = [
"syn 1.0.53", "syn 1.0.53",
] ]
[[package]]
name = "clipboard-win"
version = "3.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fdf5e01086b6be750428ba4a40619f847eb2e95756eee84b18e06e5f0b50342"
dependencies = [
"lazy-bytes-cast",
"winapi 0.3.9",
]
[[package]] [[package]]
name = "cloudabi" name = "cloudabi"
version = "0.0.3" version = "0.0.3"
@ -480,6 +490,20 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2df960f5d869b2dd8532793fde43eb5427cceb126c929747a26823ab0eeb536" checksum = "a2df960f5d869b2dd8532793fde43eb5427cceb126c929747a26823ab0eeb536"
[[package]]
name = "copypasta"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4423d79fed83ebd9ab81ec21fa97144300a961782158287dc9bf7eddac37ff0b"
dependencies = [
"clipboard-win",
"objc",
"objc-foundation",
"objc_id",
"smithay-clipboard",
"x11-clipboard",
]
[[package]] [[package]]
name = "core-foundation" name = "core-foundation"
version = "0.7.0" version = "0.7.0"
@ -1504,6 +1528,12 @@ dependencies = [
"winapi-build", "winapi-build",
] ]
[[package]]
name = "lazy-bytes-cast"
version = "5.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10257499f089cd156ad82d0a9cd57d9501fa2c989068992a97eb3c27836f206b"
[[package]] [[package]]
name = "lazy_static" name = "lazy_static"
version = "1.4.0" version = "1.4.0"
@ -1543,9 +1573,9 @@ dependencies = [
[[package]] [[package]]
name = "linked-hash-map" name = "linked-hash-map"
version = "0.5.3" version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8dd5a6d5999d9907cda8ed67bbd137d3af8085216c2ac62de5be860bd41f304a" checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3"
[[package]] [[package]]
name = "llvm-sys" name = "llvm-sys"
@ -1624,6 +1654,15 @@ dependencies = [
"winapi 0.3.9", "winapi 0.3.9",
] ]
[[package]]
name = "memmap2"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b70ca2a6103ac8b665dc150b142ef0e4e89df640c9e6cf295d189c3caebe5a"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "memoffset" name = "memoffset"
version = "0.5.6" version = "0.5.6"
@ -1811,6 +1850,18 @@ dependencies = [
"void", "void",
] ]
[[package]]
name = "nix"
version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83450fe6a6142ddd95fb064b746083fc4ef1705fe81f64a64e1d4b39f54a1055"
dependencies = [
"bitflags",
"cc",
"cfg-if 0.1.10",
"libc",
]
[[package]] [[package]]
name = "nix" name = "nix"
version = "0.19.1" version = "0.19.1"
@ -1823,6 +1874,16 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "nom"
version = "6.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab6f70b46d6325aa300f1c7bb3d470127dfc27806d8ea6bf294ee0ce643ce2b1"
dependencies = [
"memchr",
"version_check",
]
[[package]] [[package]]
name = "num-traits" name = "num-traits"
version = "0.2.14" version = "0.2.14"
@ -1874,6 +1935,17 @@ dependencies = [
"objc_exception", "objc_exception",
] ]
[[package]]
name = "objc-foundation"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9"
dependencies = [
"block",
"objc",
"objc_id",
]
[[package]] [[package]]
name = "objc_exception" name = "objc_exception"
version = "0.1.2" version = "0.1.2"
@ -1883,6 +1955,15 @@ dependencies = [
"cc", "cc",
] ]
[[package]]
name = "objc_id"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b"
dependencies = [
"objc",
]
[[package]] [[package]]
name = "object" name = "object"
version = "0.22.0" version = "0.22.0"
@ -2711,6 +2792,7 @@ dependencies = [
"bytemuck", "bytemuck",
"cgmath", "cgmath",
"colored", "colored",
"copypasta",
"criterion", "criterion",
"env_logger 0.7.1", "env_logger 0.7.1",
"futures", "futures",
@ -2905,6 +2987,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"bumpalo", "bumpalo",
"indoc", "indoc",
"linked-hash-map",
"maplit", "maplit",
"pretty_assertions", "pretty_assertions",
"quickcheck", "quickcheck",
@ -3186,6 +3269,12 @@ dependencies = [
"winapi-util", "winapi-util",
] ]
[[package]]
name = "scoped-tls"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2"
[[package]] [[package]]
name = "scopeguard" name = "scopeguard"
version = "1.1.0" version = "1.1.0"
@ -3337,8 +3426,35 @@ dependencies = [
"lazy_static", "lazy_static",
"memmap", "memmap",
"nix 0.14.1", "nix 0.14.1",
"wayland-client", "wayland-client 0.23.6",
"wayland-protocols", "wayland-protocols 0.23.6",
]
[[package]]
name = "smithay-client-toolkit"
version = "0.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "316e13a3eb853ce7bf72ad3530dc186cb2005c57c521ef5f4ada5ee4eed74de6"
dependencies = [
"bitflags",
"dlib",
"lazy_static",
"log",
"memmap2",
"nix 0.18.0",
"wayland-client 0.28.3",
"wayland-cursor",
"wayland-protocols 0.28.3",
]
[[package]]
name = "smithay-clipboard"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab88b219728cad0697a6c9e75da50bf5220ba01b3485e33e407931539a8ebc91"
dependencies = [
"smithay-client-toolkit 0.12.2",
"wayland-client 0.28.3",
] ]
[[package]] [[package]]
@ -3870,9 +3986,25 @@ dependencies = [
"libc", "libc",
"mio", "mio",
"nix 0.14.1", "nix 0.14.1",
"wayland-commons", "wayland-commons 0.23.6",
"wayland-scanner", "wayland-scanner 0.23.6",
"wayland-sys", "wayland-sys 0.23.6",
]
[[package]]
name = "wayland-client"
version = "0.28.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bdbdbe01d03b2267809f3ed99495b37395387fde789e0f2ebb78e8b43f75b6d7"
dependencies = [
"bitflags",
"downcast-rs",
"libc",
"nix 0.18.0",
"scoped-tls",
"wayland-commons 0.28.3",
"wayland-scanner 0.28.3",
"wayland-sys 0.28.3",
] ]
[[package]] [[package]]
@ -3882,7 +4014,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb66b0d1a27c39bbce712b6372131c6e25149f03ffb0cd017cf8f7de8d66dbdb" checksum = "bb66b0d1a27c39bbce712b6372131c6e25149f03ffb0cd017cf8f7de8d66dbdb"
dependencies = [ dependencies = [
"nix 0.14.1", "nix 0.14.1",
"wayland-sys", "wayland-sys 0.23.6",
]
[[package]]
name = "wayland-commons"
version = "0.28.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "480450f76717edd64ad04a4426280d737fc3d10a236b982df7b1aee19f0e2d56"
dependencies = [
"nix 0.18.0",
"once_cell",
"smallvec",
"wayland-sys 0.28.3",
]
[[package]]
name = "wayland-cursor"
version = "0.28.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6eb122c160223a7660feeaf949d0100281d1279acaaed3720eb3c9894496e5f"
dependencies = [
"nix 0.18.0",
"wayland-client 0.28.3",
"xcursor",
] ]
[[package]] [[package]]
@ -3892,9 +4047,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6cc286643656742777d55dc8e70d144fa4699e426ca8e9d4ef454f4bf15ffcf9" checksum = "6cc286643656742777d55dc8e70d144fa4699e426ca8e9d4ef454f4bf15ffcf9"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"wayland-client", "wayland-client 0.23.6",
"wayland-commons", "wayland-commons 0.23.6",
"wayland-scanner", "wayland-scanner 0.23.6",
]
[[package]]
name = "wayland-protocols"
version = "0.28.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "319a82b4d3054dd25acc32d9aee0f84fa95b63bc983fffe4703b6b8d47e01a30"
dependencies = [
"bitflags",
"wayland-client 0.28.3",
"wayland-commons 0.28.3",
"wayland-scanner 0.28.3",
] ]
[[package]] [[package]]
@ -3908,6 +4075,17 @@ dependencies = [
"xml-rs", "xml-rs",
] ]
[[package]]
name = "wayland-scanner"
version = "0.28.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7010ba5767b3fcd350decc59055390b4ebe6bd1b9279a9feb1f1888987f1133d"
dependencies = [
"proc-macro2 1.0.24",
"quote 1.0.7",
"xml-rs",
]
[[package]] [[package]]
name = "wayland-sys" name = "wayland-sys"
version = "0.23.6" version = "0.23.6"
@ -3918,6 +4096,17 @@ dependencies = [
"lazy_static", "lazy_static",
] ]
[[package]]
name = "wayland-sys"
version = "0.28.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6793834e0c35d11fd96a97297abe03d37be627e1847da52e17d7e0e3b51cc099"
dependencies = [
"dlib",
"lazy_static",
"pkg-config",
]
[[package]] [[package]]
name = "web-sys" name = "web-sys"
version = "0.3.46" version = "0.3.46"
@ -4067,8 +4256,8 @@ dependencies = [
"parking_lot 0.10.2", "parking_lot 0.10.2",
"percent-encoding", "percent-encoding",
"raw-window-handle", "raw-window-handle",
"smithay-client-toolkit", "smithay-client-toolkit 0.6.6",
"wayland-client", "wayland-client 0.23.6",
"winapi 0.3.9", "winapi 0.3.9",
"x11-dl", "x11-dl",
] ]
@ -4111,6 +4300,15 @@ dependencies = [
"pkg-config", "pkg-config",
] ]
[[package]]
name = "x11-clipboard"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5e937afd03b64b7be4f959cc044e09260a47241b71e56933f37db097bf7859d"
dependencies = [
"xcb",
]
[[package]] [[package]]
name = "x11-dl" name = "x11-dl"
version = "2.18.5" version = "2.18.5"
@ -4123,6 +4321,25 @@ dependencies = [
"pkg-config", "pkg-config",
] ]
[[package]]
name = "xcb"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62056f63138b39116f82a540c983cc11f1c90cd70b3d492a70c25eaa50bd22a6"
dependencies = [
"libc",
"log",
]
[[package]]
name = "xcursor"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a9a231574ae78801646617cefd13bfe94be907c0e4fa979cfd8b770aa3c5d08"
dependencies = [
"nom",
]
[[package]] [[package]]
name = "xdg" name = "xdg"
version = "2.2.0" version = "2.2.0"

View file

@ -59,7 +59,7 @@ esac
wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | apt-key add - wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | apt-key add -
add-apt-repository "${REPO_NAME}" add-apt-repository "${REPO_NAME}"
apt-get update apt-get update
apt-get install -y clang-$LLVM_VERSION lldb-$LLVM_VERSION lld-$LLVM_VERSION clangd-$LLVM_VERSION libc++abi-dev libunwind-dev libc6-dbg apt-get install -y clang-$LLVM_VERSION lldb-$LLVM_VERSION lld-$LLVM_VERSION clangd-$LLVM_VERSION libc++abi-dev libunwind-dev libc6-dbg libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev
wget https://sourceware.org/pub/valgrind/valgrind-3.16.1.tar.bz2 wget https://sourceware.org/pub/valgrind/valgrind-3.16.1.tar.bz2
tar -xf valgrind-3.16.1.tar.bz2 tar -xf valgrind-3.16.1.tar.bz2

View file

@ -23,6 +23,24 @@ mod cli_run {
flags: &[&str], flags: &[&str],
expected_ending: &str, expected_ending: &str,
use_valgrind: bool, use_valgrind: bool,
) {
check_output_with_stdin(
file,
"",
executable_filename,
flags,
expected_ending,
use_valgrind,
)
}
fn check_output_with_stdin(
file: &Path,
stdin_str: &str,
executable_filename: &str,
flags: &[&str],
expected_ending: &str,
use_valgrind: bool,
) { ) {
let compile_out = run_roc(&[&["build", file.to_str().unwrap()], flags].concat()); let compile_out = run_roc(&[&["build", file.to_str().unwrap()], flags].concat());
if !compile_out.stderr.is_empty() { if !compile_out.stderr.is_empty() {
@ -31,8 +49,10 @@ mod cli_run {
assert!(compile_out.status.success()); assert!(compile_out.status.success());
let out = if use_valgrind { let out = if use_valgrind {
let (valgrind_out, raw_xml) = let (valgrind_out, raw_xml) = run_with_valgrind(
run_with_valgrind(&[file.with_file_name(executable_filename).to_str().unwrap()]); stdin_str,
&[file.with_file_name(executable_filename).to_str().unwrap()],
);
if valgrind_out.status.success() { if valgrind_out.status.success() {
let memory_errors = extract_valgrind_errors(&raw_xml).unwrap_or_else(|err| { let memory_errors = extract_valgrind_errors(&raw_xml).unwrap_or_else(|err| {
@ -55,6 +75,7 @@ mod cli_run {
} else { } else {
run_cmd( run_cmd(
file.with_file_name(executable_filename).to_str().unwrap(), file.with_file_name(executable_filename).to_str().unwrap(),
stdin_str,
&[], &[],
) )
}; };
@ -174,8 +195,9 @@ mod cli_run {
#[test] #[test]
#[serial(nqueens)] #[serial(nqueens)]
fn run_nqueens_not_optimized() { fn run_nqueens_not_optimized() {
check_output( check_output_with_stdin(
&example_file("benchmarks", "NQueens.roc"), &example_file("benchmarks", "NQueens.roc"),
"",
"nqueens", "nqueens",
&[], &[],
"4\n", "4\n",

View file

@ -1,23 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
[[package]]
name = "host"
version = "0.1.0"
dependencies = [
"roc_std 0.1.0",
]
[[package]]
name = "libc"
version = "0.2.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "roc_std"
version = "0.1.0"
dependencies = [
"libc 0.2.79 (registry+https://github.com/rust-lang/crates.io-index)",
]
[metadata]
"checksum libc 0.2.79 (registry+https://github.com/rust-lang/crates.io-index)" = "2448f6066e80e3bfc792e9c98bf705b4b0fc6e8ef5b43e5889aff0eaa9c58743"

View file

@ -1,13 +0,0 @@
[package]
name = "host"
version = "0.1.0"
authors = ["Richard Feldman <oss@rtfeldman.com>"]
edition = "2018"
[lib]
crate-type = ["staticlib"]
[dependencies]
roc_std = { path = "../../../../../roc_std" }
[workspace]

View file

@ -4,7 +4,7 @@ platform examples/multi-module
packages {} packages {}
imports [] imports []
provides [ mainForHost ] provides [ mainForHost ]
effects Effect {} effects fx.Effect {}
mainForHost : Str mainForHost : Str
mainForHost = main mainForHost = main

View file

@ -1,8 +0,0 @@
# Rebuilding the host from source
Run `build.sh` to manually rebuild this platform's host.
Note that the compiler currently has its own logic for rebuilding these hosts
(in `link.rs`). It's hardcoded for now, but the long-term goal is that
hosts will be precompiled by platform authors and distributed in packages,
at which point only package authors will need to think about rebuilding hosts.

View file

@ -1,12 +0,0 @@
#!/bin/bash
# compile c_host.o and rust_host.o
clang -c host.c -o c_host.o
rustc host.rs -o rust_host.o
# link them together into host.o
ld -r c_host.o rust_host.o -o host.o
# clean up
rm -f c_host.o
rm -f rust_host.o

View file

@ -1,7 +0,0 @@
#include <stdio.h>
extern int rust_main();
int main() {
return rust_main();
}

View file

@ -0,0 +1,49 @@
const std = @import("std");
const str = @import("str.zig");
const RocStr = str.RocStr;
const testing = std.testing;
const expectEqual = testing.expectEqual;
const expect = testing.expect;
const mem = std.mem;
const Allocator = mem.Allocator;
extern fn roc__mainForHost_1_exposed(*RocCallResult) void;
const RocCallResult = extern struct { flag: usize, content: RocStr };
const Unit = extern struct {};
pub export fn main() i32 {
const stdout = std.io.getStdOut().writer();
const stderr = std.io.getStdErr().writer();
// make space for the result
var callresult = RocCallResult{ .flag = 0, .content = RocStr.empty() };
// start time
var ts1: std.os.timespec = undefined;
std.os.clock_gettime(std.os.CLOCK_REALTIME, &ts1) catch unreachable;
// actually call roc to populate the callresult
roc__mainForHost_1_exposed(&callresult);
// stdout the result
stdout.print("{}\n", .{callresult.content.asSlice()}) catch unreachable;
callresult.content.deinit(std.heap.c_allocator);
// end time
var ts2: std.os.timespec = undefined;
std.os.clock_gettime(std.os.CLOCK_REALTIME, &ts2) catch unreachable;
const delta = to_seconds(ts2) - to_seconds(ts1);
stderr.print("runtime: {d:.3}ms\n", .{delta * 1000}) catch unreachable;
return 0;
}
fn to_seconds(tms: std.os.timespec) f64 {
return @intToFloat(f64, tms.tv_sec) + (@intToFloat(f64, tms.tv_nsec) / 1_000_000_000.0);
}

View file

@ -1,28 +0,0 @@
use roc_std::RocCallResult;
use roc_std::RocStr;
use std::str;
extern "C" {
#[link_name = "roc__mainForHost_1_exposed"]
fn say_hello(output: &mut RocCallResult<RocStr>) -> ();
}
#[no_mangle]
pub fn rust_main() -> isize {
let answer = unsafe {
use std::mem::MaybeUninit;
let mut output: MaybeUninit<RocCallResult<RocStr>> = MaybeUninit::uninit();
say_hello(&mut *output.as_mut_ptr());
match output.assume_init().into() {
Ok(value) => value,
Err(msg) => panic!("roc failed with message {}", msg),
}
};
println!("Roc says: {}", str::from_utf8(answer.as_slice()).unwrap());
// Exit code
0
}

View file

@ -0,0 +1,818 @@
const std = @import("std");
const mem = std.mem;
const always_inline = std.builtin.CallOptions.Modifier.always_inline;
const Allocator = mem.Allocator;
const unicode = std.unicode;
const testing = std.testing;
const expectEqual = testing.expectEqual;
const expect = testing.expect;
const InPlace = packed enum(u8) {
InPlace,
Clone,
};
pub const RocStr = extern struct {
str_bytes: ?[*]u8,
str_len: usize,
pub inline fn empty() RocStr {
return RocStr{
.str_len = 0,
.str_bytes = null,
};
}
// This clones the pointed-to bytes if they won't fit in a
// small string, and returns a (pointer, len) tuple which points to them.
pub fn init(allocator: *Allocator, bytes_ptr: [*]const u8, length: usize) RocStr {
const roc_str_size = @sizeOf(RocStr);
if (length < roc_str_size) {
var ret_small_str = RocStr.empty();
const target_ptr = @ptrToInt(&ret_small_str);
var index: u8 = 0;
// TODO isn't there a way to bulk-zero data in Zig?
// Zero out the data, just to be safe
while (index < roc_str_size) {
var offset_ptr = @intToPtr(*u8, target_ptr + index);
offset_ptr.* = 0;
index += 1;
}
// TODO rewrite this into a for loop
index = 0;
while (index < length) {
var offset_ptr = @intToPtr(*u8, target_ptr + index);
offset_ptr.* = bytes_ptr[index];
index += 1;
}
// set the final byte to be the length
const final_byte_ptr = @intToPtr(*u8, target_ptr + roc_str_size - 1);
final_byte_ptr.* = @truncate(u8, length) ^ 0b10000000;
return ret_small_str;
} else {
var result = RocStr.initBig(allocator, u64, InPlace.Clone, length);
@memcpy(@ptrCast([*]u8, result.str_bytes), bytes_ptr, length);
return result;
}
}
pub fn initBig(allocator: *Allocator, comptime T: type, in_place: InPlace, number_of_chars: u64) RocStr {
const length = @sizeOf(T) + number_of_chars;
var new_bytes: []T = allocator.alloc(T, length) catch unreachable;
if (in_place == InPlace.InPlace) {
new_bytes[0] = @intCast(T, number_of_chars);
} else {
new_bytes[0] = std.math.minInt(T);
}
var first_element = @ptrCast([*]align(@alignOf(T)) u8, new_bytes);
first_element += @sizeOf(usize);
return RocStr{
.str_bytes = first_element,
.str_len = number_of_chars,
};
}
pub fn deinit(self: RocStr, allocator: *Allocator) void {
if (!self.isSmallStr() and !self.isEmpty()) {
const str_bytes_ptr: [*]u8 = self.str_bytes orelse unreachable;
// must include refcount
const str_bytes: []u8 = (str_bytes_ptr - 8)[0 .. self.str_len + 8];
allocator.free(str_bytes);
}
}
// This takes ownership of the pointed-to bytes if they won't fit in a
// small string, and returns a (pointer, len) tuple which points to them.
pub fn withCapacity(length: usize) RocStr {
const roc_str_size = @sizeOf(RocStr);
if (length < roc_str_size) {
return RocStr.empty();
} else {
var new_bytes: []T = allocator.alloc(u8, length) catch unreachable;
var new_bytes_ptr: [*]u8 = @ptrCast([*]u8, &new_bytes);
return RocStr{
.str_bytes = new_bytes_ptr,
.str_len = length,
};
}
}
pub fn eq(self: RocStr, other: RocStr) bool {
const self_bytes_ptr: ?[*]const u8 = self.str_bytes;
const other_bytes_ptr: ?[*]const u8 = other.str_bytes;
// If they are byte-for-byte equal, they're definitely equal!
if (self_bytes_ptr == other_bytes_ptr and self.str_len == other.str_len) {
return true;
}
const self_len = self.len();
const other_len = other.len();
// If their lengths are different, they're definitely unequal.
if (self_len != other_len) {
return false;
}
const self_u8_ptr: [*]const u8 = @ptrCast([*]const u8, &self);
const other_u8_ptr: [*]const u8 = @ptrCast([*]const u8, &other);
const self_bytes: [*]const u8 = if (self.isSmallStr() or self.isEmpty()) self_u8_ptr else self_bytes_ptr orelse unreachable;
const other_bytes: [*]const u8 = if (other.isSmallStr() or other.isEmpty()) other_u8_ptr else other_bytes_ptr orelse unreachable;
var index: usize = 0;
// TODO rewrite this into a for loop
const length = self.len();
while (index < length) {
if (self_bytes[index] != other_bytes[index]) {
return false;
}
index = index + 1;
}
return true;
}
pub fn clone(allocator: *Allocator, comptime T: type, in_place: InPlace, str: RocStr) RocStr {
if (str.isSmallStr() or str.isEmpty()) {
// just return the bytes
return str;
} else {
var new_str = RocStr.initBig(allocator, T, in_place, str.str_len);
var old_bytes: [*]u8 = @ptrCast([*]u8, str.str_bytes);
var new_bytes: [*]u8 = @ptrCast([*]u8, new_str.str_bytes);
@memcpy(new_bytes, old_bytes, str.str_len);
return new_str;
}
}
pub fn isSmallStr(self: RocStr) bool {
return @bitCast(isize, self.str_len) < 0;
}
pub fn len(self: RocStr) usize {
const bytes: [*]const u8 = @ptrCast([*]const u8, &self);
const last_byte = bytes[@sizeOf(RocStr) - 1];
const small_len = @as(usize, last_byte ^ 0b1000_0000);
const big_len = self.str_len;
// Since this conditional would be prone to branch misprediction,
// make sure it will compile to a cmov.
return if (self.isSmallStr()) small_len else big_len;
}
pub fn isEmpty(self: RocStr) bool {
return self.len() == 0;
}
pub fn asSlice(self: RocStr) []u8 {
// Since this conditional would be prone to branch misprediction,
// make sure it will compile to a cmov.
return self.asU8ptr()[0..self.len()];
}
pub fn asU8ptr(self: RocStr) [*]u8 {
// Since this conditional would be prone to branch misprediction,
// make sure it will compile to a cmov.
return if (self.isSmallStr() or self.isEmpty()) (&@bitCast([16]u8, self)) else (@ptrCast([*]u8, self.str_bytes));
}
// Given a pointer to some bytes, write the first (len) bytes of this
// RocStr's contents into it.
//
// One use for this function is writing into an `alloca` for a C string that
// only needs to live long enough to be passed as an argument to
// a C function - like the file path argument to `fopen`.
pub fn memcpy(self: RocStr, dest: [*]u8, length: usize) void {
const src = self.asU8ptr();
@memcpy(dest, src, length);
}
test "RocStr.eq: equal" {
const str1_len = 3;
var str1: [str1_len]u8 = "abc".*;
const str1_ptr: [*]u8 = &str1;
var roc_str1 = RocStr.init(testing.allocator, str1_ptr, str1_len);
const str2_len = 3;
var str2: [str2_len]u8 = "abc".*;
const str2_ptr: [*]u8 = &str2;
var roc_str2 = RocStr.init(testing.allocator, str2_ptr, str2_len);
expect(roc_str1.eq(roc_str2));
roc_str1.deinit(testing.allocator);
roc_str2.deinit(testing.allocator);
}
test "RocStr.eq: not equal different length" {
const str1_len = 4;
var str1: [str1_len]u8 = "abcd".*;
const str1_ptr: [*]u8 = &str1;
var roc_str1 = RocStr.init(testing.allocator, str1_ptr, str1_len);
const str2_len = 3;
var str2: [str2_len]u8 = "abc".*;
const str2_ptr: [*]u8 = &str2;
var roc_str2 = RocStr.init(testing.allocator, str2_ptr, str2_len);
defer {
roc_str1.deinit(testing.allocator);
roc_str2.deinit(testing.allocator);
}
expect(!roc_str1.eq(roc_str2));
}
test "RocStr.eq: not equal same length" {
const str1_len = 3;
var str1: [str1_len]u8 = "acb".*;
const str1_ptr: [*]u8 = &str1;
var roc_str1 = RocStr.init(testing.allocator, str1_ptr, str1_len);
const str2_len = 3;
var str2: [str2_len]u8 = "abc".*;
const str2_ptr: [*]u8 = &str2;
var roc_str2 = RocStr.init(testing.allocator, str2_ptr, str2_len);
defer {
roc_str1.deinit(testing.allocator);
roc_str2.deinit(testing.allocator);
}
expect(!roc_str1.eq(roc_str2));
}
};
// Str.equal
pub fn strEqual(self: RocStr, other: RocStr) callconv(.C) bool {
return self.eq(other);
}
// Str.numberOfBytes
pub fn strNumberOfBytes(string: RocStr) callconv(.C) usize {
return string.len();
}
// Str.fromInt
// When we actually use this in Roc, libc will be linked so we have access to std.heap.c_allocator
pub fn strFromIntC(int: i64) callconv(.C) RocStr {
return strFromInt(std.heap.c_allocator, int);
}
fn strFromInt(allocator: *Allocator, int: i64) RocStr {
// prepare for having multiple integer types in the future
return @call(.{ .modifier = always_inline }, strFromIntHelp, .{ allocator, i64, int });
}
fn strFromIntHelp(allocator: *Allocator, comptime T: type, int: T) RocStr {
// determine maximum size for this T
comptime const size = comptime blk: {
// the string representation of the minimum i128 value uses at most 40 characters
var buf: [40]u8 = undefined;
var result = std.fmt.bufPrint(&buf, "{}", .{std.math.minInt(T)}) catch unreachable;
break :blk result.len;
};
var buf: [size]u8 = undefined;
const result = std.fmt.bufPrint(&buf, "{}", .{int}) catch unreachable;
return RocStr.init(allocator, &buf, result.len);
}
// Str.split
// When we actually use this in Roc, libc will be linked so we have access to std.heap.c_allocator
pub fn strSplitInPlaceC(array: [*]RocStr, string: RocStr, delimiter: RocStr) callconv(.C) void {
return @call(.{ .modifier = always_inline }, strSplitInPlace, .{ std.heap.c_allocator, array, string, delimiter });
}
fn strSplitInPlace(allocator: *Allocator, array: [*]RocStr, string: RocStr, delimiter: RocStr) void {
var ret_array_index: usize = 0;
var slice_start_index: usize = 0;
var str_index: usize = 0;
const str_bytes = string.asU8ptr();
const str_len = string.len();
const delimiter_bytes_ptrs = delimiter.asU8ptr();
const delimiter_len = delimiter.len();
if (str_len > delimiter_len) {
const end_index: usize = str_len - delimiter_len + 1;
while (str_index <= end_index) {
var delimiter_index: usize = 0;
var matches_delimiter = true;
while (delimiter_index < delimiter_len) {
var delimiterChar = delimiter_bytes_ptrs[delimiter_index];
var strChar = str_bytes[str_index + delimiter_index];
if (delimiterChar != strChar) {
matches_delimiter = false;
break;
}
delimiter_index += 1;
}
if (matches_delimiter) {
const segment_len: usize = str_index - slice_start_index;
array[ret_array_index] = RocStr.init(allocator, str_bytes + slice_start_index, segment_len);
slice_start_index = str_index + delimiter_len;
ret_array_index += 1;
str_index += delimiter_len;
} else {
str_index += 1;
}
}
}
array[ret_array_index] = RocStr.init(allocator, str_bytes + slice_start_index, str_len - slice_start_index);
}
test "strSplitInPlace: no delimiter" {
// Str.split "abc" "!" == [ "abc" ]
const str_arr = "abc";
const str = RocStr.init(testing.allocator, str_arr, str_arr.len);
const delimiter_arr = "!";
const delimiter = RocStr.init(testing.allocator, delimiter_arr, delimiter_arr.len);
var array: [1]RocStr = undefined;
const array_ptr: [*]RocStr = &array;
strSplitInPlace(testing.allocator, array_ptr, str, delimiter);
var expected = [1]RocStr{
str,
};
defer {
for (array) |roc_str| {
roc_str.deinit(testing.allocator);
}
for (expected) |roc_str| {
roc_str.deinit(testing.allocator);
}
str.deinit(testing.allocator);
delimiter.deinit(testing.allocator);
}
expectEqual(array.len, expected.len);
expect(array[0].eq(expected[0]));
}
test "strSplitInPlace: empty end" {
const str_arr = "1---- ---- ---- ---- ----2---- ---- ---- ---- ----";
const str = RocStr.init(testing.allocator, str_arr, str_arr.len);
const delimiter_arr = "---- ---- ---- ---- ----";
const delimiter = RocStr.init(testing.allocator, delimiter_arr, delimiter_arr.len);
const array_len: usize = 3;
var array: [array_len]RocStr = [_]RocStr{
undefined,
undefined,
undefined,
};
const array_ptr: [*]RocStr = &array;
strSplitInPlace(testing.allocator, array_ptr, str, delimiter);
const one = RocStr.init(testing.allocator, "1", 1);
const two = RocStr.init(testing.allocator, "2", 1);
var expected = [3]RocStr{
one, two, RocStr.empty(),
};
defer {
for (array) |rocStr| {
rocStr.deinit(testing.allocator);
}
for (expected) |rocStr| {
rocStr.deinit(testing.allocator);
}
str.deinit(testing.allocator);
delimiter.deinit(testing.allocator);
}
expectEqual(array.len, expected.len);
expect(array[0].eq(expected[0]));
expect(array[1].eq(expected[1]));
expect(array[2].eq(expected[2]));
}
test "strSplitInPlace: delimiter on sides" {
const str_arr = "tttghittt";
const str = RocStr.init(testing.allocator, str_arr, str_arr.len);
const delimiter_arr = "ttt";
const delimiter = RocStr.init(testing.allocator, delimiter_arr, delimiter_arr.len);
const array_len: usize = 3;
var array: [array_len]RocStr = [_]RocStr{
undefined,
undefined,
undefined,
};
const array_ptr: [*]RocStr = &array;
strSplitInPlace(testing.allocator, array_ptr, str, delimiter);
const ghi_arr = "ghi";
const ghi = RocStr.init(testing.allocator, ghi_arr, ghi_arr.len);
var expected = [3]RocStr{
RocStr.empty(), ghi, RocStr.empty(),
};
defer {
for (array) |rocStr| {
rocStr.deinit(testing.allocator);
}
for (expected) |rocStr| {
rocStr.deinit(testing.allocator);
}
str.deinit(testing.allocator);
delimiter.deinit(testing.allocator);
}
expectEqual(array.len, expected.len);
expect(array[0].eq(expected[0]));
expect(array[1].eq(expected[1]));
expect(array[2].eq(expected[2]));
}
test "strSplitInPlace: three pieces" {
// Str.split "a!b!c" "!" == [ "a", "b", "c" ]
const str_arr = "a!b!c";
const str = RocStr.init(testing.allocator, str_arr, str_arr.len);
const delimiter_arr = "!";
const delimiter = RocStr.init(testing.allocator, delimiter_arr, delimiter_arr.len);
const array_len: usize = 3;
var array: [array_len]RocStr = undefined;
const array_ptr: [*]RocStr = &array;
strSplitInPlace(testing.allocator, array_ptr, str, delimiter);
const a = RocStr.init(testing.allocator, "a", 1);
const b = RocStr.init(testing.allocator, "b", 1);
const c = RocStr.init(testing.allocator, "c", 1);
var expected_array = [array_len]RocStr{
a, b, c,
};
defer {
for (array) |roc_str| {
roc_str.deinit(testing.allocator);
}
for (expected_array) |roc_str| {
roc_str.deinit(testing.allocator);
}
str.deinit(testing.allocator);
delimiter.deinit(testing.allocator);
}
expectEqual(expected_array.len, array.len);
expect(array[0].eq(expected_array[0]));
expect(array[1].eq(expected_array[1]));
expect(array[2].eq(expected_array[2]));
}
// This is used for `Str.split : Str, Str -> Array Str
// It is used to count how many segments the input `_str`
// needs to be broken into, so that we can allocate a array
// of that size. It always returns at least 1.
pub fn countSegments(string: RocStr, delimiter: RocStr) callconv(.C) usize {
const str_bytes = string.asU8ptr();
const str_len = string.len();
const delimiter_bytes_ptrs = delimiter.asU8ptr();
const delimiter_len = delimiter.len();
var count: usize = 1;
if (str_len > delimiter_len) {
var str_index: usize = 0;
const end_cond: usize = str_len - delimiter_len + 1;
while (str_index < end_cond) {
var delimiter_index: usize = 0;
var matches_delimiter = true;
while (delimiter_index < delimiter_len) {
const delimiterChar = delimiter_bytes_ptrs[delimiter_index];
const strChar = str_bytes[str_index + delimiter_index];
if (delimiterChar != strChar) {
matches_delimiter = false;
break;
}
delimiter_index += 1;
}
if (matches_delimiter) {
count += 1;
}
str_index += 1;
}
}
return count;
}
test "countSegments: long delimiter" {
// Str.split "str" "delimiter" == [ "str" ]
// 1 segment
const str_arr = "str";
const str = RocStr.init(testing.allocator, str_arr, str_arr.len);
const delimiter_arr = "delimiter";
const delimiter = RocStr.init(testing.allocator, delimiter_arr, delimiter_arr.len);
defer {
str.deinit(testing.allocator);
delimiter.deinit(testing.allocator);
}
const segments_count = countSegments(str, delimiter);
expectEqual(segments_count, 1);
}
test "countSegments: delimiter at start" {
// Str.split "hello there" "hello" == [ "", " there" ]
// 2 segments
const str_arr = "hello there";
const str = RocStr.init(testing.allocator, str_arr, str_arr.len);
const delimiter_arr = "hello";
const delimiter = RocStr.init(testing.allocator, delimiter_arr, delimiter_arr.len);
defer {
str.deinit(testing.allocator);
delimiter.deinit(testing.allocator);
}
const segments_count = countSegments(str, delimiter);
expectEqual(segments_count, 2);
}
test "countSegments: delimiter interspered" {
// Str.split "a!b!c" "!" == [ "a", "b", "c" ]
// 3 segments
const str_arr = "a!b!c";
const str = RocStr.init(testing.allocator, str_arr, str_arr.len);
const delimiter_arr = "!";
const delimiter = RocStr.init(testing.allocator, delimiter_arr, delimiter_arr.len);
defer {
str.deinit(testing.allocator);
delimiter.deinit(testing.allocator);
}
const segments_count = countSegments(str, delimiter);
expectEqual(segments_count, 3);
}
fn rocStrFromLiteral(bytes_arr: *const []u8) RocStr {}
// Str.startsWith
pub fn startsWith(string: RocStr, prefix: RocStr) callconv(.C) bool {
const bytes_len = string.len();
const bytes_ptr = string.asU8ptr();
const prefix_len = prefix.len();
const prefix_ptr = prefix.asU8ptr();
if (prefix_len > bytes_len) {
return false;
}
// we won't exceed bytes_len due to the previous check
var i: usize = 0;
while (i < prefix_len) {
if (bytes_ptr[i] != prefix_ptr[i]) {
return false;
}
i += 1;
}
return true;
}
test "startsWith: foo starts with fo" {
const foo = RocStr.init(testing.allocator, "foo", 3);
const fo = RocStr.init(testing.allocator, "fo", 2);
expect(startsWith(foo, fo));
}
test "startsWith: 123456789123456789 starts with 123456789123456789" {
const str = RocStr.init(testing.allocator, "123456789123456789", 18);
defer str.deinit(testing.allocator);
expect(startsWith(str, str));
}
test "startsWith: 12345678912345678910 starts with 123456789123456789" {
const str = RocStr.init(testing.allocator, "12345678912345678910", 20);
defer str.deinit(testing.allocator);
const prefix = RocStr.init(testing.allocator, "123456789123456789", 18);
defer prefix.deinit(testing.allocator);
expect(startsWith(str, prefix));
}
// Str.endsWith
pub fn endsWith(string: RocStr, suffix: RocStr) callconv(.C) bool {
const bytes_len = string.len();
const bytes_ptr = string.asU8ptr();
const suffix_len = suffix.len();
const suffix_ptr = suffix.asU8ptr();
if (suffix_len > bytes_len) {
return false;
}
const offset: usize = bytes_len - suffix_len;
var i: usize = 0;
while (i < suffix_len) {
if (bytes_ptr[i + offset] != suffix_ptr[i]) {
return false;
}
i += 1;
}
return true;
}
test "endsWith: foo ends with oo" {
const foo = RocStr.init(testing.allocator, "foo", 3);
const oo = RocStr.init(testing.allocator, "oo", 2);
defer foo.deinit(testing.allocator);
defer oo.deinit(testing.allocator);
expect(endsWith(foo, oo));
}
test "endsWith: 123456789123456789 ends with 123456789123456789" {
const str = RocStr.init(testing.allocator, "123456789123456789", 18);
defer str.deinit(testing.allocator);
expect(endsWith(str, str));
}
test "endsWith: 12345678912345678910 ends with 345678912345678910" {
const str = RocStr.init(testing.allocator, "12345678912345678910", 20);
const suffix = RocStr.init(testing.allocator, "345678912345678910", 18);
defer str.deinit(testing.allocator);
defer suffix.deinit(testing.allocator);
expect(endsWith(str, suffix));
}
test "endsWith: hello world ends with world" {
const str = RocStr.init(testing.allocator, "hello world", 11);
const suffix = RocStr.init(testing.allocator, "world", 5);
defer str.deinit(testing.allocator);
defer suffix.deinit(testing.allocator);
expect(endsWith(str, suffix));
}
// Str.concat
// When we actually use this in Roc, libc will be linked so we have access to std.heap.c_allocator
pub fn strConcatC(ptr_size: u32, result_in_place: InPlace, arg1: RocStr, arg2: RocStr) callconv(.C) RocStr {
return @call(.{ .modifier = always_inline }, strConcat, .{ std.heap.c_allocator, ptr_size, result_in_place, arg1, arg2 });
}
fn strConcat(allocator: *Allocator, ptr_size: u32, result_in_place: InPlace, arg1: RocStr, arg2: RocStr) RocStr {
return switch (ptr_size) {
4 => strConcatHelp(allocator, i32, result_in_place, arg1, arg2),
8 => strConcatHelp(allocator, i64, result_in_place, arg1, arg2),
else => unreachable,
};
}
fn strConcatHelp(allocator: *Allocator, comptime T: type, result_in_place: InPlace, arg1: RocStr, arg2: RocStr) RocStr {
if (arg1.isEmpty()) {
return RocStr.clone(allocator, T, result_in_place, arg2);
} else if (arg2.isEmpty()) {
return RocStr.clone(allocator, T, result_in_place, arg1);
} else {
const combined_length = arg1.len() + arg2.len();
const small_str_bytes = 2 * @sizeOf(T);
const result_is_big = combined_length >= small_str_bytes;
if (result_is_big) {
var result = RocStr.initBig(allocator, T, result_in_place, combined_length);
{
const old_bytes = arg1.asU8ptr();
const new_bytes: [*]u8 = @ptrCast([*]u8, result.str_bytes);
@memcpy(new_bytes, old_bytes, arg1.len());
}
{
const old_bytes = arg2.asU8ptr();
const new_bytes = @ptrCast([*]u8, result.str_bytes) + arg1.len();
@memcpy(new_bytes, old_bytes, arg2.len());
}
return result;
} else {
var result = [16]u8{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
// if the result is small, then for sure arg1 and arg2 are also small
{
var old_bytes: [*]u8 = @ptrCast([*]u8, &@bitCast([16]u8, arg1));
var new_bytes: [*]u8 = @ptrCast([*]u8, &result);
@memcpy(new_bytes, old_bytes, arg1.len());
}
{
var old_bytes: [*]u8 = @ptrCast([*]u8, &@bitCast([16]u8, arg2));
var new_bytes = @ptrCast([*]u8, &result) + arg1.len();
@memcpy(new_bytes, old_bytes, arg2.len());
}
const mask: u8 = 0b1000_0000;
const final_byte = @truncate(u8, combined_length) | mask;
result[small_str_bytes - 1] = final_byte;
return @bitCast(RocStr, result);
}
return result;
}
}
test "RocStr.concat: small concat small" {
const str1_len = 3;
var str1: [str1_len]u8 = "foo".*;
const str1_ptr: [*]u8 = &str1;
var roc_str1 = RocStr.init(testing.allocator, str1_ptr, str1_len);
const str2_len = 3;
var str2: [str2_len]u8 = "abc".*;
const str2_ptr: [*]u8 = &str2;
var roc_str2 = RocStr.init(testing.allocator, str2_ptr, str2_len);
const str3_len = 6;
var str3: [str3_len]u8 = "fooabc".*;
const str3_ptr: [*]u8 = &str3;
var roc_str3 = RocStr.init(testing.allocator, str3_ptr, str3_len);
defer {
roc_str1.deinit(testing.allocator);
roc_str2.deinit(testing.allocator);
roc_str3.deinit(testing.allocator);
}
const result = strConcat(testing.allocator, 8, InPlace.Clone, roc_str1, roc_str2);
defer result.deinit(testing.allocator);
expect(roc_str3.eq(result));
}

View file

@ -1,23 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
[[package]]
name = "host"
version = "0.1.0"
dependencies = [
"roc_std 0.1.0",
]
[[package]]
name = "libc"
version = "0.2.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "roc_std"
version = "0.1.0"
dependencies = [
"libc 0.2.79 (registry+https://github.com/rust-lang/crates.io-index)",
]
[metadata]
"checksum libc 0.2.79 (registry+https://github.com/rust-lang/crates.io-index)" = "2448f6066e80e3bfc792e9c98bf705b4b0fc6e8ef5b43e5889aff0eaa9c58743"

View file

@ -1,13 +0,0 @@
[package]
name = "host"
version = "0.1.0"
authors = ["Richard Feldman <oss@rtfeldman.com>"]
edition = "2018"
[lib]
crate-type = ["staticlib"]
[dependencies]
roc_std = { path = "../../../../../roc_std" }
[workspace]

View file

@ -4,7 +4,7 @@ platform examples/multi-dep-thunk
packages {} packages {}
imports [] imports []
provides [ mainForHost ] provides [ mainForHost ]
effects Effect {} effects fx.Effect {}
mainForHost : Str mainForHost : Str
mainForHost = main mainForHost = main

View file

@ -1,8 +0,0 @@
# Rebuilding the host from source
Run `build.sh` to manually rebuild this platform's host.
Note that the compiler currently has its own logic for rebuilding these hosts
(in `link.rs`). It's hardcoded for now, but the long-term goal is that
hosts will be precompiled by platform authors and distributed in packages,
at which point only package authors will need to think about rebuilding hosts.

View file

@ -1,12 +0,0 @@
#!/bin/bash
# compile c_host.o and rust_host.o
clang -c host.c -o c_host.o
rustc host.rs -o rust_host.o
# link them together into host.o
ld -r c_host.o rust_host.o -o host.o
# clean up
rm -f c_host.o
rm -f rust_host.o

View file

@ -1,7 +0,0 @@
#include <stdio.h>
extern int rust_main();
int main() {
return rust_main();
}

View file

@ -0,0 +1,49 @@
const std = @import("std");
const str = @import("str.zig");
const RocStr = str.RocStr;
const testing = std.testing;
const expectEqual = testing.expectEqual;
const expect = testing.expect;
const mem = std.mem;
const Allocator = mem.Allocator;
extern fn roc__mainForHost_1_exposed(*RocCallResult) void;
const RocCallResult = extern struct { flag: usize, content: RocStr };
const Unit = extern struct {};
pub export fn main() i32 {
const stdout = std.io.getStdOut().writer();
const stderr = std.io.getStdErr().writer();
// make space for the result
var callresult = RocCallResult{ .flag = 0, .content = RocStr.empty() };
// start time
var ts1: std.os.timespec = undefined;
std.os.clock_gettime(std.os.CLOCK_REALTIME, &ts1) catch unreachable;
// actually call roc to populate the callresult
roc__mainForHost_1_exposed(&callresult);
// stdout the result
stdout.print("{}\n", .{callresult.content.asSlice()}) catch unreachable;
callresult.content.deinit(std.heap.c_allocator);
// end time
var ts2: std.os.timespec = undefined;
std.os.clock_gettime(std.os.CLOCK_REALTIME, &ts2) catch unreachable;
const delta = to_seconds(ts2) - to_seconds(ts1);
stderr.print("runtime: {d:.3}ms\n", .{delta * 1000}) catch unreachable;
return 0;
}
fn to_seconds(tms: std.os.timespec) f64 {
return @intToFloat(f64, tms.tv_sec) + (@intToFloat(f64, tms.tv_nsec) / 1_000_000_000.0);
}

View file

@ -1,28 +0,0 @@
use roc_std::RocCallResult;
use roc_std::RocStr;
use std::str;
extern "C" {
#[link_name = "roc__mainForHost_1_exposed"]
fn say_hello(output: &mut RocCallResult<RocStr>) -> ();
}
#[no_mangle]
pub fn rust_main() -> isize {
let answer = unsafe {
use std::mem::MaybeUninit;
let mut output: MaybeUninit<RocCallResult<RocStr>> = MaybeUninit::uninit();
say_hello(&mut *output.as_mut_ptr());
match output.assume_init().into() {
Ok(value) => value,
Err(msg) => panic!("roc failed with message {}", msg),
}
};
println!("Roc says: {}", str::from_utf8(answer.as_slice()).unwrap());
// Exit code
0
}

View file

@ -0,0 +1,818 @@
const std = @import("std");
const mem = std.mem;
const always_inline = std.builtin.CallOptions.Modifier.always_inline;
const Allocator = mem.Allocator;
const unicode = std.unicode;
const testing = std.testing;
const expectEqual = testing.expectEqual;
const expect = testing.expect;
const InPlace = packed enum(u8) {
InPlace,
Clone,
};
pub const RocStr = extern struct {
str_bytes: ?[*]u8,
str_len: usize,
pub inline fn empty() RocStr {
return RocStr{
.str_len = 0,
.str_bytes = null,
};
}
// This clones the pointed-to bytes if they won't fit in a
// small string, and returns a (pointer, len) tuple which points to them.
pub fn init(allocator: *Allocator, bytes_ptr: [*]const u8, length: usize) RocStr {
const roc_str_size = @sizeOf(RocStr);
if (length < roc_str_size) {
var ret_small_str = RocStr.empty();
const target_ptr = @ptrToInt(&ret_small_str);
var index: u8 = 0;
// TODO isn't there a way to bulk-zero data in Zig?
// Zero out the data, just to be safe
while (index < roc_str_size) {
var offset_ptr = @intToPtr(*u8, target_ptr + index);
offset_ptr.* = 0;
index += 1;
}
// TODO rewrite this into a for loop
index = 0;
while (index < length) {
var offset_ptr = @intToPtr(*u8, target_ptr + index);
offset_ptr.* = bytes_ptr[index];
index += 1;
}
// set the final byte to be the length
const final_byte_ptr = @intToPtr(*u8, target_ptr + roc_str_size - 1);
final_byte_ptr.* = @truncate(u8, length) ^ 0b10000000;
return ret_small_str;
} else {
var result = RocStr.initBig(allocator, u64, InPlace.Clone, length);
@memcpy(@ptrCast([*]u8, result.str_bytes), bytes_ptr, length);
return result;
}
}
pub fn initBig(allocator: *Allocator, comptime T: type, in_place: InPlace, number_of_chars: u64) RocStr {
const length = @sizeOf(T) + number_of_chars;
var new_bytes: []T = allocator.alloc(T, length) catch unreachable;
if (in_place == InPlace.InPlace) {
new_bytes[0] = @intCast(T, number_of_chars);
} else {
new_bytes[0] = std.math.minInt(T);
}
var first_element = @ptrCast([*]align(@alignOf(T)) u8, new_bytes);
first_element += @sizeOf(usize);
return RocStr{
.str_bytes = first_element,
.str_len = number_of_chars,
};
}
pub fn deinit(self: RocStr, allocator: *Allocator) void {
if (!self.isSmallStr() and !self.isEmpty()) {
const str_bytes_ptr: [*]u8 = self.str_bytes orelse unreachable;
// must include refcount
const str_bytes: []u8 = (str_bytes_ptr - 8)[0 .. self.str_len + 8];
allocator.free(str_bytes);
}
}
// This takes ownership of the pointed-to bytes if they won't fit in a
// small string, and returns a (pointer, len) tuple which points to them.
pub fn withCapacity(length: usize) RocStr {
const roc_str_size = @sizeOf(RocStr);
if (length < roc_str_size) {
return RocStr.empty();
} else {
var new_bytes: []T = allocator.alloc(u8, length) catch unreachable;
var new_bytes_ptr: [*]u8 = @ptrCast([*]u8, &new_bytes);
return RocStr{
.str_bytes = new_bytes_ptr,
.str_len = length,
};
}
}
pub fn eq(self: RocStr, other: RocStr) bool {
const self_bytes_ptr: ?[*]const u8 = self.str_bytes;
const other_bytes_ptr: ?[*]const u8 = other.str_bytes;
// If they are byte-for-byte equal, they're definitely equal!
if (self_bytes_ptr == other_bytes_ptr and self.str_len == other.str_len) {
return true;
}
const self_len = self.len();
const other_len = other.len();
// If their lengths are different, they're definitely unequal.
if (self_len != other_len) {
return false;
}
const self_u8_ptr: [*]const u8 = @ptrCast([*]const u8, &self);
const other_u8_ptr: [*]const u8 = @ptrCast([*]const u8, &other);
const self_bytes: [*]const u8 = if (self.isSmallStr() or self.isEmpty()) self_u8_ptr else self_bytes_ptr orelse unreachable;
const other_bytes: [*]const u8 = if (other.isSmallStr() or other.isEmpty()) other_u8_ptr else other_bytes_ptr orelse unreachable;
var index: usize = 0;
// TODO rewrite this into a for loop
const length = self.len();
while (index < length) {
if (self_bytes[index] != other_bytes[index]) {
return false;
}
index = index + 1;
}
return true;
}
pub fn clone(allocator: *Allocator, comptime T: type, in_place: InPlace, str: RocStr) RocStr {
if (str.isSmallStr() or str.isEmpty()) {
// just return the bytes
return str;
} else {
var new_str = RocStr.initBig(allocator, T, in_place, str.str_len);
var old_bytes: [*]u8 = @ptrCast([*]u8, str.str_bytes);
var new_bytes: [*]u8 = @ptrCast([*]u8, new_str.str_bytes);
@memcpy(new_bytes, old_bytes, str.str_len);
return new_str;
}
}
pub fn isSmallStr(self: RocStr) bool {
return @bitCast(isize, self.str_len) < 0;
}
pub fn len(self: RocStr) usize {
const bytes: [*]const u8 = @ptrCast([*]const u8, &self);
const last_byte = bytes[@sizeOf(RocStr) - 1];
const small_len = @as(usize, last_byte ^ 0b1000_0000);
const big_len = self.str_len;
// Since this conditional would be prone to branch misprediction,
// make sure it will compile to a cmov.
return if (self.isSmallStr()) small_len else big_len;
}
pub fn isEmpty(self: RocStr) bool {
return self.len() == 0;
}
pub fn asSlice(self: RocStr) []u8 {
// Since this conditional would be prone to branch misprediction,
// make sure it will compile to a cmov.
return self.asU8ptr()[0..self.len()];
}
pub fn asU8ptr(self: RocStr) [*]u8 {
// Since this conditional would be prone to branch misprediction,
// make sure it will compile to a cmov.
return if (self.isSmallStr() or self.isEmpty()) (&@bitCast([16]u8, self)) else (@ptrCast([*]u8, self.str_bytes));
}
// Given a pointer to some bytes, write the first (len) bytes of this
// RocStr's contents into it.
//
// One use for this function is writing into an `alloca` for a C string that
// only needs to live long enough to be passed as an argument to
// a C function - like the file path argument to `fopen`.
pub fn memcpy(self: RocStr, dest: [*]u8, length: usize) void {
const src = self.asU8ptr();
@memcpy(dest, src, length);
}
test "RocStr.eq: equal" {
const str1_len = 3;
var str1: [str1_len]u8 = "abc".*;
const str1_ptr: [*]u8 = &str1;
var roc_str1 = RocStr.init(testing.allocator, str1_ptr, str1_len);
const str2_len = 3;
var str2: [str2_len]u8 = "abc".*;
const str2_ptr: [*]u8 = &str2;
var roc_str2 = RocStr.init(testing.allocator, str2_ptr, str2_len);
expect(roc_str1.eq(roc_str2));
roc_str1.deinit(testing.allocator);
roc_str2.deinit(testing.allocator);
}
test "RocStr.eq: not equal different length" {
const str1_len = 4;
var str1: [str1_len]u8 = "abcd".*;
const str1_ptr: [*]u8 = &str1;
var roc_str1 = RocStr.init(testing.allocator, str1_ptr, str1_len);
const str2_len = 3;
var str2: [str2_len]u8 = "abc".*;
const str2_ptr: [*]u8 = &str2;
var roc_str2 = RocStr.init(testing.allocator, str2_ptr, str2_len);
defer {
roc_str1.deinit(testing.allocator);
roc_str2.deinit(testing.allocator);
}
expect(!roc_str1.eq(roc_str2));
}
test "RocStr.eq: not equal same length" {
const str1_len = 3;
var str1: [str1_len]u8 = "acb".*;
const str1_ptr: [*]u8 = &str1;
var roc_str1 = RocStr.init(testing.allocator, str1_ptr, str1_len);
const str2_len = 3;
var str2: [str2_len]u8 = "abc".*;
const str2_ptr: [*]u8 = &str2;
var roc_str2 = RocStr.init(testing.allocator, str2_ptr, str2_len);
defer {
roc_str1.deinit(testing.allocator);
roc_str2.deinit(testing.allocator);
}
expect(!roc_str1.eq(roc_str2));
}
};
// Str.equal
pub fn strEqual(self: RocStr, other: RocStr) callconv(.C) bool {
return self.eq(other);
}
// Str.numberOfBytes
pub fn strNumberOfBytes(string: RocStr) callconv(.C) usize {
return string.len();
}
// Str.fromInt
// When we actually use this in Roc, libc will be linked so we have access to std.heap.c_allocator
pub fn strFromIntC(int: i64) callconv(.C) RocStr {
return strFromInt(std.heap.c_allocator, int);
}
fn strFromInt(allocator: *Allocator, int: i64) RocStr {
// prepare for having multiple integer types in the future
return @call(.{ .modifier = always_inline }, strFromIntHelp, .{ allocator, i64, int });
}
fn strFromIntHelp(allocator: *Allocator, comptime T: type, int: T) RocStr {
// determine maximum size for this T
comptime const size = comptime blk: {
// the string representation of the minimum i128 value uses at most 40 characters
var buf: [40]u8 = undefined;
var result = std.fmt.bufPrint(&buf, "{}", .{std.math.minInt(T)}) catch unreachable;
break :blk result.len;
};
var buf: [size]u8 = undefined;
const result = std.fmt.bufPrint(&buf, "{}", .{int}) catch unreachable;
return RocStr.init(allocator, &buf, result.len);
}
// Str.split
// When we actually use this in Roc, libc will be linked so we have access to std.heap.c_allocator
pub fn strSplitInPlaceC(array: [*]RocStr, string: RocStr, delimiter: RocStr) callconv(.C) void {
return @call(.{ .modifier = always_inline }, strSplitInPlace, .{ std.heap.c_allocator, array, string, delimiter });
}
fn strSplitInPlace(allocator: *Allocator, array: [*]RocStr, string: RocStr, delimiter: RocStr) void {
var ret_array_index: usize = 0;
var slice_start_index: usize = 0;
var str_index: usize = 0;
const str_bytes = string.asU8ptr();
const str_len = string.len();
const delimiter_bytes_ptrs = delimiter.asU8ptr();
const delimiter_len = delimiter.len();
if (str_len > delimiter_len) {
const end_index: usize = str_len - delimiter_len + 1;
while (str_index <= end_index) {
var delimiter_index: usize = 0;
var matches_delimiter = true;
while (delimiter_index < delimiter_len) {
var delimiterChar = delimiter_bytes_ptrs[delimiter_index];
var strChar = str_bytes[str_index + delimiter_index];
if (delimiterChar != strChar) {
matches_delimiter = false;
break;
}
delimiter_index += 1;
}
if (matches_delimiter) {
const segment_len: usize = str_index - slice_start_index;
array[ret_array_index] = RocStr.init(allocator, str_bytes + slice_start_index, segment_len);
slice_start_index = str_index + delimiter_len;
ret_array_index += 1;
str_index += delimiter_len;
} else {
str_index += 1;
}
}
}
array[ret_array_index] = RocStr.init(allocator, str_bytes + slice_start_index, str_len - slice_start_index);
}
test "strSplitInPlace: no delimiter" {
// Str.split "abc" "!" == [ "abc" ]
const str_arr = "abc";
const str = RocStr.init(testing.allocator, str_arr, str_arr.len);
const delimiter_arr = "!";
const delimiter = RocStr.init(testing.allocator, delimiter_arr, delimiter_arr.len);
var array: [1]RocStr = undefined;
const array_ptr: [*]RocStr = &array;
strSplitInPlace(testing.allocator, array_ptr, str, delimiter);
var expected = [1]RocStr{
str,
};
defer {
for (array) |roc_str| {
roc_str.deinit(testing.allocator);
}
for (expected) |roc_str| {
roc_str.deinit(testing.allocator);
}
str.deinit(testing.allocator);
delimiter.deinit(testing.allocator);
}
expectEqual(array.len, expected.len);
expect(array[0].eq(expected[0]));
}
test "strSplitInPlace: empty end" {
const str_arr = "1---- ---- ---- ---- ----2---- ---- ---- ---- ----";
const str = RocStr.init(testing.allocator, str_arr, str_arr.len);
const delimiter_arr = "---- ---- ---- ---- ----";
const delimiter = RocStr.init(testing.allocator, delimiter_arr, delimiter_arr.len);
const array_len: usize = 3;
var array: [array_len]RocStr = [_]RocStr{
undefined,
undefined,
undefined,
};
const array_ptr: [*]RocStr = &array;
strSplitInPlace(testing.allocator, array_ptr, str, delimiter);
const one = RocStr.init(testing.allocator, "1", 1);
const two = RocStr.init(testing.allocator, "2", 1);
var expected = [3]RocStr{
one, two, RocStr.empty(),
};
defer {
for (array) |rocStr| {
rocStr.deinit(testing.allocator);
}
for (expected) |rocStr| {
rocStr.deinit(testing.allocator);
}
str.deinit(testing.allocator);
delimiter.deinit(testing.allocator);
}
expectEqual(array.len, expected.len);
expect(array[0].eq(expected[0]));
expect(array[1].eq(expected[1]));
expect(array[2].eq(expected[2]));
}
test "strSplitInPlace: delimiter on sides" {
const str_arr = "tttghittt";
const str = RocStr.init(testing.allocator, str_arr, str_arr.len);
const delimiter_arr = "ttt";
const delimiter = RocStr.init(testing.allocator, delimiter_arr, delimiter_arr.len);
const array_len: usize = 3;
var array: [array_len]RocStr = [_]RocStr{
undefined,
undefined,
undefined,
};
const array_ptr: [*]RocStr = &array;
strSplitInPlace(testing.allocator, array_ptr, str, delimiter);
const ghi_arr = "ghi";
const ghi = RocStr.init(testing.allocator, ghi_arr, ghi_arr.len);
var expected = [3]RocStr{
RocStr.empty(), ghi, RocStr.empty(),
};
defer {
for (array) |rocStr| {
rocStr.deinit(testing.allocator);
}
for (expected) |rocStr| {
rocStr.deinit(testing.allocator);
}
str.deinit(testing.allocator);
delimiter.deinit(testing.allocator);
}
expectEqual(array.len, expected.len);
expect(array[0].eq(expected[0]));
expect(array[1].eq(expected[1]));
expect(array[2].eq(expected[2]));
}
test "strSplitInPlace: three pieces" {
// Str.split "a!b!c" "!" == [ "a", "b", "c" ]
const str_arr = "a!b!c";
const str = RocStr.init(testing.allocator, str_arr, str_arr.len);
const delimiter_arr = "!";
const delimiter = RocStr.init(testing.allocator, delimiter_arr, delimiter_arr.len);
const array_len: usize = 3;
var array: [array_len]RocStr = undefined;
const array_ptr: [*]RocStr = &array;
strSplitInPlace(testing.allocator, array_ptr, str, delimiter);
const a = RocStr.init(testing.allocator, "a", 1);
const b = RocStr.init(testing.allocator, "b", 1);
const c = RocStr.init(testing.allocator, "c", 1);
var expected_array = [array_len]RocStr{
a, b, c,
};
defer {
for (array) |roc_str| {
roc_str.deinit(testing.allocator);
}
for (expected_array) |roc_str| {
roc_str.deinit(testing.allocator);
}
str.deinit(testing.allocator);
delimiter.deinit(testing.allocator);
}
expectEqual(expected_array.len, array.len);
expect(array[0].eq(expected_array[0]));
expect(array[1].eq(expected_array[1]));
expect(array[2].eq(expected_array[2]));
}
// This is used for `Str.split : Str, Str -> Array Str
// It is used to count how many segments the input `_str`
// needs to be broken into, so that we can allocate a array
// of that size. It always returns at least 1.
pub fn countSegments(string: RocStr, delimiter: RocStr) callconv(.C) usize {
const str_bytes = string.asU8ptr();
const str_len = string.len();
const delimiter_bytes_ptrs = delimiter.asU8ptr();
const delimiter_len = delimiter.len();
var count: usize = 1;
if (str_len > delimiter_len) {
var str_index: usize = 0;
const end_cond: usize = str_len - delimiter_len + 1;
while (str_index < end_cond) {
var delimiter_index: usize = 0;
var matches_delimiter = true;
while (delimiter_index < delimiter_len) {
const delimiterChar = delimiter_bytes_ptrs[delimiter_index];
const strChar = str_bytes[str_index + delimiter_index];
if (delimiterChar != strChar) {
matches_delimiter = false;
break;
}
delimiter_index += 1;
}
if (matches_delimiter) {
count += 1;
}
str_index += 1;
}
}
return count;
}
test "countSegments: long delimiter" {
// Str.split "str" "delimiter" == [ "str" ]
// 1 segment
const str_arr = "str";
const str = RocStr.init(testing.allocator, str_arr, str_arr.len);
const delimiter_arr = "delimiter";
const delimiter = RocStr.init(testing.allocator, delimiter_arr, delimiter_arr.len);
defer {
str.deinit(testing.allocator);
delimiter.deinit(testing.allocator);
}
const segments_count = countSegments(str, delimiter);
expectEqual(segments_count, 1);
}
test "countSegments: delimiter at start" {
// Str.split "hello there" "hello" == [ "", " there" ]
// 2 segments
const str_arr = "hello there";
const str = RocStr.init(testing.allocator, str_arr, str_arr.len);
const delimiter_arr = "hello";
const delimiter = RocStr.init(testing.allocator, delimiter_arr, delimiter_arr.len);
defer {
str.deinit(testing.allocator);
delimiter.deinit(testing.allocator);
}
const segments_count = countSegments(str, delimiter);
expectEqual(segments_count, 2);
}
test "countSegments: delimiter interspered" {
// Str.split "a!b!c" "!" == [ "a", "b", "c" ]
// 3 segments
const str_arr = "a!b!c";
const str = RocStr.init(testing.allocator, str_arr, str_arr.len);
const delimiter_arr = "!";
const delimiter = RocStr.init(testing.allocator, delimiter_arr, delimiter_arr.len);
defer {
str.deinit(testing.allocator);
delimiter.deinit(testing.allocator);
}
const segments_count = countSegments(str, delimiter);
expectEqual(segments_count, 3);
}
fn rocStrFromLiteral(bytes_arr: *const []u8) RocStr {}
// Str.startsWith
pub fn startsWith(string: RocStr, prefix: RocStr) callconv(.C) bool {
const bytes_len = string.len();
const bytes_ptr = string.asU8ptr();
const prefix_len = prefix.len();
const prefix_ptr = prefix.asU8ptr();
if (prefix_len > bytes_len) {
return false;
}
// we won't exceed bytes_len due to the previous check
var i: usize = 0;
while (i < prefix_len) {
if (bytes_ptr[i] != prefix_ptr[i]) {
return false;
}
i += 1;
}
return true;
}
test "startsWith: foo starts with fo" {
const foo = RocStr.init(testing.allocator, "foo", 3);
const fo = RocStr.init(testing.allocator, "fo", 2);
expect(startsWith(foo, fo));
}
test "startsWith: 123456789123456789 starts with 123456789123456789" {
const str = RocStr.init(testing.allocator, "123456789123456789", 18);
defer str.deinit(testing.allocator);
expect(startsWith(str, str));
}
test "startsWith: 12345678912345678910 starts with 123456789123456789" {
const str = RocStr.init(testing.allocator, "12345678912345678910", 20);
defer str.deinit(testing.allocator);
const prefix = RocStr.init(testing.allocator, "123456789123456789", 18);
defer prefix.deinit(testing.allocator);
expect(startsWith(str, prefix));
}
// Str.endsWith
pub fn endsWith(string: RocStr, suffix: RocStr) callconv(.C) bool {
const bytes_len = string.len();
const bytes_ptr = string.asU8ptr();
const suffix_len = suffix.len();
const suffix_ptr = suffix.asU8ptr();
if (suffix_len > bytes_len) {
return false;
}
const offset: usize = bytes_len - suffix_len;
var i: usize = 0;
while (i < suffix_len) {
if (bytes_ptr[i + offset] != suffix_ptr[i]) {
return false;
}
i += 1;
}
return true;
}
test "endsWith: foo ends with oo" {
const foo = RocStr.init(testing.allocator, "foo", 3);
const oo = RocStr.init(testing.allocator, "oo", 2);
defer foo.deinit(testing.allocator);
defer oo.deinit(testing.allocator);
expect(endsWith(foo, oo));
}
test "endsWith: 123456789123456789 ends with 123456789123456789" {
const str = RocStr.init(testing.allocator, "123456789123456789", 18);
defer str.deinit(testing.allocator);
expect(endsWith(str, str));
}
test "endsWith: 12345678912345678910 ends with 345678912345678910" {
const str = RocStr.init(testing.allocator, "12345678912345678910", 20);
const suffix = RocStr.init(testing.allocator, "345678912345678910", 18);
defer str.deinit(testing.allocator);
defer suffix.deinit(testing.allocator);
expect(endsWith(str, suffix));
}
test "endsWith: hello world ends with world" {
const str = RocStr.init(testing.allocator, "hello world", 11);
const suffix = RocStr.init(testing.allocator, "world", 5);
defer str.deinit(testing.allocator);
defer suffix.deinit(testing.allocator);
expect(endsWith(str, suffix));
}
// Str.concat
// When we actually use this in Roc, libc will be linked so we have access to std.heap.c_allocator
pub fn strConcatC(ptr_size: u32, result_in_place: InPlace, arg1: RocStr, arg2: RocStr) callconv(.C) RocStr {
return @call(.{ .modifier = always_inline }, strConcat, .{ std.heap.c_allocator, ptr_size, result_in_place, arg1, arg2 });
}
fn strConcat(allocator: *Allocator, ptr_size: u32, result_in_place: InPlace, arg1: RocStr, arg2: RocStr) RocStr {
return switch (ptr_size) {
4 => strConcatHelp(allocator, i32, result_in_place, arg1, arg2),
8 => strConcatHelp(allocator, i64, result_in_place, arg1, arg2),
else => unreachable,
};
}
fn strConcatHelp(allocator: *Allocator, comptime T: type, result_in_place: InPlace, arg1: RocStr, arg2: RocStr) RocStr {
if (arg1.isEmpty()) {
return RocStr.clone(allocator, T, result_in_place, arg2);
} else if (arg2.isEmpty()) {
return RocStr.clone(allocator, T, result_in_place, arg1);
} else {
const combined_length = arg1.len() + arg2.len();
const small_str_bytes = 2 * @sizeOf(T);
const result_is_big = combined_length >= small_str_bytes;
if (result_is_big) {
var result = RocStr.initBig(allocator, T, result_in_place, combined_length);
{
const old_bytes = arg1.asU8ptr();
const new_bytes: [*]u8 = @ptrCast([*]u8, result.str_bytes);
@memcpy(new_bytes, old_bytes, arg1.len());
}
{
const old_bytes = arg2.asU8ptr();
const new_bytes = @ptrCast([*]u8, result.str_bytes) + arg1.len();
@memcpy(new_bytes, old_bytes, arg2.len());
}
return result;
} else {
var result = [16]u8{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
// if the result is small, then for sure arg1 and arg2 are also small
{
var old_bytes: [*]u8 = @ptrCast([*]u8, &@bitCast([16]u8, arg1));
var new_bytes: [*]u8 = @ptrCast([*]u8, &result);
@memcpy(new_bytes, old_bytes, arg1.len());
}
{
var old_bytes: [*]u8 = @ptrCast([*]u8, &@bitCast([16]u8, arg2));
var new_bytes = @ptrCast([*]u8, &result) + arg1.len();
@memcpy(new_bytes, old_bytes, arg2.len());
}
const mask: u8 = 0b1000_0000;
const final_byte = @truncate(u8, combined_length) | mask;
result[small_str_bytes - 1] = final_byte;
return @bitCast(RocStr, result);
}
return result;
}
}
test "RocStr.concat: small concat small" {
const str1_len = 3;
var str1: [str1_len]u8 = "foo".*;
const str1_ptr: [*]u8 = &str1;
var roc_str1 = RocStr.init(testing.allocator, str1_ptr, str1_len);
const str2_len = 3;
var str2: [str2_len]u8 = "abc".*;
const str2_ptr: [*]u8 = &str2;
var roc_str2 = RocStr.init(testing.allocator, str2_ptr, str2_len);
const str3_len = 6;
var str3: [str3_len]u8 = "fooabc".*;
const str3_ptr: [*]u8 = &str3;
var roc_str3 = RocStr.init(testing.allocator, str3_ptr, str3_len);
defer {
roc_str1.deinit(testing.allocator);
roc_str2.deinit(testing.allocator);
roc_str3.deinit(testing.allocator);
}
const result = strConcat(testing.allocator, 8, InPlace.Clone, roc_str1, roc_str2);
defer result.deinit(testing.allocator);
expect(roc_str3.eq(result));
}

View file

@ -61,15 +61,29 @@ pub fn run_roc(args: &[&str]) -> Out {
} }
#[allow(dead_code)] #[allow(dead_code)]
pub fn run_cmd(cmd_name: &str, args: &[&str]) -> Out { pub fn run_cmd(cmd_name: &str, stdin_str: &str, args: &[&str]) -> Out {
let mut cmd = Command::new(cmd_name); let mut cmd = Command::new(cmd_name);
for arg in args { for arg in args {
cmd.arg(arg); cmd.arg(arg);
} }
let output = cmd let mut child = cmd
.output() .stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap_or_else(|_| panic!("failed to execute cmd `{}` in CLI test", cmd_name));
{
let stdin = child.stdin.as_mut().expect("Failed to open stdin");
stdin
.write_all(stdin_str.as_bytes())
.expect("Failed to write to stdin");
}
let output = child
.wait_with_output()
.unwrap_or_else(|_| panic!("failed to execute cmd `{}` in CLI test", cmd_name)); .unwrap_or_else(|_| panic!("failed to execute cmd `{}` in CLI test", cmd_name));
Out { Out {
@ -80,7 +94,7 @@ pub fn run_cmd(cmd_name: &str, args: &[&str]) -> Out {
} }
#[allow(dead_code)] #[allow(dead_code)]
pub fn run_with_valgrind(args: &[&str]) -> (Out, String) { pub fn run_with_valgrind(stdin_str: &str, args: &[&str]) -> (Out, String) {
//TODO: figure out if there is a better way to get the valgrind executable. //TODO: figure out if there is a better way to get the valgrind executable.
let mut cmd = Command::new("valgrind"); let mut cmd = Command::new("valgrind");
let named_tempfile = let named_tempfile =
@ -114,8 +128,23 @@ pub fn run_with_valgrind(args: &[&str]) -> (Out, String) {
cmd.arg(arg); cmd.arg(arg);
} }
let output = cmd cmd.stdin(Stdio::piped());
.output() cmd.stdout(Stdio::piped());
cmd.stderr(Stdio::piped());
let mut child = cmd
.spawn()
.expect("failed to execute compiled `valgrind` binary in CLI test");
{
let stdin = child.stdin.as_mut().expect("Failed to open stdin");
stdin
.write_all(stdin_str.as_bytes())
.expect("Failed to write to stdin");
}
let output = child
.wait_with_output()
.expect("failed to execute compiled `valgrind` binary in CLI test"); .expect("failed to execute compiled `valgrind` binary in CLI test");
let mut file = named_tempfile.into_file(); let mut file = named_tempfile.into_file();

View file

@ -43,7 +43,7 @@ use roc_collections::all::{ImMap, MutSet};
use roc_module::ident::TagName; use roc_module::ident::TagName;
use roc_module::low_level::LowLevel; use roc_module::low_level::LowLevel;
use roc_module::symbol::{Interns, ModuleId, Symbol}; use roc_module::symbol::{Interns, ModuleId, Symbol};
use roc_mono::ir::{CallType, JoinPointId, Wrapped}; use roc_mono::ir::{BranchInfo, CallType, JoinPointId, ModifyRc, Wrapped};
use roc_mono::layout::{Builtin, ClosureLayout, Layout, LayoutIds, MemoryMode, UnionLayout}; use roc_mono::layout::{Builtin, ClosureLayout, Layout, LayoutIds, MemoryMode, UnionLayout};
use target_lexicon::CallingConvention; use target_lexicon::CallingConvention;
@ -2056,7 +2056,7 @@ pub fn build_exp_stmt<'a, 'ctx, 'env>(
cond_layout: cond_layout.clone(), cond_layout: cond_layout.clone(),
cond_symbol: *cond_symbol, cond_symbol: *cond_symbol,
branches, branches,
default_branch, default_branch: default_branch.1,
ret_type, ret_type,
}; };
@ -2129,24 +2129,65 @@ pub fn build_exp_stmt<'a, 'ctx, 'env>(
// This doesn't currently do anything // This doesn't currently do anything
context.i64_type().const_zero().into() context.i64_type().const_zero().into()
} }
Inc(symbol, inc_amount, cont) => {
let (value, layout) = load_symbol_and_layout(scope, symbol);
let layout = layout.clone();
if layout.contains_refcounted() {
increment_refcount_layout(env, parent, layout_ids, *inc_amount, value, &layout);
}
/*
Inc(symbol1, 1, Dec(symbol2, cont)) if symbol1 == symbol2 => {
dbg!(symbol1);
build_exp_stmt(env, layout_ids, scope, parent, cont) build_exp_stmt(env, layout_ids, scope, parent, cont)
} }
Dec(symbol, cont) => { */
let (value, layout) = load_symbol_and_layout(scope, symbol); Refcounting(modify, cont) => {
use ModifyRc::*;
if layout.contains_refcounted() { match modify {
decrement_refcount_layout(env, parent, layout_ids, value, layout); Inc(symbol, inc_amount) => {
match cont {
Refcounting(ModifyRc::Dec(symbol1), contcont)
if *inc_amount == 1 && symbol == symbol1 =>
{
// the inc and dec cancel out
build_exp_stmt(env, layout_ids, scope, parent, contcont)
}
_ => {
let (value, layout) = load_symbol_and_layout(scope, symbol);
let layout = layout.clone();
if layout.contains_refcounted() {
increment_refcount_layout(
env,
parent,
layout_ids,
*inc_amount,
value,
&layout,
);
}
build_exp_stmt(env, layout_ids, scope, parent, cont)
}
}
}
Dec(symbol) => {
let (value, layout) = load_symbol_and_layout(scope, symbol);
if layout.contains_refcounted() {
decrement_refcount_layout(env, parent, layout_ids, value, layout);
}
build_exp_stmt(env, layout_ids, scope, parent, cont)
}
DecRef(symbol) => {
let (value, layout) = load_symbol_and_layout(scope, symbol);
if layout.is_refcounted() {
let value_ptr = value.into_pointer_value();
let refcount_ptr = PointerToRefcount::from_ptr_to_data(env, value_ptr);
refcount_ptr.decrement(env, layout);
}
build_exp_stmt(env, layout_ids, scope, parent, cont)
}
} }
build_exp_stmt(env, layout_ids, scope, parent, cont)
} }
RuntimeError(error_msg) => { RuntimeError(error_msg) => {
@ -2315,7 +2356,7 @@ fn extract_tag_discriminant_ptr<'a, 'ctx, 'env>(
struct SwitchArgsIr<'a, 'ctx> { struct SwitchArgsIr<'a, 'ctx> {
pub cond_symbol: Symbol, pub cond_symbol: Symbol,
pub cond_layout: Layout<'a>, pub cond_layout: Layout<'a>,
pub branches: &'a [(u64, roc_mono::ir::Stmt<'a>)], pub branches: &'a [(u64, BranchInfo<'a>, roc_mono::ir::Stmt<'a>)],
pub default_branch: &'a roc_mono::ir::Stmt<'a>, pub default_branch: &'a roc_mono::ir::Stmt<'a>,
pub ret_type: BasicTypeEnum<'ctx>, pub ret_type: BasicTypeEnum<'ctx>,
} }
@ -2444,7 +2485,7 @@ fn build_switch_ir<'a, 'ctx, 'env>(
if let Layout::Builtin(Builtin::Int1) = cond_layout { if let Layout::Builtin(Builtin::Int1) = cond_layout {
match (branches, default_branch) { match (branches, default_branch) {
([(0, false_branch)], true_branch) | ([(1, true_branch)], false_branch) => { ([(0, _, false_branch)], true_branch) | ([(1, _, true_branch)], false_branch) => {
let then_block = context.append_basic_block(parent, "then_block"); let then_block = context.append_basic_block(parent, "then_block");
let else_block = context.append_basic_block(parent, "else_block"); let else_block = context.append_basic_block(parent, "else_block");
@ -2482,7 +2523,7 @@ fn build_switch_ir<'a, 'ctx, 'env>(
let default_block = context.append_basic_block(parent, "default"); let default_block = context.append_basic_block(parent, "default");
let mut cases = Vec::with_capacity_in(branches.len(), arena); let mut cases = Vec::with_capacity_in(branches.len(), arena);
for (int, _) in branches.iter() { for (int, _, _) in branches.iter() {
// Switch constants must all be same type as switch value! // Switch constants must all be same type as switch value!
// e.g. this is incorrect, and will trigger a LLVM warning: // e.g. this is incorrect, and will trigger a LLVM warning:
// //
@ -2512,7 +2553,7 @@ fn build_switch_ir<'a, 'ctx, 'env>(
builder.build_switch(cond, default_block, &cases); builder.build_switch(cond, default_block, &cases);
for ((_, branch_expr), (_, block)) in branches.iter().zip(cases) { for ((_, _, branch_expr), (_, block)) in branches.iter().zip(cases) {
builder.position_at_end(block); builder.position_at_end(block);
let branch_val = build_exp_stmt(env, layout_ids, scope, parent, branch_expr); let branch_val = build_exp_stmt(env, layout_ids, scope, parent, branch_expr);

View file

@ -133,7 +133,7 @@ impl<'ctx> PointerToRefcount<'ctx> {
self.set_refcount(env, new_refcount); self.set_refcount(env, new_refcount);
} }
fn decrement<'a, 'env>(&self, env: &Env<'a, 'ctx, 'env>, layout: &Layout<'a>) { pub fn decrement<'a, 'env>(&self, env: &Env<'a, 'ctx, 'env>, layout: &Layout<'a>) {
let context = env.context; let context = env.context;
let block = env.builder.get_insert_block().expect("to be in a function"); let block = env.builder.get_insert_block().expect("to be in a function");
let di_location = env.builder.get_current_debug_location().unwrap(); let di_location = env.builder.get_current_debug_location().unwrap();

View file

@ -1936,6 +1936,7 @@ mod gen_primitives {
} }
#[test] #[test]
#[ignore]
fn rosetree_basic() { fn rosetree_basic() {
assert_non_opt_evals_to!( assert_non_opt_evals_to!(
indoc!( indoc!(

View file

@ -395,21 +395,19 @@ where
.. ..
} => { } => {
self.set_last_seen(*cond_symbol, stmt); self.set_last_seen(*cond_symbol, stmt);
for (_, branch) in *branches { for (_, _, branch) in *branches {
self.scan_ast(branch); self.scan_ast(branch);
} }
self.scan_ast(default_branch); self.scan_ast(default_branch.1);
} }
Stmt::Ret(sym) => { Stmt::Ret(sym) => {
self.set_last_seen(*sym, stmt); self.set_last_seen(*sym, stmt);
} }
Stmt::Rethrow => {} Stmt::Rethrow => {}
Stmt::Inc(sym, _inc, following) => { Stmt::Refcounting(modify, following) => {
self.set_last_seen(*sym, stmt); let sym = modify.get_symbol();
self.scan_ast(following);
} self.set_last_seen(sym, stmt);
Stmt::Dec(sym, following) => {
self.set_last_seen(*sym, stmt);
self.scan_ast(following); self.scan_ast(following);
} }
Stmt::Join { Stmt::Join {

View file

@ -16,7 +16,8 @@ use roc_constrain::module::{
use roc_constrain::module::{constrain_module, ExposedModuleTypes, SubsByModule}; use roc_constrain::module::{constrain_module, ExposedModuleTypes, SubsByModule};
use roc_module::ident::{Ident, Lowercase, ModuleName, QualifiedModuleName, TagName}; use roc_module::ident::{Ident, Lowercase, ModuleName, QualifiedModuleName, TagName};
use roc_module::symbol::{ use roc_module::symbol::{
IdentIds, Interns, ModuleId, ModuleIds, PQModuleName, PackageModuleIds, Symbol, IdentIds, Interns, ModuleId, ModuleIds, PQModuleName, PackageModuleIds, PackageQualified,
Symbol,
}; };
use roc_mono::ir::{ use roc_mono::ir::{
CapturedSymbols, ExternalSpecializations, PartialProc, PendingSpecialization, Proc, Procs, CapturedSymbols, ExternalSpecializations, PartialProc, PendingSpecialization, Proc, Procs,
@ -94,25 +95,41 @@ enum Status {
Done, Done,
} }
#[derive(Default, Debug)] #[derive(Clone, Debug, PartialEq, Eq, Hash)]
struct Dependencies { enum Job<'a> {
waiting_for: MutMap<(ModuleId, Phase), MutSet<(ModuleId, Phase)>>, Step(ModuleId, Phase),
notifies: MutMap<(ModuleId, Phase), MutSet<(ModuleId, Phase)>>, ResolveShorthand(&'a str),
status: MutMap<(ModuleId, Phase), Status>,
} }
impl Dependencies { #[derive(Default, Debug)]
struct Dependencies<'a> {
waiting_for: MutMap<Job<'a>, MutSet<Job<'a>>>,
notifies: MutMap<Job<'a>, MutSet<Job<'a>>>,
status: MutMap<Job<'a>, Status>,
}
impl<'a> Dependencies<'a> {
/// Add all the dependencies for a module, return (module, phase) pairs that can make progress /// Add all the dependencies for a module, return (module, phase) pairs that can make progress
pub fn add_module( pub fn add_module(
&mut self, &mut self,
module_id: ModuleId, module_id: ModuleId,
opt_effect_module: Option<ModuleId>, dependencies: &MutSet<PackageQualified<'a, ModuleId>>,
dependencies: &MutMap<ModuleId, Region>,
goal_phase: Phase, goal_phase: Phase,
) -> MutSet<(ModuleId, Phase)> { ) -> MutSet<(ModuleId, Phase)> {
use Phase::*; use Phase::*;
for dep in dependencies.keys().copied() { let mut output = MutSet::default();
for dep in dependencies.iter() {
let has_package_dependency = self.add_package_dependency(dep, Phase::LoadHeader);
let dep = *dep.as_inner();
if !has_package_dependency {
// loading can start immediately on this dependency
output.insert((dep, Phase::LoadHeader));
}
// to parse and generate constraints, the headers of all dependencies must be loaded! // to parse and generate constraints, the headers of all dependencies must be loaded!
// otherwise, we don't know whether an imported symbol is actually exposed // otherwise, we don't know whether an imported symbol is actually exposed
self.add_dependency_help(module_id, dep, Phase::Parse, Phase::LoadHeader); self.add_dependency_help(module_id, dep, Phase::Parse, Phase::LoadHeader);
@ -144,17 +161,6 @@ impl Dependencies {
self.add_to_status(module_id, goal_phase); self.add_to_status(module_id, goal_phase);
let mut output = MutSet::default();
// all the dependencies can be loaded
for dep in dependencies.keys() {
// TODO figure out how to "load" (because it doesn't exist on the file system) the Effect module
if Some(*dep) != opt_effect_module {
output.insert((*dep, LoadHeader));
}
}
output output
} }
@ -195,7 +201,7 @@ impl Dependencies {
break; break;
} }
if let Vacant(entry) = self.status.entry((module_id, *phase)) { if let Vacant(entry) = self.status.entry(Job::Step(module_id, *phase)) {
entry.insert(Status::NotStarted); entry.insert(Status::NotStarted);
} }
} }
@ -203,11 +209,19 @@ impl Dependencies {
/// Propagate a notification, return (module, phase) pairs that can make progress /// Propagate a notification, return (module, phase) pairs that can make progress
pub fn notify(&mut self, module_id: ModuleId, phase: Phase) -> MutSet<(ModuleId, Phase)> { pub fn notify(&mut self, module_id: ModuleId, phase: Phase) -> MutSet<(ModuleId, Phase)> {
self.status.insert((module_id, phase), Status::Done); self.notify_help(Job::Step(module_id, phase))
}
/// Propagate a notification, return (module, phase) pairs that can make progress
pub fn notify_package(&mut self, shorthand: &'a str) -> MutSet<(ModuleId, Phase)> {
self.notify_help(Job::ResolveShorthand(shorthand))
}
fn notify_help(&mut self, key: Job<'a>) -> MutSet<(ModuleId, Phase)> {
self.status.insert(key.clone(), Status::Done);
let mut output = MutSet::default(); let mut output = MutSet::default();
let key = (module_id, phase);
if let Some(to_notify) = self.notifies.get(&key) { if let Some(to_notify) = self.notifies.get(&key) {
for notify_key in to_notify { for notify_key in to_notify {
let mut is_empty = false; let mut is_empty = false;
@ -218,7 +232,10 @@ impl Dependencies {
if is_empty { if is_empty {
self.waiting_for.remove(notify_key); self.waiting_for.remove(notify_key);
output.insert(*notify_key);
if let Job::Step(module, phase) = *notify_key {
output.insert((module, phase));
}
} }
} }
} }
@ -228,6 +245,48 @@ impl Dependencies {
output output
} }
fn add_package_dependency(
&mut self,
module: &PackageQualified<'a, ModuleId>,
next_phase: Phase,
) -> bool {
match module {
PackageQualified::Unqualified(_) => {
// no dependency, we can just start loading the file
false
}
PackageQualified::Qualified(shorthand, module_id) => {
let job = Job::ResolveShorthand(shorthand);
let next_step = Job::Step(*module_id, next_phase);
match self.status.get(&job) {
None | Some(Status::NotStarted) | Some(Status::Pending) => {
// this shorthand is not resolved, add a dependency
{
let entry = self
.waiting_for
.entry(next_step.clone())
.or_insert_with(Default::default);
entry.insert(job.clone());
}
{
let entry = self.notifies.entry(job).or_insert_with(Default::default);
entry.insert(next_step);
}
true
}
Some(Status::Done) => {
// shorthand is resolved; no dependency
false
}
}
}
}
}
/// A waits for B, and B will notify A when it completes the phase /// A waits for B, and B will notify A when it completes the phase
fn add_dependency(&mut self, a: ModuleId, b: ModuleId, phase: Phase) { fn add_dependency(&mut self, a: ModuleId, b: ModuleId, phase: Phase) {
self.add_dependency_help(a, b, phase, phase); self.add_dependency_help(a, b, phase, phase);
@ -236,12 +295,12 @@ impl Dependencies {
/// phase_a of module a is waiting for phase_b of module_b /// phase_a of module a is waiting for phase_b of module_b
fn add_dependency_help(&mut self, a: ModuleId, b: ModuleId, phase_a: Phase, phase_b: Phase) { fn add_dependency_help(&mut self, a: ModuleId, b: ModuleId, phase_a: Phase, phase_b: Phase) {
// no need to wait if the dependency is already done! // no need to wait if the dependency is already done!
if let Some(Status::Done) = self.status.get(&(b, phase_b)) { if let Some(Status::Done) = self.status.get(&Job::Step(b, phase_b)) {
return; return;
} }
let key = (a, phase_a); let key = Job::Step(a, phase_a);
let value = (b, phase_b); let value = Job::Step(b, phase_b);
match self.waiting_for.get_mut(&key) { match self.waiting_for.get_mut(&key) {
Some(existing) => { Some(existing) => {
existing.insert(value); existing.insert(value);
@ -253,8 +312,8 @@ impl Dependencies {
} }
} }
let key = (b, phase_b); let key = Job::Step(b, phase_b);
let value = (a, phase_a); let value = Job::Step(a, phase_a);
match self.notifies.get_mut(&key) { match self.notifies.get_mut(&key) {
Some(existing) => { Some(existing) => {
existing.insert(value); existing.insert(value);
@ -311,7 +370,11 @@ struct ModuleCache<'a> {
fn start_phase<'a>(module_id: ModuleId, phase: Phase, state: &mut State<'a>) -> Vec<BuildTask<'a>> { fn start_phase<'a>(module_id: ModuleId, phase: Phase, state: &mut State<'a>) -> Vec<BuildTask<'a>> {
// we blindly assume all dependencies are met // we blindly assume all dependencies are met
match state.dependencies.status.get_mut(&(module_id, phase)) { match state
.dependencies
.status
.get_mut(&Job::Step(module_id, phase))
{
Some(current @ Status::NotStarted) => { Some(current @ Status::NotStarted) => {
// start this phase! // start this phase!
*current = Status::Pending; *current = Status::Pending;
@ -336,7 +399,7 @@ fn start_phase<'a>(module_id: ModuleId, phase: Phase, state: &mut State<'a>) ->
state state
.dependencies .dependencies
.status .status
.insert((module_id, Phase::LoadHeader), Status::Pending); .insert(Job::Step(module_id, Phase::LoadHeader), Status::Pending);
} }
_ => unreachable!( _ => unreachable!(
"Pair {:?} is not in dependencies.status, that should never happen!", "Pair {:?} is not in dependencies.status, that should never happen!",
@ -558,6 +621,7 @@ struct ModuleHeader<'a> {
deps_by_name: MutMap<PQModuleName<'a>, ModuleId>, deps_by_name: MutMap<PQModuleName<'a>, ModuleId>,
packages: MutMap<&'a str, PackageOrPath<'a>>, packages: MutMap<&'a str, PackageOrPath<'a>>,
imported_modules: MutMap<ModuleId, Region>, imported_modules: MutMap<ModuleId, Region>,
package_qualified_imported_modules: MutSet<PackageQualified<'a, ModuleId>>,
exposes: Vec<Symbol>, exposes: Vec<Symbol>,
exposed_imports: MutMap<Ident, (Symbol, Region)>, exposed_imports: MutMap<Ident, (Symbol, Region)>,
src: &'a [u8], src: &'a [u8],
@ -566,8 +630,13 @@ struct ModuleHeader<'a> {
#[derive(Debug)] #[derive(Debug)]
enum HeaderFor<'a> { enum HeaderFor<'a> {
App { to_platform: To<'a> }, App {
PkgConfig, to_platform: To<'a>,
},
PkgConfig {
/// usually `base`
config_shorthand: &'a str,
},
Interface, Interface,
} }
@ -649,6 +718,7 @@ enum Msg<'a> {
module_docs: Option<ModuleDocumentation>, module_docs: Option<ModuleDocumentation>,
}, },
MadeEffectModule { MadeEffectModule {
type_shortname: &'a str,
constrained_module: ConstrainedModule, constrained_module: ConstrainedModule,
canonicalization_problems: Vec<roc_problem::can::Problem>, canonicalization_problems: Vec<roc_problem::can::Problem>,
module_docs: ModuleDocumentation, module_docs: ModuleDocumentation,
@ -704,12 +774,11 @@ struct State<'a> {
pub exposed_types: SubsByModule, pub exposed_types: SubsByModule,
pub output_path: Option<&'a str>, pub output_path: Option<&'a str>,
pub platform_path: Option<To<'a>>, pub platform_path: Option<To<'a>>,
pub opt_effect_module: Option<ModuleId>,
pub headers_parsed: MutSet<ModuleId>, pub headers_parsed: MutSet<ModuleId>,
pub module_cache: ModuleCache<'a>, pub module_cache: ModuleCache<'a>,
pub dependencies: Dependencies, pub dependencies: Dependencies<'a>,
pub procedures: MutMap<(Symbol, Layout<'a>), Proc<'a>>, pub procedures: MutMap<(Symbol, Layout<'a>), Proc<'a>>,
pub exposed_to_host: MutMap<Symbol, Variable>, pub exposed_to_host: MutMap<Symbol, Variable>,
@ -1313,7 +1382,6 @@ where
stdlib, stdlib,
output_path: None, output_path: None,
platform_path: None, platform_path: None,
opt_effect_module: None,
module_cache: ModuleCache::default(), module_cache: ModuleCache::default(),
dependencies: Dependencies::default(), dependencies: Dependencies::default(),
procedures: MutMap::default(), procedures: MutMap::default(),
@ -1452,25 +1520,35 @@ fn update<'a>(
Ok(state) Ok(state)
} }
Header(header, header_extra) => { Header(header, header_extra) => {
use HeaderFor::*;
log!("loaded header for {:?}", header.module_id); log!("loaded header for {:?}", header.module_id);
let home = header.module_id; let home = header.module_id;
let mut work = MutSet::default();
{ {
let mut shorthands = (*state.arc_shorthands).lock(); let mut shorthands = (*state.arc_shorthands).lock();
for (shorthand, package_or_path) in header.packages.iter() { for (shorthand, package_or_path) in header.packages.iter() {
shorthands.insert(shorthand, package_or_path.clone()); shorthands.insert(shorthand, package_or_path.clone());
} }
if let PkgConfig {
config_shorthand, ..
} = header_extra
{
work.extend(state.dependencies.notify_package(config_shorthand));
}
} }
use HeaderFor::*;
match header_extra { match header_extra {
App { to_platform } => { App { to_platform } => {
debug_assert_eq!(state.platform_path, None); debug_assert_eq!(state.platform_path, None);
state.platform_path = Some(to_platform.clone()); state.platform_path = Some(to_platform.clone());
} }
PkgConfig => { PkgConfig { .. } => {
debug_assert_eq!(state.platform_id, None); debug_assert_eq!(state.platform_id, None);
state.platform_id = Some(header.module_id); state.platform_id = Some(header.module_id);
@ -1500,12 +1578,11 @@ fn update<'a>(
.exposed_symbols_by_module .exposed_symbols_by_module
.insert(home, exposed_symbols); .insert(home, exposed_symbols);
let work = state.dependencies.add_module( work.extend(state.dependencies.add_module(
header.module_id, header.module_id,
state.opt_effect_module, &header.package_qualified_imported_modules,
&header.imported_modules,
state.goal_phase, state.goal_phase,
); ));
state.module_cache.headers.insert(header.module_id, header); state.module_cache.headers.insert(header.module_id, header);
@ -1528,7 +1605,7 @@ fn update<'a>(
// //
// e.g. for `app "blah"` we should generate an output file named "blah" // e.g. for `app "blah"` we should generate an output file named "blah"
match &parsed.module_name { match &parsed.module_name {
ModuleNameEnum::PkgConfig(_) => {} ModuleNameEnum::PkgConfig => {}
ModuleNameEnum::App(output_str) => match output_str { ModuleNameEnum::App(output_str) => match output_str {
StrLiteral::PlainLine(path) => { StrLiteral::PlainLine(path) => {
state.output_path = Some(path); state.output_path = Some(path);
@ -1589,11 +1666,10 @@ fn update<'a>(
constrained_module, constrained_module,
canonicalization_problems, canonicalization_problems,
module_docs, module_docs,
type_shortname,
} => { } => {
let module_id = constrained_module.module.module_id; let module_id = constrained_module.module.module_id;
state.opt_effect_module = Some(module_id);
log!("made effect module for {:?}", module_id); log!("made effect module for {:?}", module_id);
state state
.module_cache .module_cache
@ -1621,6 +1697,8 @@ fn update<'a>(
state.goal_phase, state.goal_phase,
); );
work.extend(state.dependencies.notify_package(type_shortname));
work.extend(state.dependencies.notify(module_id, Phase::LoadHeader)); work.extend(state.dependencies.notify(module_id, Phase::LoadHeader));
work.extend(state.dependencies.notify(module_id, Phase::Parse)); work.extend(state.dependencies.notify(module_id, Phase::Parse));
@ -1787,7 +1865,7 @@ fn update<'a>(
} }
MadeSpecializations { MadeSpecializations {
module_id, module_id,
ident_ids, mut ident_ids,
subs, subs,
procedures, procedures,
external_specializations_requested, external_specializations_requested,
@ -1799,32 +1877,40 @@ fn update<'a>(
state.module_cache.mono_problems.insert(module_id, problems); state.module_cache.mono_problems.insert(module_id, problems);
state.procedures.extend(procedures);
state.constrained_ident_ids.insert(module_id, ident_ids);
state.timings.insert(module_id, module_timing);
for (module_id, requested) in external_specializations_requested {
let existing = match state
.module_cache
.external_specializations_requested
.entry(module_id)
{
Vacant(entry) => entry.insert(ExternalSpecializations::default()),
Occupied(entry) => entry.into_mut(),
};
existing.extend(requested);
}
let work = state let work = state
.dependencies .dependencies
.notify(module_id, Phase::MakeSpecializations); .notify(module_id, Phase::MakeSpecializations);
state.procedures.extend(procedures);
state.timings.insert(module_id, module_timing);
if state.dependencies.solved_all() && state.goal_phase == Phase::MakeSpecializations { if state.dependencies.solved_all() && state.goal_phase == Phase::MakeSpecializations {
debug_assert!(work.is_empty(), "still work remaining {:?}", &work); debug_assert!(work.is_empty(), "still work remaining {:?}", &work);
Proc::insert_refcount_operations(arena, &mut state.procedures); Proc::insert_refcount_operations(arena, &mut state.procedures);
Proc::optimize_refcount_operations(
arena,
module_id,
&mut ident_ids,
&mut state.procedures,
);
state.constrained_ident_ids.insert(module_id, ident_ids);
for (module_id, requested) in external_specializations_requested {
let existing = match state
.module_cache
.external_specializations_requested
.entry(module_id)
{
Vacant(entry) => entry.insert(ExternalSpecializations::default()),
Occupied(entry) => entry.into_mut(),
};
existing.extend(requested);
}
// display the mono IR of the module, for debug purposes // display the mono IR of the module, for debug purposes
if roc_mono::ir::PRETTY_PRINT_IR_SYMBOLS { if roc_mono::ir::PRETTY_PRINT_IR_SYMBOLS {
let procs_string = state let procs_string = state
@ -1850,6 +1936,21 @@ fn update<'a>(
// the originally requested module, we're all done! // the originally requested module, we're all done!
return Ok(state); return Ok(state);
} else { } else {
state.constrained_ident_ids.insert(module_id, ident_ids);
for (module_id, requested) in external_specializations_requested {
let existing = match state
.module_cache
.external_specializations_requested
.entry(module_id)
{
Vacant(entry) => entry.insert(ExternalSpecializations::default()),
Occupied(entry) => entry.into_mut(),
};
existing.extend(requested);
}
start_tasks(work, &mut state, &injector, worker_listeners)?; start_tasks(work, &mut state, &injector, worker_listeners)?;
} }
@ -2039,7 +2140,7 @@ fn load_pkg_config<'a>(
let effects_module_msg = fabricate_effects_module( let effects_module_msg = fabricate_effects_module(
arena, arena,
shorthand, header.effects.effect_shortname,
module_ids, module_ids,
ident_ids_by_module, ident_ids_by_module,
mode, mode,
@ -2366,7 +2467,7 @@ enum ModuleNameEnum<'a> {
/// A filename /// A filename
App(StrLiteral<'a>), App(StrLiteral<'a>),
Interface(roc_parse::header::ModuleName<'a>), Interface(roc_parse::header::ModuleName<'a>),
PkgConfig(&'a str), PkgConfig,
} }
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
@ -2386,7 +2487,7 @@ fn send_header<'a>(
use ModuleNameEnum::*; use ModuleNameEnum::*;
let declared_name: ModuleName = match &loc_name.value { let declared_name: ModuleName = match &loc_name.value {
PkgConfig(_) => unreachable!(), PkgConfig => unreachable!(),
App(_) => ModuleName::APP.into(), App(_) => ModuleName::APP.into(),
Interface(module_name) => { Interface(module_name) => {
// TODO check to see if module_name is consistent with filename. // TODO check to see if module_name is consistent with filename.
@ -2539,6 +2640,20 @@ fn send_header<'a>(
None => HeaderFor::Interface, None => HeaderFor::Interface,
}; };
let mut package_qualified_imported_modules = MutSet::default();
for (pq_module_name, module_id) in &deps_by_name {
match pq_module_name {
PackageQualified::Unqualified(_) => {
package_qualified_imported_modules
.insert(PackageQualified::Unqualified(*module_id));
}
PackageQualified::Qualified(shorthand, _) => {
package_qualified_imported_modules
.insert(PackageQualified::Qualified(shorthand, *module_id));
}
}
}
( (
home, home,
Msg::Header( Msg::Header(
@ -2549,6 +2664,7 @@ fn send_header<'a>(
module_name: loc_name.value, module_name: loc_name.value,
packages: package_entries, packages: package_entries,
imported_modules, imported_modules,
package_qualified_imported_modules,
deps_by_name, deps_by_name,
exposes: exposed, exposes: exposed,
src: parse_state.bytes, src: parse_state.bytes,
@ -2735,9 +2851,26 @@ fn send_header_two<'a>(
// We always need to send these, even if deps is empty, // We always need to send these, even if deps is empty,
// because the coordinator thread needs to receive this message // because the coordinator thread needs to receive this message
// to decrement its "pending" count. // to decrement its "pending" count.
let module_name = ModuleNameEnum::PkgConfig(shorthand); let module_name = ModuleNameEnum::PkgConfig;
let extra = HeaderFor::PkgConfig {
config_shorthand: shorthand,
};
let mut package_qualified_imported_modules = MutSet::default();
for (pq_module_name, module_id) in &deps_by_name {
match pq_module_name {
PackageQualified::Unqualified(_) => {
package_qualified_imported_modules
.insert(PackageQualified::Unqualified(*module_id));
}
PackageQualified::Qualified(shorthand, _) => {
package_qualified_imported_modules
.insert(PackageQualified::Qualified(shorthand, *module_id));
}
}
}
let extra = HeaderFor::PkgConfig;
( (
home, home,
Msg::Header( Msg::Header(
@ -2748,6 +2881,7 @@ fn send_header_two<'a>(
module_name, module_name,
packages: package_entries, packages: package_entries,
imported_modules, imported_modules,
package_qualified_imported_modules,
deps_by_name, deps_by_name,
exposes: exposed, exposes: exposed,
src: parse_state.bytes, src: parse_state.bytes,
@ -2912,11 +3046,12 @@ fn fabricate_effects_module<'a>(
let num_exposes = header.provides.len() + 1; let num_exposes = header.provides.len() + 1;
let mut exposed: Vec<Symbol> = Vec::with_capacity(num_exposes); let mut exposed: Vec<Symbol> = Vec::with_capacity(num_exposes);
let effects = header.effects;
let module_id: ModuleId; let module_id: ModuleId;
let PlatformHeader { effects, .. } = header;
let effect_entries = unpack_exposes_entries(arena, &effects.entries); let effect_entries = unpack_exposes_entries(arena, &effects.entries);
let name = effects.type_name; let name = effects.effect_type_name;
let declared_name: ModuleName = name.into(); let declared_name: ModuleName = name.into();
let hardcoded_effect_symbols = { let hardcoded_effect_symbols = {
@ -3138,6 +3273,7 @@ fn fabricate_effects_module<'a>(
Ok(( Ok((
module_id, module_id,
Msg::MadeEffectModule { Msg::MadeEffectModule {
type_shortname: effects.effect_shortname,
constrained_module, constrained_module,
canonicalization_problems: module_output.problems, canonicalization_problems: module_output.problems,
module_docs, module_docs,
@ -3205,7 +3341,7 @@ where
// Generate documentation information // Generate documentation information
// TODO: store timing information? // TODO: store timing information?
let module_docs = match module_name { let module_docs = match module_name {
ModuleNameEnum::PkgConfig(_) => None, ModuleNameEnum::PkgConfig => None,
ModuleNameEnum::App(_) => None, ModuleNameEnum::App(_) => None,
ModuleNameEnum::Interface(name) => Some(crate::docs::generate_module_docs( ModuleNameEnum::Interface(name) => Some(crate::docs::generate_module_docs(
name.as_str().into(), name.as_str().into(),

View file

@ -317,18 +317,20 @@ impl fmt::Debug for ModuleId {
/// 4. throw away short names. stash the module id in the can env under the resolved module name /// 4. throw away short names. stash the module id in the can env under the resolved module name
/// 5. test: /// 5. test:
/// Package-qualified module name
#[derive(Debug, Clone, PartialEq, Eq, Hash)] #[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum PQModuleName<'a> { pub enum PackageQualified<'a, T> {
Unqualified(InlinableString), Unqualified(T),
Qualified(&'a str, InlinableString), Qualified(&'a str, T),
} }
impl<'a> PQModuleName<'a> { /// Package-qualified module name
pub fn to_module_name(&self) -> &InlinableString { pub type PQModuleName<'a> = PackageQualified<'a, InlinableString>;
impl<'a, T> PackageQualified<'a, T> {
pub fn as_inner(&self) -> &T {
match self { match self {
PQModuleName::Unqualified(name) => name, PackageQualified::Unqualified(name) => name,
PQModuleName::Qualified(_, name) => name, PackageQualified::Qualified(_, name) => name,
} }
} }
} }
@ -364,13 +366,13 @@ impl<'a> PackageModuleIds<'a> {
let by_name: MutMap<InlinableString, ModuleId> = self let by_name: MutMap<InlinableString, ModuleId> = self
.by_name .by_name
.into_iter() .into_iter()
.map(|(pqname, module_id)| (pqname.to_module_name().clone(), module_id)) .map(|(pqname, module_id)| (pqname.as_inner().clone(), module_id))
.collect(); .collect();
let by_id: Vec<InlinableString> = self let by_id: Vec<InlinableString> = self
.by_id .by_id
.into_iter() .into_iter()
.map(|pqname| pqname.to_module_name().clone()) .map(|pqname| pqname.as_inner().clone())
.collect(); .collect();
ModuleIds { by_name, by_id } ModuleIds { by_name, by_id }

View file

@ -18,6 +18,7 @@ roc_problem = { path = "../problem" }
ven_pretty = { path = "../../vendor/pretty" } ven_pretty = { path = "../../vendor/pretty" }
bumpalo = { version = "3.2", features = ["collections"] } bumpalo = { version = "3.2", features = ["collections"] }
ven_ena = { path = "../../vendor/ena" } ven_ena = { path = "../../vendor/ena" }
linked-hash-map = "0.5.4"
[dev-dependencies] [dev-dependencies]
roc_constrain = { path = "../constrain" } roc_constrain = { path = "../constrain" }

View file

@ -165,10 +165,10 @@ impl<'a> ParamMap<'a> {
default_branch, default_branch,
.. ..
} => { } => {
stack.extend(branches.iter().map(|b| &b.1)); stack.extend(branches.iter().map(|b| &b.2));
stack.push(default_branch); stack.push(default_branch.1);
} }
Inc(_, _, _) | Dec(_, _) => unreachable!("these have not been introduced yet"), Refcounting(_, _) => unreachable!("these have not been introduced yet"),
Ret(_) | Rethrow | Jump(_, _) | RuntimeError(_) => { Ret(_) | Rethrow | Jump(_, _) | RuntimeError(_) => {
// these are terminal, do nothing // these are terminal, do nothing
@ -508,12 +508,12 @@ impl<'a> BorrowInfState<'a> {
default_branch, default_branch,
.. ..
} => { } => {
for (_, b) in branches.iter() { for (_, _, b) in branches.iter() {
self.collect_stmt(b); self.collect_stmt(b);
} }
self.collect_stmt(default_branch); self.collect_stmt(default_branch.1);
} }
Inc(_, _, _) | Dec(_, _) => unreachable!("these have not been introduced yet"), Refcounting(_, _) => unreachable!("these have not been introduced yet"),
Ret(_) | RuntimeError(_) | Rethrow => { Ret(_) | RuntimeError(_) | Rethrow => {
// these are terminal, do nothing // these are terminal, do nothing

View file

@ -1,6 +1,6 @@
use crate::exhaustive::{Ctor, RenderAs, TagId, Union}; use crate::exhaustive::{Ctor, RenderAs, TagId, Union};
use crate::ir::{ use crate::ir::{
DestructType, Env, Expr, JoinPointId, Literal, Param, Pattern, Procs, Stmt, Wrapped, BranchInfo, DestructType, Env, Expr, JoinPointId, Literal, Param, Pattern, Procs, Stmt, Wrapped,
}; };
use crate::layout::{Builtin, Layout, LayoutCache, UnionLayout}; use crate::layout::{Builtin, Layout, LayoutCache, UnionLayout};
use roc_collections::all::{MutMap, MutSet}; use roc_collections::all::{MutMap, MutSet};
@ -1355,20 +1355,86 @@ fn compile_test<'a>(
lhs: Symbol, lhs: Symbol,
rhs: Symbol, rhs: Symbol,
fail: &'a Stmt<'a>, fail: &'a Stmt<'a>,
cond: Stmt<'a>,
) -> Stmt<'a> {
compile_test_help(
env,
ConstructorKnown::Neither,
ret_layout,
stores,
lhs,
rhs,
fail,
cond,
)
}
#[allow(clippy::too_many_arguments)]
fn compile_test_help<'a>(
env: &mut Env<'a, '_>,
branch_info: ConstructorKnown<'a>,
ret_layout: Layout<'a>,
stores: bumpalo::collections::Vec<'a, (Symbol, Layout<'a>, Expr<'a>)>,
lhs: Symbol,
rhs: Symbol,
fail: &'a Stmt<'a>,
mut cond: Stmt<'a>, mut cond: Stmt<'a>,
) -> Stmt<'a> { ) -> Stmt<'a> {
// if test_symbol then cond else fail // if test_symbol then cond else fail
let test_symbol = env.unique_symbol(); let test_symbol = env.unique_symbol();
let arena = env.arena; let arena = env.arena;
cond = crate::ir::cond( let (pass_info, fail_info) = {
env, use ConstructorKnown::*;
test_symbol, match branch_info {
Layout::Builtin(Builtin::Int1), Both {
cond, scrutinee,
fail.clone(), layout,
pass,
fail,
} => {
let pass_info = BranchInfo::Constructor {
scrutinee,
layout: layout.clone(),
tag_id: pass,
};
let fail_info = BranchInfo::Constructor {
scrutinee,
layout: layout.clone(),
tag_id: fail,
};
(pass_info, fail_info)
}
OnlyPass {
scrutinee,
layout,
tag_id,
} => {
let pass_info = BranchInfo::Constructor {
scrutinee,
layout: layout.clone(),
tag_id,
};
(pass_info, BranchInfo::None)
}
Neither => (BranchInfo::None, BranchInfo::None),
}
};
let branches = env.arena.alloc([(1u64, pass_info, cond)]);
let default_branch = (fail_info, &*env.arena.alloc(fail.clone()));
cond = Stmt::Switch {
cond_symbol: test_symbol,
cond_layout: Layout::Builtin(Builtin::Int1),
ret_layout, ret_layout,
); branches,
default_branch,
};
let test = Expr::Call(crate::ir::Call { let test = Expr::Call(crate::ir::Call {
call_type: crate::ir::CallType::LowLevel { op: LowLevel::Eq }, call_type: crate::ir::CallType::LowLevel { op: LowLevel::Eq },
@ -1412,6 +1478,53 @@ fn compile_tests<'a>(
cond cond
} }
enum ConstructorKnown<'a> {
Both {
scrutinee: Symbol,
layout: Layout<'a>,
pass: u8,
fail: u8,
},
OnlyPass {
scrutinee: Symbol,
layout: Layout<'a>,
tag_id: u8,
},
Neither,
}
impl<'a> ConstructorKnown<'a> {
fn from_test_chain(
cond_symbol: Symbol,
cond_layout: &Layout<'a>,
test_chain: &[(Path, Test)],
) -> Self {
match test_chain {
[(path, test)] => match (path, test) {
(Path::Empty, Test::IsCtor { tag_id, union, .. }) => {
if union.alternatives.len() == 2 {
// excluded middle: we also know the tag_id in the fail branch
ConstructorKnown::Both {
layout: cond_layout.clone(),
scrutinee: cond_symbol,
pass: *tag_id,
fail: (*tag_id == 0) as u8,
}
} else {
ConstructorKnown::OnlyPass {
layout: cond_layout.clone(),
scrutinee: cond_symbol,
tag_id: *tag_id,
}
}
}
_ => ConstructorKnown::Neither,
},
_ => ConstructorKnown::Neither,
}
}
}
// TODO procs and layout are currently unused, but potentially required // TODO procs and layout are currently unused, but potentially required
// for defining optional fields? // for defining optional fields?
// if not, do remove // if not, do remove
@ -1447,8 +1560,6 @@ fn decide_to_branching<'a>(
} => { } => {
// generate a (nested) if-then-else // generate a (nested) if-then-else
let (tests, guard) = stores_and_condition(env, cond_symbol, &cond_layout, test_chain);
let pass_expr = decide_to_branching( let pass_expr = decide_to_branching(
env, env,
procs, procs,
@ -1471,6 +1582,11 @@ fn decide_to_branching<'a>(
jumps, jumps,
); );
let chain_branch_info =
ConstructorKnown::from_test_chain(cond_symbol, &cond_layout, &test_chain);
let (tests, guard) = stores_and_condition(env, cond_symbol, &cond_layout, test_chain);
let number_of_tests = tests.len() as i64 + guard.is_some() as i64; let number_of_tests = tests.len() as i64 + guard.is_some() as i64;
debug_assert!(number_of_tests > 0); debug_assert!(number_of_tests > 0);
@ -1478,7 +1594,26 @@ fn decide_to_branching<'a>(
let fail = env.arena.alloc(fail_expr); let fail = env.arena.alloc(fail_expr);
if number_of_tests == 1 { if number_of_tests == 1 {
// if there is just one test, compile to a simple if-then-else // if there is just one test, compile to a simple if-then-else
compile_tests(env, ret_layout, tests, guard, fail, pass_expr)
if guard.is_none() {
// use knowledge about constructors for optimization
debug_assert_eq!(tests.len(), 1);
let (new_stores, lhs, rhs, _layout) = tests.into_iter().next().unwrap();
compile_test_help(
env,
chain_branch_info,
ret_layout.clone(),
new_stores,
lhs,
rhs,
fail,
pass_expr,
)
} else {
compile_tests(env, ret_layout, tests, guard, fail, pass_expr)
}
} else { } else {
// otherwise, we use a join point so the code for the `else` case // otherwise, we use a join point so the code for the `else` case
// is only generated once. // is only generated once.
@ -1540,7 +1675,7 @@ fn decide_to_branching<'a>(
other => todo!("other {:?}", other), other => todo!("other {:?}", other),
}; };
branches.push((tag, branch)); branches.push((tag, BranchInfo::None, branch));
} }
// We have learned more about the exact layout of the cond (based on the path) // We have learned more about the exact layout of the cond (based on the path)
@ -1549,7 +1684,7 @@ fn decide_to_branching<'a>(
cond_layout: inner_cond_layout, cond_layout: inner_cond_layout,
cond_symbol: inner_cond_symbol, cond_symbol: inner_cond_symbol,
branches: branches.into_bump_slice(), branches: branches.into_bump_slice(),
default_branch: env.arena.alloc(default_branch), default_branch: (BranchInfo::None, env.arena.alloc(default_branch)),
ret_layout, ret_layout,
}; };

View file

@ -0,0 +1,570 @@
use crate::ir::{BranchInfo, Expr, ModifyRc, Stmt, Wrapped};
use crate::layout::{Layout, UnionLayout};
use bumpalo::collections::Vec;
use bumpalo::Bump;
use linked_hash_map::LinkedHashMap;
use roc_collections::all::MutMap;
use roc_module::symbol::{IdentIds, ModuleId, Symbol};
// This file is heavily inspired by the Perceus paper
//
// https://www.microsoft.com/en-us/research/uploads/prod/2020/11/perceus-tr-v1.pdf
//
// With how we insert RC instructions, this pattern is very common:
//
// when xs is
// Cons x xx ->
// inc x;
// inc xx;
// dec xs;
// ...
//
// This pattern is very inefficient, because it will first increment the tail (recursively),
// and then decrement it again. We can see this more clearly if we inline/specialize the `dec xs`
//
// when xs is
// Cons x xx ->
// inc x;
// inc xx;
// dec x;
// dec xx;
// decref xs
// ...
//
// Here `decref` non-recursively decrements (and possibly frees) `xs`. Now the idea is that we can
// fuse `inc x; dec x` by just doing nothing: they cancel out
//
// We can do slightly more, in the `Nil` case
//
// when xs is
// ...
// Nil ->
// dec xs;
// accum
//
// Here we know that `Nil` is represented by NULL (a linked list has a NullableUnwrapped layout),
// so we can just drop the `dec xs`
//
// # complications
//
// Let's work through the `Cons x xx` example
//
// First we need to know the constructor of `xs` in the particular block. This information would
// normally be lost when we compile pattern matches, but we keep it in the `BrachInfo` field of
// switch branches. here we also store the symbol that was switched on, and the layout of that
// symbol.
//
// Next, we need to know that `x` and `xx` alias the head and tail of `xs`. We store that
// information when encountering a `AccessAtIndex` into `xs`.
//
// In most cases these two pieces of information are enough. We keep track of a
// `LinkedHashMap<Symbol, i64>`: `LinkedHashMap` remembers insertion order, which is crucial here.
// The `i64` value represents the increment (positive value) or decrement (negative value). When
// the value is 0, increments and decrements have cancelled out and we just emit nothing.
//
// We need to do slightly more work in the case of
//
// when xs is
// Cons _ xx ->
// recurse xx (1 + accum)
//
// In this case, the head is not bound. That's OK when the list elements are not refcounted (or
// contain anything refcounted). But when they do, we can't expand the `dec xs` because there is no
// way to reference the head element.
//
// Our refcounting mechanism can't deal well with unused variables (it'll leak their memory). But
// we can insert the access after RC instructions have been inserted. So in the above case we
// actually get
//
// when xs is
// Cons _ xx ->
// let v1 = AccessAtIndex 1 xs
// inc v1;
// let xx = AccessAtIndex 2 xs
// inc xx;
// dec v1;
// dec xx;
// decref xs;
// recurse xx (1 + accum)
//
// Here we see another problem: the increments and decrements cannot be fused immediately.
// Therefore we add a rule that we can "push down" increments and decrements past
//
// - `Let`s binding a `AccessAtIndex`
// - refcount operations
//
// This allows the aforementioned `LinkedHashMap` to accumulate all changes, and then emit
// all (uncancelled) modifications at once before any "non-push-downable-stmt", hence:
//
// when xs is
// Cons _ xx ->
// let v1 = AccessAtIndex 1 xs
// let xx = AccessAtIndex 2 xs
// dec v1;
// decref xs;
// recurse xx (1 + accum)
pub struct Env<'a, 'i> {
/// bump allocator
pub arena: &'a Bump,
/// required for creating new `Symbol`s
pub home: ModuleId,
pub ident_ids: &'i mut IdentIds,
/// layout of the symbol
pub layout_map: MutMap<Symbol, Layout<'a>>,
/// record for each symbol, the aliases of its fields
pub alias_map: MutMap<Symbol, MutMap<u64, Symbol>>,
/// for a symbol (found in a `when x is`), record in which branch we are
pub constructor_map: MutMap<Symbol, u64>,
/// increments and decrements deferred until later
pub deferred: Deferred<'a>,
}
#[derive(Debug)]
pub struct Deferred<'a> {
pub inc_dec_map: LinkedHashMap<Symbol, i64>,
pub assignments: Vec<'a, (Symbol, Expr<'a>, Layout<'a>)>,
pub decrefs: Vec<'a, Symbol>,
}
impl<'a, 'i> Env<'a, 'i> {
fn insert_branch_info(&mut self, info: &BranchInfo<'a>) {
match info {
BranchInfo::Constructor {
layout,
scrutinee,
tag_id,
} => {
self.constructor_map.insert(*scrutinee, *tag_id as u64);
self.layout_map.insert(*scrutinee, layout.clone());
}
BranchInfo::None => (),
}
}
fn remove_branch_info(&mut self, info: &BranchInfo) {
match info {
BranchInfo::Constructor { scrutinee, .. } => {
self.constructor_map.remove(scrutinee);
self.layout_map.remove(scrutinee);
}
BranchInfo::None => (),
}
}
pub fn unique_symbol(&mut self) -> Symbol {
let ident_id = self.ident_ids.gen_unique();
self.home.register_debug_idents(&self.ident_ids);
Symbol::new(self.home, ident_id)
}
fn manual_unique_symbol(home: ModuleId, ident_ids: &mut IdentIds) -> Symbol {
let ident_id = ident_ids.gen_unique();
home.register_debug_idents(&ident_ids);
Symbol::new(home, ident_id)
}
}
fn layout_for_constructor<'a>(
_arena: &'a Bump,
layout: &Layout<'a>,
constructor: u64,
) -> ConstructorLayout<&'a [Layout<'a>]> {
use ConstructorLayout::*;
use Layout::*;
match layout {
Union(variant) => {
use UnionLayout::*;
match variant {
NullableUnwrapped {
nullable_id,
other_fields,
} => {
if (constructor > 0) == *nullable_id {
ConstructorLayout::IsNull
} else {
ConstructorLayout::HasFields(other_fields)
}
}
NullableWrapped {
nullable_id,
other_tags,
} => {
if constructor as i64 == *nullable_id {
ConstructorLayout::IsNull
} else {
ConstructorLayout::HasFields(other_tags[constructor as usize])
}
}
NonRecursive(fields) | Recursive(fields) => HasFields(fields[constructor as usize]),
NonNullableUnwrapped(fields) => {
debug_assert_eq!(constructor, 0);
HasFields(fields)
}
}
}
_ => unreachable!(),
}
}
fn work_for_constructor<'a>(
env: &mut Env<'a, '_>,
symbol: &Symbol,
) -> ConstructorLayout<Vec<'a, Symbol>> {
use ConstructorLayout::*;
let mut result = Vec::new_in(env.arena);
let constructor = match env.constructor_map.get(symbol) {
None => return ConstructorLayout::Unknown,
Some(v) => *v,
};
let full_layout = match env.layout_map.get(symbol) {
None => return ConstructorLayout::Unknown,
Some(v) => v,
};
let field_aliases = env.alias_map.get(symbol);
match layout_for_constructor(env.arena, full_layout, constructor) {
Unknown => Unknown,
IsNull => IsNull,
HasFields(cons_layout) => {
// figure out if there is at least one aliased refcounted field. Only then
// does it make sense to inline the decrement
let at_least_one_aliased = (|| {
for (i, field_layout) in cons_layout.iter().enumerate() {
if field_layout.contains_refcounted()
&& field_aliases.and_then(|map| map.get(&(i as u64))).is_some()
{
return true;
}
}
false
})();
// for each field, if it has refcounted content, check if it has an alias
// if so, use the alias, otherwise load the field.
for (i, field_layout) in cons_layout.iter().enumerate() {
if field_layout.contains_refcounted() {
match field_aliases.and_then(|map| map.get(&(i as u64))) {
Some(alias_symbol) => {
// the field was bound in a pattern match
result.push(*alias_symbol);
}
None if at_least_one_aliased => {
// the field was not bound in a pattern match
// we have to extract it now, but we only extract it
// if at least one field is aliased.
let expr = Expr::AccessAtIndex {
index: i as u64,
field_layouts: cons_layout,
structure: *symbol,
wrapped: Wrapped::MultiTagUnion,
};
// create a fresh symbol for this field
let alias_symbol = Env::manual_unique_symbol(env.home, env.ident_ids);
let layout = if let Layout::RecursivePointer = field_layout {
full_layout.clone()
} else {
field_layout.clone()
};
env.deferred.assignments.push((alias_symbol, expr, layout));
result.push(alias_symbol);
}
None => {
// if all refcounted fields were unaliased, generate a normal decrement
// of the whole structure (less code generated this way)
return ConstructorLayout::Unknown;
}
}
}
}
ConstructorLayout::HasFields(result)
}
}
}
fn can_push_inc_through(stmt: &Stmt) -> bool {
use Stmt::*;
match stmt {
Let(_, expr, _, _) => {
// we can always delay an increment/decrement until after a field access
matches!(expr, Expr::AccessAtIndex { .. } | Expr::Literal(_))
}
Refcounting(ModifyRc::Inc(_, _), _) => true,
Refcounting(ModifyRc::Dec(_), _) => true,
_ => false,
}
}
#[derive(Debug)]
enum ConstructorLayout<T> {
IsNull,
HasFields(T),
Unknown,
}
pub fn expand_and_cancel<'a>(env: &mut Env<'a, '_>, stmt: &'a Stmt<'a>) -> &'a Stmt<'a> {
use Stmt::*;
let mut deferred_default = Deferred {
inc_dec_map: Default::default(),
assignments: Vec::new_in(env.arena),
decrefs: Vec::new_in(env.arena),
};
let deferred = if can_push_inc_through(stmt) {
deferred_default
} else {
std::mem::swap(&mut deferred_default, &mut env.deferred);
deferred_default
};
let mut result = {
match stmt {
Let(mut symbol, expr, layout, cont) => {
env.layout_map.insert(symbol, layout.clone());
let mut expr = expr;
let mut layout = layout;
let mut cont = cont;
// prevent long chains of `Let`s from blowing the stack
let mut literal_stack = Vec::new_in(env.arena);
while !matches!(&expr, Expr::AccessAtIndex { .. } ) {
if let Stmt::Let(symbol1, expr1, layout1, cont1) = cont {
literal_stack.push((symbol, expr.clone(), layout.clone()));
symbol = *symbol1;
expr = expr1;
layout = layout1;
cont = cont1;
} else {
break;
}
}
let new_cont;
if let Expr::AccessAtIndex {
structure, index, ..
} = &expr
{
let entry = env
.alias_map
.entry(*structure)
.or_insert_with(MutMap::default);
entry.insert(*index, symbol);
new_cont = expand_and_cancel(env, cont);
// make sure to remove the alias, so other branches don't use it by accident
env.alias_map
.get_mut(structure)
.and_then(|map| map.remove(index));
} else {
new_cont = expand_and_cancel(env, cont);
}
let stmt = Let(symbol, expr.clone(), layout.clone(), new_cont);
let mut stmt = &*env.arena.alloc(stmt);
for (symbol, expr, layout) in literal_stack.into_iter().rev() {
stmt = env.arena.alloc(Stmt::Let(symbol, expr, layout, stmt));
}
stmt
}
Switch {
cond_symbol,
cond_layout,
ret_layout,
branches,
default_branch,
} => {
let mut new_branches = Vec::with_capacity_in(branches.len(), env.arena);
for (id, info, branch) in branches.iter() {
env.insert_branch_info(info);
let branch = expand_and_cancel(env, branch);
env.remove_branch_info(info);
env.constructor_map.remove(cond_symbol);
new_branches.push((*id, info.clone(), branch.clone()));
}
env.insert_branch_info(&default_branch.0);
let new_default = (
default_branch.0.clone(),
expand_and_cancel(env, default_branch.1),
);
env.remove_branch_info(&default_branch.0);
let stmt = Switch {
cond_symbol: *cond_symbol,
cond_layout: cond_layout.clone(),
ret_layout: ret_layout.clone(),
branches: new_branches.into_bump_slice(),
default_branch: new_default,
};
&*env.arena.alloc(stmt)
}
Refcounting(ModifyRc::DecRef(_symbol), _cont) => unreachable!("not introduced yet"),
Refcounting(ModifyRc::Dec(symbol), cont) => {
use ConstructorLayout::*;
match work_for_constructor(env, symbol) {
HasFields(dec_symbols) => {
// we can inline the decrement
// decref the current cell
env.deferred.decrefs.push(*symbol);
// and record decrements for all the fields
for dec_symbol in dec_symbols {
let count = env.deferred.inc_dec_map.entry(dec_symbol).or_insert(0);
*count -= 1;
}
}
Unknown => {
// we can't inline the decrement; just record it
let count = env.deferred.inc_dec_map.entry(*symbol).or_insert(0);
*count -= 1;
}
IsNull => {
// we decrement a value represented as `NULL` at runtime;
// we can drop this decrement completely
}
}
expand_and_cancel(env, cont)
}
Refcounting(ModifyRc::Inc(symbol, inc_amount), cont) => {
let count = env.deferred.inc_dec_map.entry(*symbol).or_insert(0);
*count += *inc_amount as i64;
expand_and_cancel(env, cont)
}
Invoke {
symbol,
call,
layout,
pass,
fail,
} => {
let pass = expand_and_cancel(env, pass);
let fail = expand_and_cancel(env, fail);
let stmt = Invoke {
symbol: *symbol,
call: call.clone(),
layout: layout.clone(),
pass,
fail,
};
env.arena.alloc(stmt)
}
Join {
id,
parameters,
continuation,
remainder,
} => {
let continuation = expand_and_cancel(env, continuation);
let remainder = expand_and_cancel(env, remainder);
let stmt = Join {
id: *id,
parameters,
continuation,
remainder,
};
env.arena.alloc(stmt)
}
Rethrow | Ret(_) | Jump(_, _) | RuntimeError(_) => stmt,
}
};
for symbol in deferred.decrefs {
let stmt = Refcounting(ModifyRc::DecRef(symbol), result);
result = env.arena.alloc(stmt);
}
// do all decrements
for (symbol, amount) in deferred.inc_dec_map.iter().rev() {
use std::cmp::Ordering;
match amount.cmp(&0) {
Ordering::Equal => {
// do nothing else
}
Ordering::Greater => {
// do nothing yet
}
Ordering::Less => {
// the RC insertion should not double decrement in a block
debug_assert_eq!(*amount, -1);
// insert missing decrements
let stmt = Refcounting(ModifyRc::Dec(*symbol), result);
result = env.arena.alloc(stmt);
}
}
}
for (symbol, amount) in deferred.inc_dec_map.into_iter().rev() {
use std::cmp::Ordering;
match amount.cmp(&0) {
Ordering::Equal => {
// do nothing else
}
Ordering::Greater => {
// insert missing increments
let stmt = Refcounting(ModifyRc::Inc(symbol, amount as u64), result);
result = env.arena.alloc(stmt);
}
Ordering::Less => {
// already done
}
}
}
for (symbol, expr, layout) in deferred.assignments {
let stmt = Stmt::Let(symbol, expr, layout, result);
result = env.arena.alloc(stmt);
}
result
}

View file

@ -1,5 +1,5 @@
use crate::borrow::ParamMap; use crate::borrow::ParamMap;
use crate::ir::{Expr, JoinPointId, Param, Proc, Stmt}; use crate::ir::{Expr, JoinPointId, ModifyRc, Param, Proc, Stmt};
use crate::layout::Layout; use crate::layout::Layout;
use bumpalo::collections::Vec; use bumpalo::collections::Vec;
use bumpalo::Bump; use bumpalo::Bump;
@ -52,8 +52,9 @@ pub fn occuring_variables(stmt: &Stmt<'_>) -> (MutSet<Symbol>, MutSet<Symbol>) {
Rethrow => {} Rethrow => {}
Inc(symbol, _, cont) | Dec(symbol, cont) => { Refcounting(modify, cont) => {
result.insert(*symbol); let symbol = modify.get_symbol();
result.insert(symbol);
stack.push(cont); stack.push(cont);
} }
@ -81,8 +82,8 @@ pub fn occuring_variables(stmt: &Stmt<'_>) -> (MutSet<Symbol>, MutSet<Symbol>) {
} => { } => {
result.insert(*cond_symbol); result.insert(*cond_symbol);
stack.extend(branches.iter().map(|(_, s)| s)); stack.extend(branches.iter().map(|(_, _, s)| s));
stack.push(default_branch); stack.push(default_branch.1);
} }
RuntimeError(_) => {} RuntimeError(_) => {}
@ -277,7 +278,8 @@ impl<'a> Context<'a> {
return stmt; return stmt;
} }
self.arena.alloc(Stmt::Inc(symbol, inc_amount, stmt)) let modify = ModifyRc::Inc(symbol, inc_amount);
self.arena.alloc(Stmt::Refcounting(modify, stmt))
} }
fn add_dec(&self, symbol: Symbol, stmt: &'a Stmt<'a>) -> &'a Stmt<'a> { fn add_dec(&self, symbol: Symbol, stmt: &'a Stmt<'a>) -> &'a Stmt<'a> {
@ -293,7 +295,8 @@ impl<'a> Context<'a> {
return stmt; return stmt;
} }
self.arena.alloc(Stmt::Dec(symbol, stmt)) let modify = ModifyRc::Dec(symbol);
self.arena.alloc(Stmt::Refcounting(modify, stmt))
} }
fn add_inc_before_consume_all( fn add_inc_before_consume_all(
@ -820,13 +823,13 @@ impl<'a> Context<'a> {
let case_live_vars = collect_stmt(stmt, &self.jp_live_vars, MutSet::default()); let case_live_vars = collect_stmt(stmt, &self.jp_live_vars, MutSet::default());
let branches = Vec::from_iter_in( let branches = Vec::from_iter_in(
branches.iter().map(|(label, branch)| { branches.iter().map(|(label, info, branch)| {
// TODO should we use ctor info like Lean? // TODO should we use ctor info like Lean?
let ctx = self.clone(); let ctx = self.clone();
let (b, alt_live_vars) = ctx.visit_stmt(branch); let (b, alt_live_vars) = ctx.visit_stmt(branch);
let b = ctx.add_dec_for_alt(&case_live_vars, &alt_live_vars, b); let b = ctx.add_dec_for_alt(&case_live_vars, &alt_live_vars, b);
(*label, b.clone()) (*label, info.clone(), b.clone())
}), }),
self.arena, self.arena,
) )
@ -835,8 +838,12 @@ impl<'a> Context<'a> {
let default_branch = { let default_branch = {
// TODO should we use ctor info like Lean? // TODO should we use ctor info like Lean?
let ctx = self.clone(); let ctx = self.clone();
let (b, alt_live_vars) = ctx.visit_stmt(default_branch); let (b, alt_live_vars) = ctx.visit_stmt(default_branch.1);
ctx.add_dec_for_alt(&case_live_vars, &alt_live_vars, b)
(
default_branch.0.clone(),
ctx.add_dec_for_alt(&case_live_vars, &alt_live_vars, b),
)
}; };
let switch = self.arena.alloc(Switch { let switch = self.arena.alloc(Switch {
@ -850,7 +857,7 @@ impl<'a> Context<'a> {
(switch, case_live_vars) (switch, case_live_vars)
} }
RuntimeError(_) | Inc(_, _, _) | Dec(_, _) => (stmt, MutSet::default()), RuntimeError(_) | Refcounting(_, _) => (stmt, MutSet::default()),
} }
} }
} }
@ -901,23 +908,12 @@ pub fn collect_stmt(
vars vars
} }
Inc(symbol, _, cont) | Dec(symbol, cont) => { Refcounting(modify, cont) => {
vars.insert(*symbol); let symbol = modify.get_symbol();
vars.insert(symbol);
collect_stmt(cont, jp_live_vars, vars) collect_stmt(cont, jp_live_vars, vars)
} }
Jump(id, arguments) => {
vars.extend(arguments.iter().copied());
// NOTE deviation from Lean
// we fall through when no join point is available
if let Some(jvars) = jp_live_vars.get(id) {
vars.extend(jvars);
}
vars
}
Join { Join {
id: j, id: j,
parameters, parameters,
@ -935,6 +931,18 @@ pub fn collect_stmt(
collect_stmt(b, &jp_live_vars, vars) collect_stmt(b, &jp_live_vars, vars)
} }
Jump(id, arguments) => {
vars.extend(arguments.iter().copied());
// NOTE deviation from Lean
// we fall through when no join point is available
if let Some(jvars) = jp_live_vars.get(id) {
vars.extend(jvars);
}
vars
}
Switch { Switch {
cond_symbol, cond_symbol,
branches, branches,
@ -943,11 +951,11 @@ pub fn collect_stmt(
} => { } => {
vars.insert(*cond_symbol); vars.insert(*cond_symbol);
for (_, branch) in branches.iter() { for (_, _info, branch) in branches.iter() {
vars.extend(collect_stmt(branch, jp_live_vars, vars.clone())); vars.extend(collect_stmt(branch, jp_live_vars, vars.clone()));
} }
vars.extend(collect_stmt(default_branch, jp_live_vars, vars.clone())); vars.extend(collect_stmt(default_branch.1, jp_live_vars, vars.clone()));
vars vars
} }

View file

@ -157,6 +157,36 @@ impl<'a> Proc<'a> {
crate::inc_dec::visit_proc(arena, borrow_params, proc); crate::inc_dec::visit_proc(arena, borrow_params, proc);
} }
} }
pub fn optimize_refcount_operations<'i>(
arena: &'a Bump,
home: ModuleId,
ident_ids: &'i mut IdentIds,
procs: &mut MutMap<(Symbol, Layout<'a>), Proc<'a>>,
) {
use crate::expand_rc;
let deferred = expand_rc::Deferred {
inc_dec_map: Default::default(),
assignments: Vec::new_in(arena),
decrefs: Vec::new_in(arena),
};
let mut env = expand_rc::Env {
home,
arena,
ident_ids,
layout_map: Default::default(),
alias_map: Default::default(),
constructor_map: Default::default(),
deferred,
};
for (_, proc) in procs.iter_mut() {
let b = expand_rc::expand_and_cancel(&mut env, arena.alloc(proc.body.clone()));
proc.body = b.clone();
}
}
} }
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
@ -729,8 +759,8 @@ pub fn cond<'a>(
fail: Stmt<'a>, fail: Stmt<'a>,
ret_layout: Layout<'a>, ret_layout: Layout<'a>,
) -> Stmt<'a> { ) -> Stmt<'a> {
let branches = env.arena.alloc([(1u64, pass)]); let branches = env.arena.alloc([(1u64, BranchInfo::None, pass)]);
let default_branch = env.arena.alloc(fail); let default_branch = (BranchInfo::None, &*env.arena.alloc(fail));
Stmt::Switch { Stmt::Switch {
cond_symbol, cond_symbol,
@ -758,16 +788,15 @@ pub enum Stmt<'a> {
cond_layout: Layout<'a>, cond_layout: Layout<'a>,
/// The u64 in the tuple will be compared directly to the condition Expr. /// The u64 in the tuple will be compared directly to the condition Expr.
/// If they are equal, this branch will be taken. /// If they are equal, this branch will be taken.
branches: &'a [(u64, Stmt<'a>)], branches: &'a [(u64, BranchInfo<'a>, Stmt<'a>)],
/// If no other branches pass, this default branch will be taken. /// If no other branches pass, this default branch will be taken.
default_branch: &'a Stmt<'a>, default_branch: (BranchInfo<'a>, &'a Stmt<'a>),
/// Each branch must return a value of this type. /// Each branch must return a value of this type.
ret_layout: Layout<'a>, ret_layout: Layout<'a>,
}, },
Ret(Symbol), Ret(Symbol),
Rethrow, Rethrow,
Inc(Symbol, u64, &'a Stmt<'a>), Refcounting(ModifyRc, &'a Stmt<'a>),
Dec(Symbol, &'a Stmt<'a>),
Join { Join {
id: JoinPointId, id: JoinPointId,
parameters: &'a [Param<'a>], parameters: &'a [Param<'a>],
@ -780,6 +809,91 @@ pub enum Stmt<'a> {
RuntimeError(&'a str), RuntimeError(&'a str),
} }
/// in the block below, symbol `scrutinee` is assumed be be of shape `tag_id`
#[derive(Clone, Debug, PartialEq)]
pub enum BranchInfo<'a> {
None,
Constructor {
scrutinee: Symbol,
layout: Layout<'a>,
tag_id: u8,
},
}
impl<'a> BranchInfo<'a> {
pub fn to_doc<'b, D, A>(&'b self, alloc: &'b D) -> DocBuilder<'b, D, A>
where
D: DocAllocator<'b, A>,
D::Doc: Clone,
A: Clone,
{
use BranchInfo::*;
match self {
Constructor {
tag_id,
scrutinee,
layout: _,
} if PRETTY_PRINT_IR_SYMBOLS => alloc
.hardline()
.append(" BranchInfo: { scrutinee: ")
.append(symbol_to_doc(alloc, *scrutinee))
.append(", tag_id: ")
.append(format!("{}", tag_id))
.append("} "),
_ => alloc.text(""),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum ModifyRc {
Inc(Symbol, u64),
Dec(Symbol),
DecRef(Symbol),
}
impl ModifyRc {
pub fn to_doc<'b, D, A>(&'b self, alloc: &'b D) -> DocBuilder<'b, D, A>
where
D: DocAllocator<'b, A>,
D::Doc: Clone,
A: Clone,
{
use ModifyRc::*;
match self {
Inc(symbol, 1) => alloc
.text("inc ")
.append(symbol_to_doc(alloc, *symbol))
.append(";"),
Inc(symbol, n) => alloc
.text("inc ")
.append(alloc.text(format!("{}", n)))
.append(symbol_to_doc(alloc, *symbol))
.append(";"),
Dec(symbol) => alloc
.text("dec ")
.append(symbol_to_doc(alloc, *symbol))
.append(";"),
DecRef(symbol) => alloc
.text("decref ")
.append(symbol_to_doc(alloc, *symbol))
.append(";"),
}
}
pub fn get_symbol(&self) -> Symbol {
use ModifyRc::*;
match self {
Inc(symbol, _) => *symbol,
Dec(symbol) => *symbol,
DecRef(symbol) => *symbol,
}
}
}
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub enum Literal<'a> { pub enum Literal<'a> {
// Literals // Literals
@ -1133,13 +1247,18 @@ impl<'a> Stmt<'a> {
.text("let ") .text("let ")
.append(symbol_to_doc(alloc, *symbol)) .append(symbol_to_doc(alloc, *symbol))
//.append(" : ") //.append(" : ")
//.append(alloc.text(format!("{:?}", layout))) //.append(alloc.text(format!("{:?}", _layout)))
.append(" = ") .append(" = ")
.append(expr.to_doc(alloc)) .append(expr.to_doc(alloc))
.append(";") .append(";")
.append(alloc.hardline()) .append(alloc.hardline())
.append(cont.to_doc(alloc)), .append(cont.to_doc(alloc)),
Refcounting(modify, cont) => modify
.to_doc(alloc)
.append(alloc.hardline())
.append(cont.to_doc(alloc)),
Invoke { Invoke {
symbol, symbol,
call, call,
@ -1186,16 +1305,18 @@ impl<'a> Stmt<'a> {
.. ..
} => { } => {
match branches { match branches {
[(1, pass)] => { [(1, info, pass)] => {
let fail = default_branch; let fail = default_branch.1;
alloc alloc
.text("if ") .text("if ")
.append(symbol_to_doc(alloc, *cond_symbol)) .append(symbol_to_doc(alloc, *cond_symbol))
.append(" then") .append(" then")
.append(info.to_doc(alloc))
.append(alloc.hardline()) .append(alloc.hardline())
.append(pass.to_doc(alloc).indent(4)) .append(pass.to_doc(alloc).indent(4))
.append(alloc.hardline()) .append(alloc.hardline())
.append(alloc.text("else")) .append(alloc.text("else"))
.append(default_branch.0.to_doc(alloc))
.append(alloc.hardline()) .append(alloc.hardline())
.append(fail.to_doc(alloc).indent(4)) .append(fail.to_doc(alloc).indent(4))
} }
@ -1204,12 +1325,12 @@ impl<'a> Stmt<'a> {
let default_doc = alloc let default_doc = alloc
.text("default:") .text("default:")
.append(alloc.hardline()) .append(alloc.hardline())
.append(default_branch.to_doc(alloc).indent(4)) .append(default_branch.1.to_doc(alloc).indent(4))
.indent(4); .indent(4);
let branches_docs = branches let branches_docs = branches
.iter() .iter()
.map(|(tag, expr)| { .map(|(tag, _info, expr)| {
alloc alloc
.text(format!("case {}:", tag)) .text(format!("case {}:", tag))
.append(alloc.hardline()) .append(alloc.hardline())
@ -1267,25 +1388,6 @@ impl<'a> Stmt<'a> {
.append(alloc.intersperse(it, alloc.space())) .append(alloc.intersperse(it, alloc.space()))
.append(";") .append(";")
} }
Inc(symbol, 1, cont) => alloc
.text("inc ")
.append(symbol_to_doc(alloc, *symbol))
.append(";")
.append(alloc.hardline())
.append(cont.to_doc(alloc)),
Inc(symbol, n, cont) => alloc
.text("inc ")
.append(alloc.text(format!("{}", n)))
.append(symbol_to_doc(alloc, *symbol))
.append(";")
.append(alloc.hardline())
.append(cont.to_doc(alloc)),
Dec(symbol, cont) => alloc
.text("dec ")
.append(symbol_to_doc(alloc, *symbol))
.append(";")
.append(alloc.hardline())
.append(cont.to_doc(alloc)),
} }
} }
@ -4657,17 +4759,17 @@ fn substitute_in_stmt_help<'a>(
default_branch, default_branch,
ret_layout, ret_layout,
} => { } => {
let opt_default = substitute_in_stmt_help(arena, default_branch, subs); let opt_default = substitute_in_stmt_help(arena, default_branch.1, subs);
let mut did_change = false; let mut did_change = false;
let opt_branches = Vec::from_iter_in( let opt_branches = Vec::from_iter_in(
branches.iter().map(|(label, branch)| { branches.iter().map(|(label, info, branch)| {
match substitute_in_stmt_help(arena, branch, subs) { match substitute_in_stmt_help(arena, branch, subs) {
None => None, None => None,
Some(branch) => { Some(branch) => {
did_change = true; did_change = true;
Some((*label, branch.clone())) Some((*label, info.clone(), branch.clone()))
} }
} }
}), }),
@ -4675,7 +4777,10 @@ fn substitute_in_stmt_help<'a>(
); );
if opt_default.is_some() || did_change { if opt_default.is_some() || did_change {
let default_branch = opt_default.unwrap_or(default_branch); let default_branch = (
default_branch.0.clone(),
opt_default.unwrap_or(default_branch.1),
);
let branches = if did_change { let branches = if did_change {
let new = Vec::from_iter_in( let new = Vec::from_iter_in(
@ -4708,14 +4813,13 @@ fn substitute_in_stmt_help<'a>(
Some(s) => Some(arena.alloc(Ret(s))), Some(s) => Some(arena.alloc(Ret(s))),
None => None, None => None,
}, },
Inc(symbol, inc, cont) => match substitute_in_stmt_help(arena, cont, subs) { Refcounting(modify, cont) => {
Some(cont) => Some(arena.alloc(Inc(*symbol, *inc, cont))), // TODO should we substitute in the ModifyRc?
None => None, match substitute_in_stmt_help(arena, cont, subs) {
}, Some(cont) => Some(arena.alloc(Refcounting(*modify, cont))),
Dec(symbol, cont) => match substitute_in_stmt_help(arena, cont, subs) { None => None,
Some(cont) => Some(arena.alloc(Dec(*symbol, cont))), }
None => None, }
},
Jump(id, args) => { Jump(id, args) => {
let mut did_change = false; let mut did_change = false;

View file

@ -647,8 +647,9 @@ impl<'a> Layout<'a> {
| NonNullableUnwrapped(_) => true, | NonNullableUnwrapped(_) => true,
} }
} }
RecursivePointer => true,
Closure(_, closure_layout, _) => closure_layout.contains_refcounted(), Closure(_, closure_layout, _) => closure_layout.contains_refcounted(),
FunctionPointer(_, _) | RecursivePointer | Pointer(_) => false, FunctionPointer(_, _) | Pointer(_) => false,
} }
} }
} }

View file

@ -3,6 +3,7 @@
#![allow(clippy::large_enum_variant)] #![allow(clippy::large_enum_variant)]
pub mod borrow; pub mod borrow;
pub mod expand_rc;
pub mod inc_dec; pub mod inc_dec;
pub mod ir; pub mod ir;
pub mod layout; pub mod layout;

View file

@ -181,17 +181,17 @@ fn insert_jumps<'a>(
default_branch, default_branch,
ret_layout, ret_layout,
} => { } => {
let opt_default = insert_jumps(arena, default_branch, goal_id, needle); let opt_default = insert_jumps(arena, default_branch.1, goal_id, needle);
let mut did_change = false; let mut did_change = false;
let opt_branches = Vec::from_iter_in( let opt_branches = Vec::from_iter_in(
branches.iter().map(|(label, branch)| { branches.iter().map(|(label, info, branch)| {
match insert_jumps(arena, branch, goal_id, needle) { match insert_jumps(arena, branch, goal_id, needle) {
None => None, None => None,
Some(branch) => { Some(branch) => {
did_change = true; did_change = true;
Some((*label, branch.clone())) Some((*label, info.clone(), branch.clone()))
} }
} }
}), }),
@ -199,7 +199,10 @@ fn insert_jumps<'a>(
); );
if opt_default.is_some() || did_change { if opt_default.is_some() || did_change {
let default_branch = opt_default.unwrap_or(default_branch); let default_branch = (
default_branch.0.clone(),
opt_default.unwrap_or(default_branch.1),
);
let branches = if did_change { let branches = if did_change {
let new = Vec::from_iter_in( let new = Vec::from_iter_in(
@ -228,12 +231,8 @@ fn insert_jumps<'a>(
None None
} }
} }
Inc(symbol, inc, cont) => match insert_jumps(arena, cont, goal_id, needle) { Refcounting(modify, cont) => match insert_jumps(arena, cont, goal_id, needle) {
Some(cont) => Some(arena.alloc(Inc(*symbol, *inc, cont))), Some(cont) => Some(arena.alloc(Refcounting(*modify, cont))),
None => None,
},
Dec(symbol, cont) => match insert_jumps(arena, cont, goal_id, needle) {
Some(cont) => Some(arena.alloc(Dec(*symbol, cont))),
None => None, None => None,
}, },

View file

@ -884,14 +884,14 @@ mod test_mono {
joinpoint Test.8 Test.3: joinpoint Test.8 Test.3:
ret Test.3; ret Test.3;
in in
let Test.12 = 1i64; let Test.15 = 1i64;
let Test.13 = Index 0 Test.2; let Test.16 = Index 0 Test.2;
let Test.17 = lowlevel Eq Test.12 Test.13; let Test.17 = lowlevel Eq Test.15 Test.16;
if Test.17 then if Test.17 then
let Test.14 = Index 1 Test.2; let Test.12 = Index 1 Test.2;
let Test.15 = 3i64; let Test.13 = 3i64;
let Test.16 = lowlevel Eq Test.15 Test.14; let Test.14 = lowlevel Eq Test.13 Test.12;
if Test.16 then if Test.14 then
let Test.9 = 1i64; let Test.9 = 1i64;
jump Test.8 Test.9; jump Test.8 Test.9;
else else
@ -1961,28 +1961,24 @@ mod test_mono {
let Test.16 = S Test.19 Test.18; let Test.16 = S Test.19 Test.18;
let Test.14 = S Test.17 Test.16; let Test.14 = S Test.17 Test.16;
let Test.2 = S Test.15 Test.14; let Test.2 = S Test.15 Test.14;
let Test.7 = 0i64; let Test.11 = 0i64;
let Test.8 = Index 0 Test.2; let Test.12 = Index 0 Test.2;
let Test.13 = lowlevel Eq Test.7 Test.8; let Test.13 = lowlevel Eq Test.11 Test.12;
if Test.13 then if Test.13 then
let Test.9 = Index 1 Test.2; let Test.7 = Index 1 Test.2;
inc Test.9; let Test.8 = 0i64;
let Test.10 = 0i64; let Test.9 = Index 0 Test.7;
let Test.11 = Index 0 Test.9; let Test.10 = lowlevel Eq Test.8 Test.9;
dec Test.9; if Test.10 then
let Test.12 = lowlevel Eq Test.10 Test.11;
if Test.12 then
let Test.4 = Index 1 Test.2; let Test.4 = Index 1 Test.2;
inc Test.4;
dec Test.2;
let Test.3 = 1i64; let Test.3 = 1i64;
decref Test.2;
ret Test.3; ret Test.3;
else else
dec Test.2;
let Test.5 = 0i64; let Test.5 = 0i64;
dec Test.2;
ret Test.5; ret Test.5;
else else
dec Test.2;
let Test.6 = 0i64; let Test.6 = 0i64;
ret Test.6; ret Test.6;
"# "#

View file

@ -143,13 +143,15 @@ pub struct PlatformHeader<'a> {
pub after_provides: &'a [CommentOrNewline<'a>], pub after_provides: &'a [CommentOrNewline<'a>],
} }
/// e.g. fx.Effects
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub struct Effects<'a> { pub struct Effects<'a> {
pub spaces_before_effects_keyword: &'a [CommentOrNewline<'a>], pub spaces_before_effects_keyword: &'a [CommentOrNewline<'a>],
pub spaces_after_effects_keyword: &'a [CommentOrNewline<'a>], pub spaces_after_effects_keyword: &'a [CommentOrNewline<'a>],
pub spaces_after_type_name: &'a [CommentOrNewline<'a>], pub spaces_after_type_name: &'a [CommentOrNewline<'a>],
pub type_name: &'a str, pub effect_shortname: &'a str,
pub entries: Vec<'a, Loc<TypedIdent<'a>>>, pub effect_type_name: &'a str,
pub entries: &'a [Loc<TypedIdent<'a>>],
} }
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]

View file

@ -490,6 +490,11 @@ fn effects<'a>() -> impl Parser<'a, Effects<'a>> {
let (spaces_before_effects_keyword, state) = let (spaces_before_effects_keyword, state) =
skip_second!(space1(0), ascii_string("effects")).parse(arena, state)?; skip_second!(space1(0), ascii_string("effects")).parse(arena, state)?;
let (spaces_after_effects_keyword, state) = space1(0).parse(arena, state)?; let (spaces_after_effects_keyword, state) = space1(0).parse(arena, state)?;
// e.g. `fx.`
let (type_shortname, state) =
skip_second!(lowercase_ident(), ascii_char(b'.')).parse(arena, state)?;
let ((type_name, spaces_after_type_name), state) = let ((type_name, spaces_after_type_name), state) =
and!(uppercase_ident(), space1(0)).parse(arena, state)?; and!(uppercase_ident(), space1(0)).parse(arena, state)?;
let (entries, state) = collection!( let (entries, state) = collection!(
@ -506,8 +511,9 @@ fn effects<'a>() -> impl Parser<'a, Effects<'a>> {
spaces_before_effects_keyword, spaces_before_effects_keyword,
spaces_after_effects_keyword, spaces_after_effects_keyword,
spaces_after_type_name, spaces_after_type_name,
type_name, effect_shortname: type_shortname,
entries, effect_type_name: type_name,
entries: entries.into_bump_slice(),
}, },
state, state,
)) ))

View file

@ -2514,8 +2514,9 @@ mod test_parse {
}; };
let arena = Bump::new(); let arena = Bump::new();
let effects = Effects { let effects = Effects {
type_name: "Blah", effect_type_name: "Blah",
entries: Vec::new_in(&arena), effect_shortname: "fx",
entries: &[],
spaces_before_effects_keyword: &[], spaces_before_effects_keyword: &[],
spaces_after_effects_keyword: &[], spaces_after_effects_keyword: &[],
spaces_after_type_name: &[], spaces_after_type_name: &[],
@ -2541,7 +2542,7 @@ mod test_parse {
after_provides: &[], after_provides: &[],
}; };
let src = "platform rtfeldman/blah requires {} exposes [] packages {} imports [] provides [] effects Blah {}"; let src = "platform rtfeldman/blah requires {} exposes [] packages {} imports [] provides [] effects fx.Blah {}";
let actual = platform_header() let actual = platform_header()
.parse(&arena, State::new(src.as_bytes(), Attempting::Module)) .parse(&arena, State::new(src.as_bytes(), Attempting::Module))
.map(|tuple| tuple.0); .map(|tuple| tuple.0);
@ -2571,8 +2572,9 @@ mod test_parse {
let provide_entry = Located::new(5, 5, 15, 26, Exposed("mainForHost")); let provide_entry = Located::new(5, 5, 15, 26, Exposed("mainForHost"));
let provides = bumpalo::vec![in &arena; provide_entry]; let provides = bumpalo::vec![in &arena; provide_entry];
let effects = Effects { let effects = Effects {
type_name: "Effect", effect_type_name: "Effect",
entries: Vec::new_in(&arena), effect_shortname: "fx",
entries: &[],
spaces_before_effects_keyword: newlines, spaces_before_effects_keyword: newlines,
spaces_after_effects_keyword: &[], spaces_after_effects_keyword: &[],
spaces_after_type_name: &[], spaces_after_type_name: &[],
@ -2606,7 +2608,7 @@ mod test_parse {
packages { foo: "./foo" } packages { foo: "./foo" }
imports [] imports []
provides [ mainForHost ] provides [ mainForHost ]
effects Effect {} effects fx.Effect {}
"# "#
); );
let actual = platform_header() let actual = platform_header()

View file

@ -69,7 +69,7 @@ colored = "2"
pest = "2.1" pest = "2.1"
pest_derive = "2.1" pest_derive = "2.1"
ropey = "1.2.0" ropey = "1.2.0"
copypasta = "0.7.1"
[dependencies.bytemuck] [dependencies.bytemuck]
version = "1.4" version = "1.4"

View file

@ -1,6 +1,11 @@
## Getting started ## Getting started
Run the following from the roc folder: - Install the compiler, see [here](../BUILDING_FROM_SOURCE)
- On ubuntu run the following to make the clipboard work:
```
sudo apt install libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev
```
- Run the following from the roc folder:
``` ```
cargo run edit examples/hello-world/Hello.roc cargo run edit examples/hello-world/Hello.roc
@ -10,6 +15,8 @@ cargo run edit examples/hello-world/Hello.roc
If you encounter an error like `gfx_backend_vulkan ... Failed to detect any valid GPUs in the current config ...` make sure the correct graphics card drivers are installed. On ubuntu `sudo ubuntu-drivers autoinstall` can resolve the problem. If you encounter an error like `gfx_backend_vulkan ... Failed to detect any valid GPUs in the current config ...` make sure the correct graphics card drivers are installed. On ubuntu `sudo ubuntu-drivers autoinstall` can resolve the problem.
If you encounter problems with integrated graphics hardware, install `mesa-vulkan-drivers` and `vulkan-tools`.
## Inspiration ## Inspiration
We thank the following open source projects in particular for inspiring us when designing the Roc editor: We thank the following open source projects in particular for inspiring us when designing the Roc editor:

View file

@ -9,6 +9,34 @@ use snafu::{Backtrace, ErrorCompat, Snafu};
#[derive(Debug, Snafu)] #[derive(Debug, Snafu)]
#[snafu(visibility(pub))] #[snafu(visibility(pub))]
pub enum EdError { pub enum EdError {
#[snafu(display("ClipboardReadFailed: could not get clipboard contents: {}", err_msg))]
ClipboardReadFailed { err_msg: String },
#[snafu(display("ClipboardWriteFailed: could not set clipboard contents: {}", err_msg))]
ClipboardWriteFailed { err_msg: String },
#[snafu(display(
"ClipboardInitFailed: could not initialize ClipboardContext: {}.",
err_msg
))]
ClipboardInitFailed { err_msg: String },
#[snafu(display(
"FileOpenFailed: failed to open file with path {} with the following error: {}.",
path_str,
err_msg
))]
FileOpenFailed { path_str: String, err_msg: String },
#[snafu(display("InvalidSelection: {}.", err_msg))]
InvalidSelection {
err_msg: String,
backtrace: Backtrace,
},
#[snafu(display("MissingGlyphDims: glyph_dim_rect_opt was None for model. It needs to be set using the example_code_glyph_rect function."))]
MissingGlyphDims { backtrace: Backtrace },
#[snafu(display( #[snafu(display(
"OutOfBounds: index {} was out of bounds for {} with length {}.", "OutOfBounds: index {} was out of bounds for {} with length {}.",
index, index,
@ -21,19 +49,7 @@ pub enum EdError {
len: usize, len: usize,
backtrace: Backtrace, backtrace: Backtrace,
}, },
#[snafu(display("InvalidSelection: {}.", err_msg))]
InvalidSelection {
err_msg: String,
backtrace: Backtrace,
},
#[snafu(display("MissingGlyphDims: glyph_dim_rect_opt was None for model. It needs to be set using the example_code_glyph_rect function."))]
MissingGlyphDims { backtrace: Backtrace },
#[snafu(display(
"FileOpenFailed: failed to open file with path {} with the following error: {}.",
path_str,
err_msg
))]
FileOpenFailed { path_str: String, err_msg: String },
#[snafu(display("TextBufReadFailed: the file {} could be opened but we encountered the following error while trying to read it: {}.", path_str, err_msg))] #[snafu(display("TextBufReadFailed: the file {} could be opened but we encountered the following error while trying to read it: {}.", path_str, err_msg))]
TextBufReadFailed { path_str: String, err_msg: String }, TextBufReadFailed { path_str: String, err_msg: String },
} }
@ -88,3 +104,9 @@ fn contains_one_of(main_str: &str, contain_slice: &[&str]) -> bool {
false false
} }
impl From<EdError> for String {
fn from(ed_error: EdError) -> Self {
format!("{}", ed_error)
}
}

View file

@ -1,48 +1,52 @@
use crate::mvc::ed_model::EdModel; use crate::error::EdResult;
use crate::mvc::update::{ use crate::mvc::app_model::AppModel;
move_caret_down, move_caret_left, move_caret_right, move_caret_up, MoveCaretFun, use crate::mvc::app_update::{handle_copy, handle_paste, pass_keydown_to_focused};
}; use winit::event::VirtualKeyCode::*;
use winit::event::{ElementState, ModifiersState, VirtualKeyCode}; use winit::event::{ElementState, ModifiersState, VirtualKeyCode};
pub fn handle_keydown( pub fn handle_keydown(
elem_state: ElementState, elem_state: ElementState,
virtual_keycode: VirtualKeyCode, virtual_keycode: VirtualKeyCode,
modifiers: ModifiersState, modifiers: ModifiersState,
ed_model: &mut EdModel, app_model: &mut AppModel,
) { ) -> EdResult<()> {
use winit::event::VirtualKeyCode::*;
if let ElementState::Released = elem_state { if let ElementState::Released = elem_state {
return; return Ok(());
} }
match virtual_keycode { match virtual_keycode {
Left => handle_arrow(move_caret_left, &modifiers, ed_model), Left => pass_keydown_to_focused(&modifiers, virtual_keycode, app_model),
Up => handle_arrow(move_caret_up, &modifiers, ed_model), Up => pass_keydown_to_focused(&modifiers, virtual_keycode, app_model),
Right => handle_arrow(move_caret_right, &modifiers, ed_model), Right => pass_keydown_to_focused(&modifiers, virtual_keycode, app_model),
Down => handle_arrow(move_caret_down, &modifiers, ed_model), Down => pass_keydown_to_focused(&modifiers, virtual_keycode, app_model),
Copy => {
todo!("copy"); Copy => handle_copy(app_model)?,
} Paste => handle_paste(app_model)?,
Paste => {
todo!("paste");
}
Cut => { Cut => {
//handle_cut(app_model)?
todo!("cut"); todo!("cut");
} }
_ => {}
}
}
fn handle_arrow(move_caret_fun: MoveCaretFun, modifiers: &ModifiersState, ed_model: &mut EdModel) { C => {
let (new_caret_pos, new_selection_opt) = move_caret_fun( if modifiers.ctrl() {
ed_model.caret_pos, handle_copy(app_model)?
ed_model.selection_opt, }
modifiers.shift(), }
&ed_model.text_buf, V => {
); if modifiers.ctrl() {
ed_model.caret_pos = new_caret_pos; handle_paste(app_model)?
ed_model.selection_opt = new_selection_opt; }
}
X => {
if modifiers.ctrl() {
//handle_cut(app_model)?
todo!("cut");
}
}
_ => (),
}
Ok(())
} }
// pub fn handle_text_input( // pub fn handle_text_input(

View file

@ -25,8 +25,8 @@ use crate::graphics::style::CODE_FONT_SIZE;
use crate::graphics::style::CODE_TXT_XY; use crate::graphics::style::CODE_TXT_XY;
use crate::mvc::app_model::AppModel; use crate::mvc::app_model::AppModel;
use crate::mvc::ed_model::EdModel; use crate::mvc::ed_model::EdModel;
use crate::mvc::{ed_model, ed_view, update}; use crate::mvc::{app_update, ed_model, ed_view};
// use crate::resources::strings::NOTHING_OPENED; //use crate::resources::strings::NOTHING_OPENED;
use crate::vec_result::get_res; use crate::vec_result::get_res;
use bumpalo::Bump; use bumpalo::Bump;
use cgmath::Vector2; use cgmath::Vector2;
@ -162,7 +162,7 @@ fn run_event_loop(file_path_opt: Option<&Path>) -> Result<(), Box<dyn Error>> {
None None
}; };
let mut app_model = AppModel { ed_model_opt }; let mut app_model = AppModel::init(ed_model_opt);
let mut keyboard_modifiers = ModifiersState::empty(); let mut keyboard_modifiers = ModifiersState::empty();
@ -218,7 +218,7 @@ fn run_event_loop(file_path_opt: Option<&Path>) -> Result<(), Box<dyn Error>> {
event: event::WindowEvent::ReceivedCharacter(ch), event: event::WindowEvent::ReceivedCharacter(ch),
.. ..
} => { } => {
if let Err(e) = update::handle_new_char(&mut app_model, &ch) { if let Err(e) = app_update::handle_new_char(&ch, &mut app_model) {
print_err(&e) print_err(&e)
} }
} }
@ -230,12 +230,16 @@ fn run_event_loop(file_path_opt: Option<&Path>) -> Result<(), Box<dyn Error>> {
if let Some(virtual_keycode) = input.virtual_keycode { if let Some(virtual_keycode) = input.virtual_keycode {
if let Some(ref mut ed_model) = app_model.ed_model_opt { if let Some(ref mut ed_model) = app_model.ed_model_opt {
if ed_model.has_focus { if ed_model.has_focus {
keyboard_input::handle_keydown( let keydown_res = keyboard_input::handle_keydown(
input.state, input.state,
virtual_keycode, virtual_keycode,
keyboard_modifiers, keyboard_modifiers,
ed_model, &mut app_model,
); );
if let Err(e) = keydown_res {
print_err(&e)
}
} }
} }
} }

View file

@ -1,6 +1,101 @@
use super::ed_model::EdModel; use super::ed_model::EdModel;
use crate::error::EdError::{ClipboardInitFailed, ClipboardReadFailed, ClipboardWriteFailed};
use crate::error::{print_err, EdResult};
use copypasta::{ClipboardContext, ClipboardProvider};
use std::fmt;
#[derive(Debug)] #[derive(Debug)]
pub struct AppModel { pub struct AppModel {
pub ed_model_opt: Option<EdModel>, pub ed_model_opt: Option<EdModel>,
pub clipboard_opt: Option<Clipboard>,
}
impl AppModel {
pub fn init(ed_model_opt: Option<EdModel>) -> AppModel {
AppModel {
ed_model_opt,
clipboard_opt: AppModel::init_clipboard_opt(),
}
}
pub fn init_clipboard_opt() -> Option<Clipboard> {
let clipboard_res = Clipboard::init();
match clipboard_res {
Ok(clipboard) => Some(clipboard),
Err(e) => {
print_err(&e);
None
}
}
}
}
pub struct Clipboard {
context: ClipboardContext,
}
impl Clipboard {
pub fn init() -> EdResult<Clipboard> {
let context_res = ClipboardContext::new();
match context_res {
Ok(context) => Ok(Clipboard { context }),
Err(e) => Err(ClipboardInitFailed {
err_msg: e.to_string(),
}),
}
}
// clipboard crate needs this to be mutable
pub fn get_content(&mut self) -> EdResult<String> {
let content_res = self.context.get_contents();
match content_res {
Ok(content_str) => Ok(content_str),
Err(e) => Err(ClipboardReadFailed {
err_msg: e.to_string(),
}),
}
}
pub fn set_content(&mut self, copy_str: String) -> EdResult<()> {
let content_set_res = self.context.set_contents(copy_str);
match content_set_res {
Ok(_) => Ok(()),
Err(e) => Err(ClipboardWriteFailed {
err_msg: e.to_string(),
}),
}
}
}
pub fn set_clipboard_txt(clipboard_opt: &mut Option<Clipboard>, txt: &str) -> EdResult<()> {
if let Some(ref mut clipboard) = clipboard_opt {
clipboard.set_content(txt.to_owned())?;
} else {
return Err(ClipboardWriteFailed {
err_msg: "Clipboard was never initialized succesfully.".to_owned(),
});
}
Ok(())
}
pub fn get_clipboard_txt(clipboard_opt: &mut Option<Clipboard>) -> EdResult<String> {
if let Some(ref mut clipboard) = clipboard_opt {
clipboard.get_content()
} else {
Err(ClipboardReadFailed {
err_msg: "Clipboard was never initialized succesfully.".to_owned(),
})
}
}
impl fmt::Debug for Clipboard {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// showing the clipboard would require a mut ref which is not possible
f.debug_struct("Clipboard (can't show)").finish()
}
} }

View file

@ -0,0 +1,208 @@
use super::app_model;
use super::app_model::AppModel;
use super::ed_model::Position;
use super::ed_update;
use crate::error::EdResult;
use winit::event::{ModifiersState, VirtualKeyCode};
pub fn handle_copy(app_model: &mut AppModel) -> EdResult<()> {
if let Some(ref mut ed_model) = app_model.ed_model_opt {
if ed_model.has_focus {
let selected_str_opt = super::ed_model::get_selected_str(ed_model)?;
if let Some(selected_str) = selected_str_opt {
app_model::set_clipboard_txt(&mut app_model.clipboard_opt, selected_str)?;
}
}
}
Ok(())
}
pub fn handle_paste(app_model: &mut AppModel) -> EdResult<()> {
if let Some(ref mut ed_model) = app_model.ed_model_opt {
if ed_model.has_focus {
let clipboard_content = app_model::get_clipboard_txt(&mut app_model.clipboard_opt)?;
if !clipboard_content.is_empty() {
let mut rsplit_iter = clipboard_content.rsplit('\n');
// safe unwrap because we checked if empty
let last_line_nr_chars = rsplit_iter.next().unwrap().len();
let clipboard_nr_lines = rsplit_iter.count();
let old_caret_pos = ed_model.caret_pos;
if let Some(selection) = ed_model.selection_opt {
let start_caret_pos = selection.start_pos;
ed_model.text_buf.del_selection(selection)?;
ed_model.selection_opt = None;
ed_model
.text_buf
.insert_str(start_caret_pos, &clipboard_content)?;
if clipboard_nr_lines > 0 {
ed_model.caret_pos = Position {
line: start_caret_pos.line + clipboard_nr_lines,
column: last_line_nr_chars,
}
} else {
ed_model.caret_pos = Position {
line: start_caret_pos.line,
column: start_caret_pos.column + last_line_nr_chars,
}
}
} else {
ed_model
.text_buf
.insert_str(old_caret_pos, &clipboard_content)?;
if clipboard_nr_lines > 0 {
ed_model.caret_pos = Position {
line: old_caret_pos.line + clipboard_nr_lines,
column: last_line_nr_chars,
}
} else {
ed_model.caret_pos = Position {
line: old_caret_pos.line,
column: old_caret_pos.column + last_line_nr_chars,
}
}
}
}
}
}
Ok(())
}
pub fn pass_keydown_to_focused(
modifiers: &ModifiersState,
virtual_keycode: VirtualKeyCode,
app_model: &mut AppModel,
) {
if let Some(ref mut ed_model) = app_model.ed_model_opt {
if ed_model.has_focus {
ed_update::handle_key_down(modifiers, virtual_keycode, ed_model);
}
}
}
pub fn handle_new_char(received_char: &char, app_model: &mut AppModel) -> EdResult<()> {
if let Some(ref mut ed_model) = app_model.ed_model_opt {
if ed_model.has_focus {
ed_update::handle_new_char(received_char, ed_model)?;
}
}
Ok(())
}
#[cfg(test)]
pub mod test_app_update {
use crate::mvc::app_model;
use crate::mvc::app_model::{AppModel, Clipboard};
use crate::mvc::app_update::{handle_copy, handle_paste};
use crate::mvc::ed_model::{EdModel, Position, RawSelection};
use crate::mvc::ed_update::test_ed_update::gen_caret_text_buf;
use crate::selection::test_selection::{all_lines_vec, convert_selection_to_dsl};
use crate::text_buffer::TextBuffer;
pub fn mock_app_model(
text_buf: TextBuffer,
caret_pos: Position,
selection_opt: Option<RawSelection>,
clipboard_opt: Option<Clipboard>,
) -> AppModel {
AppModel {
ed_model_opt: Some(EdModel {
text_buf,
caret_pos,
selection_opt,
glyph_dim_rect_opt: None,
has_focus: true,
}),
clipboard_opt,
}
}
fn assert_copy(
pre_lines_str: &[&str],
expected_clipboard_content: &str,
clipboard_opt: Option<Clipboard>,
) -> Result<Option<Clipboard>, String> {
let (caret_pos, selection_opt, pre_text_buf) = gen_caret_text_buf(pre_lines_str)?;
let mut app_model = mock_app_model(pre_text_buf, caret_pos, selection_opt, clipboard_opt);
handle_copy(&mut app_model)?;
let clipboard_content = app_model::get_clipboard_txt(&mut app_model.clipboard_opt)?;
assert_eq!(clipboard_content, expected_clipboard_content);
Ok(app_model.clipboard_opt)
}
fn assert_paste(
pre_lines_str: &[&str],
clipboard_content: &str,
expected_post_lines_str: &[&str],
clipboard_opt: Option<Clipboard>,
) -> Result<Option<Clipboard>, String> {
let (caret_pos, selection_opt, pre_text_buf) = gen_caret_text_buf(pre_lines_str)?;
let mut app_model = mock_app_model(pre_text_buf, caret_pos, selection_opt, clipboard_opt);
app_model::set_clipboard_txt(&mut app_model.clipboard_opt, clipboard_content)?;
handle_paste(&mut app_model)?;
let ed_model = app_model.ed_model_opt.unwrap();
let mut text_buf_lines = all_lines_vec(&ed_model.text_buf);
let post_lines_str = convert_selection_to_dsl(
ed_model.selection_opt,
ed_model.caret_pos,
&mut text_buf_lines,
)?;
assert_eq!(post_lines_str, expected_post_lines_str);
Ok(app_model.clipboard_opt)
}
#[test]
#[ignore] // ignored because of clipboard problems on ci
fn copy_paste() -> Result<(), String> {
// can only init clipboard once
let mut clipboard_opt = AppModel::init_clipboard_opt();
clipboard_opt = assert_copy(&["[a]|"], "a", clipboard_opt)?;
clipboard_opt = assert_copy(&["|[b]"], "b", clipboard_opt)?;
clipboard_opt = assert_copy(&["a[ ]|"], " ", clipboard_opt)?;
clipboard_opt = assert_copy(&["[ ]|b"], " ", clipboard_opt)?;
clipboard_opt = assert_copy(&["a\n", "[b\n", "]|"], "b\n", clipboard_opt)?;
clipboard_opt = assert_copy(&["[a\n", " b\n", "]|"], "a\n b\n", clipboard_opt)?;
clipboard_opt = assert_copy(
&["abc\n", "d[ef\n", "ghi]|\n", "jkl"],
"ef\nghi",
clipboard_opt,
)?;
clipboard_opt = assert_paste(&["|"], "", &["|"], clipboard_opt)?;
clipboard_opt = assert_paste(&["|"], "a", &["a|"], clipboard_opt)?;
clipboard_opt = assert_paste(&["a|"], "b", &["ab|"], clipboard_opt)?;
clipboard_opt = assert_paste(&["|a"], "b", &["b|a"], clipboard_opt)?;
clipboard_opt = assert_paste(&["[a]|"], "c", &["c|"], clipboard_opt)?;
clipboard_opt = assert_paste(&["[ab]|"], "d", &["d|"], clipboard_opt)?;
clipboard_opt = assert_paste(&["a[b]|c"], "e", &["ae|c"], clipboard_opt)?;
clipboard_opt = assert_paste(&["a\n", "[b\n", "]|"], "f", &["a\n", "f|"], clipboard_opt)?;
assert_paste(
&["abc\n", "d[ef\n", "ghi]|\n", "jkl"],
"ef\nghi",
&["abc\n", "def\n", "ghi|\n", "jkl"],
clipboard_opt,
)?;
Ok(())
}
}

View file

@ -25,6 +25,16 @@ pub fn init_model(file_path: &Path) -> EdResult<EdModel> {
}) })
} }
pub fn get_selected_str(ed_model: &EdModel) -> EdResult<Option<&str>> {
if let Some(curr_selection) = ed_model.selection_opt {
let selected_str = ed_model.text_buf.get_selection(curr_selection)?;
Ok(Some(selected_str))
} else {
Ok(None)
}
}
#[derive(Debug, Copy, Clone)] #[derive(Debug, Copy, Clone)]
pub struct Position { pub struct Position {
pub line: usize, pub line: usize,

View file

@ -1,10 +1,11 @@
use super::app_model::AppModel;
use super::ed_model::EdModel; use super::ed_model::EdModel;
use super::ed_model::{Position, RawSelection}; use super::ed_model::{Position, RawSelection};
use crate::error::EdResult; use crate::error::EdResult;
use crate::text_buffer::TextBuffer; use crate::text_buffer::TextBuffer;
use crate::util::is_newline; use crate::util::is_newline;
use std::cmp::{max, min}; use std::cmp::{max, min};
use winit::event::VirtualKeyCode::*;
use winit::event::{ModifiersState, VirtualKeyCode};
pub type MoveCaretFun = pub type MoveCaretFun =
fn(Position, Option<RawSelection>, bool, &TextBuffer) -> (Position, Option<RawSelection>); fn(Position, Option<RawSelection>, bool, &TextBuffer) -> (Position, Option<RawSelection>);
@ -299,6 +300,17 @@ pub fn move_caret_down(
(new_caret_pos, new_selection_opt) (new_caret_pos, new_selection_opt)
} }
fn handle_arrow(move_caret_fun: MoveCaretFun, modifiers: &ModifiersState, ed_model: &mut EdModel) {
let (new_caret_pos, new_selection_opt) = move_caret_fun(
ed_model.caret_pos,
ed_model.selection_opt,
modifiers.shift(),
&ed_model.text_buf,
);
ed_model.caret_pos = new_caret_pos;
ed_model.selection_opt = new_selection_opt;
}
fn del_selection(selection: RawSelection, ed_model: &mut EdModel) -> EdResult<()> { fn del_selection(selection: RawSelection, ed_model: &mut EdModel) -> EdResult<()> {
ed_model.text_buf.del_selection(selection)?; ed_model.text_buf.del_selection(selection)?;
ed_model.caret_pos = selection.start_pos; ed_model.caret_pos = selection.start_pos;
@ -306,79 +318,99 @@ fn del_selection(selection: RawSelection, ed_model: &mut EdModel) -> EdResult<()
Ok(()) Ok(())
} }
pub fn handle_new_char(app_model: &mut AppModel, received_char: &char) -> EdResult<()> { pub fn handle_new_char(received_char: &char, ed_model: &mut EdModel) -> EdResult<()> {
if let Some(ref mut ed_model) = app_model.ed_model_opt { let old_caret_pos = ed_model.caret_pos;
let old_caret_pos = ed_model.caret_pos;
match received_char { match received_char {
'\u{8}' | '\u{7f}' => { '\u{8}' | '\u{7f}' => {
// On Linux, '\u{8}' is backspace, // On Linux, '\u{8}' is backspace,
// on macOS '\u{7f}'. // on macOS '\u{7f}'.
if let Some(selection) = ed_model.selection_opt { if let Some(selection) = ed_model.selection_opt {
del_selection(selection, ed_model)?; del_selection(selection, ed_model)?;
} else { } else {
ed_model.caret_pos = ed_model.caret_pos =
move_caret_left(old_caret_pos, None, false, &ed_model.text_buf).0; move_caret_left(old_caret_pos, None, false, &ed_model.text_buf).0;
ed_model.text_buf.pop_char(old_caret_pos); ed_model.text_buf.pop_char(old_caret_pos);
}
} }
ch if is_newline(ch) => {
if let Some(selection) = ed_model.selection_opt {
del_selection(selection, ed_model)?;
ed_model.text_buf.insert_char(ed_model.caret_pos, &'\n')?;
} else {
ed_model.text_buf.insert_char(old_caret_pos, &'\n')?;
ed_model.caret_pos = Position { ed_model.selection_opt = None;
line: old_caret_pos.line + 1,
column: 0,
};
}
}
'\u{e000}'..='\u{f8ff}' | '\u{f0000}'..='\u{ffffd}' | '\u{100000}'..='\u{10fffd}' => {
// These are private use characters; ignore them.
// See http://www.unicode.org/faq/private_use.html
}
_ => {
if let Some(selection) = ed_model.selection_opt {
del_selection(selection, ed_model)?;
ed_model
.text_buf
.insert_char(ed_model.caret_pos, received_char)?;
ed_model.caret_pos =
move_caret_right(ed_model.caret_pos, None, false, &ed_model.text_buf).0;
} else {
ed_model
.text_buf
.insert_char(old_caret_pos, received_char)?;
ed_model.caret_pos = Position {
line: old_caret_pos.line,
column: old_caret_pos.column + 1,
};
}
}
} }
ch if is_newline(ch) => {
if let Some(selection) = ed_model.selection_opt {
del_selection(selection, ed_model)?;
ed_model.text_buf.insert_char(ed_model.caret_pos, &'\n')?;
} else {
ed_model.text_buf.insert_char(old_caret_pos, &'\n')?;
ed_model.selection_opt = None; ed_model.caret_pos = Position {
line: old_caret_pos.line + 1,
column: 0,
};
}
ed_model.selection_opt = None;
}
'\u{3}'
| '\u{16}'
| '\u{30}'
| '\u{e000}'..='\u{f8ff}'
| '\u{f0000}'..='\u{ffffd}'
| '\u{100000}'..='\u{10fffd}' => {
// chars that can be ignored
}
_ => {
if let Some(selection) = ed_model.selection_opt {
del_selection(selection, ed_model)?;
ed_model
.text_buf
.insert_char(ed_model.caret_pos, received_char)?;
ed_model.caret_pos =
move_caret_right(ed_model.caret_pos, None, false, &ed_model.text_buf).0;
} else {
ed_model
.text_buf
.insert_char(old_caret_pos, received_char)?;
ed_model.caret_pos = Position {
line: old_caret_pos.line,
column: old_caret_pos.column + 1,
};
}
ed_model.selection_opt = None;
}
} }
Ok(()) Ok(())
} }
pub fn handle_key_down(
modifiers: &ModifiersState,
virtual_keycode: VirtualKeyCode,
ed_model: &mut EdModel,
) {
match virtual_keycode {
Left => handle_arrow(move_caret_left, modifiers, ed_model),
Up => handle_arrow(move_caret_up, modifiers, ed_model),
Right => handle_arrow(move_caret_right, modifiers, ed_model),
Down => handle_arrow(move_caret_down, modifiers, ed_model),
_ => {}
}
}
#[cfg(test)] #[cfg(test)]
mod test_update { pub mod test_ed_update {
use crate::mvc::app_model::AppModel; use crate::mvc::app_update::test_app_update::mock_app_model;
use crate::mvc::ed_model::{EdModel, Position, RawSelection}; use crate::mvc::ed_model::{Position, RawSelection};
use crate::mvc::update::handle_new_char; use crate::mvc::ed_update::handle_new_char;
use crate::selection::test_selection::{ use crate::selection::test_selection::{
all_lines_vec, convert_dsl_to_selection, convert_selection_to_dsl, text_buffer_from_dsl_str, all_lines_vec, convert_dsl_to_selection, convert_selection_to_dsl, text_buffer_from_dsl_str,
}; };
use crate::text_buffer::TextBuffer; use crate::text_buffer::TextBuffer;
fn gen_caret_text_buf( pub fn gen_caret_text_buf(
lines: &[&str], lines: &[&str],
) -> Result<(Position, Option<RawSelection>, TextBuffer), String> { ) -> Result<(Position, Option<RawSelection>, TextBuffer), String> {
let lines_string_slice: Vec<String> = lines.iter().map(|l| l.to_string()).collect(); let lines_string_slice: Vec<String> = lines.iter().map(|l| l.to_string()).collect();
@ -388,22 +420,6 @@ mod test_update {
Ok((caret_pos, selection_opt, text_buf)) Ok((caret_pos, selection_opt, text_buf))
} }
fn mock_app_model(
text_buf: TextBuffer,
caret_pos: Position,
selection_opt: Option<RawSelection>,
) -> AppModel {
AppModel {
ed_model_opt: Some(EdModel {
text_buf,
caret_pos,
selection_opt,
glyph_dim_rect_opt: None,
has_focus: true,
}),
}
}
fn assert_insert( fn assert_insert(
pre_lines_str: &[&str], pre_lines_str: &[&str],
expected_post_lines_str: &[&str], expected_post_lines_str: &[&str],
@ -411,24 +427,21 @@ mod test_update {
) -> Result<(), String> { ) -> Result<(), String> {
let (caret_pos, selection_opt, pre_text_buf) = gen_caret_text_buf(pre_lines_str)?; let (caret_pos, selection_opt, pre_text_buf) = gen_caret_text_buf(pre_lines_str)?;
let mut app_model = mock_app_model(pre_text_buf, caret_pos, selection_opt); let app_model = mock_app_model(pre_text_buf, caret_pos, selection_opt, None);
let mut ed_model = app_model.ed_model_opt.unwrap();
if let Err(e) = handle_new_char(&mut app_model, &new_char) { if let Err(e) = handle_new_char(&new_char, &mut ed_model) {
return Err(e.to_string()); return Err(e.to_string());
} }
if let Some(ed_model) = app_model.ed_model_opt { let mut actual_lines = all_lines_vec(&ed_model.text_buf);
let mut actual_lines = all_lines_vec(&ed_model.text_buf); let dsl_slice = convert_selection_to_dsl(
let dsl_slice = convert_selection_to_dsl( ed_model.selection_opt,
ed_model.selection_opt, ed_model.caret_pos,
ed_model.caret_pos, &mut actual_lines,
&mut actual_lines, )
) .unwrap();
.unwrap(); assert_eq!(dsl_slice, expected_post_lines_str);
assert_eq!(dsl_slice, expected_post_lines_str);
} else {
panic!("Mock AppModel did not have an EdModel.");
}
Ok(()) Ok(())
} }

View file

@ -1,4 +1,5 @@
pub mod app_model; pub mod app_model;
pub mod app_update;
pub mod ed_model; pub mod ed_model;
pub mod ed_update;
pub mod ed_view; pub mod ed_view;
pub mod update;

View file

@ -126,7 +126,7 @@ pub fn create_selection_rects<'a>(
pub mod test_selection { pub mod test_selection {
use crate::error::{EdResult, OutOfBounds}; use crate::error::{EdResult, OutOfBounds};
use crate::mvc::ed_model::{Position, RawSelection}; use crate::mvc::ed_model::{Position, RawSelection};
use crate::mvc::update::{ use crate::mvc::ed_update::{
move_caret_down, move_caret_left, move_caret_right, move_caret_up, MoveCaretFun, move_caret_down, move_caret_left, move_caret_right, move_caret_up, MoveCaretFun,
}; };
use crate::text_buffer::TextBuffer; use crate::text_buffer::TextBuffer;

View file

@ -22,18 +22,15 @@ pub struct TextBuffer {
impl TextBuffer { impl TextBuffer {
pub fn insert_char(&mut self, caret_pos: Position, new_char: &char) -> EdResult<()> { pub fn insert_char(&mut self, caret_pos: Position, new_char: &char) -> EdResult<()> {
self.insert_str(caret_pos, &new_char.to_string())
}
pub fn insert_str(&mut self, caret_pos: Position, new_str: &str) -> EdResult<()> {
let char_indx = self.pos_to_char_indx(caret_pos); let char_indx = self.pos_to_char_indx(caret_pos);
ensure!( self.check_bounds(char_indx)?;
char_indx <= self.text_rope.len_chars(),
OutOfBounds {
index: char_indx,
collection_name: "Rope",
len: self.text_rope.len_chars()
}
);
self.text_rope.insert(char_indx, &new_char.to_string()); self.text_rope.insert(char_indx, new_str);
Ok(()) Ok(())
} }
@ -49,23 +46,47 @@ impl TextBuffer {
pub fn del_selection(&mut self, raw_sel: RawSelection) -> EdResult<()> { pub fn del_selection(&mut self, raw_sel: RawSelection) -> EdResult<()> {
let (start_char_indx, end_char_indx) = self.sel_to_tup(raw_sel)?; let (start_char_indx, end_char_indx) = self.sel_to_tup(raw_sel)?;
ensure!( self.check_bounds(end_char_indx)?;
end_char_indx <= self.text_rope.len_chars(),
OutOfBounds {
index: end_char_indx,
collection_name: "Rope",
len: self.text_rope.len_chars()
}
);
self.text_rope.remove(start_char_indx..end_char_indx); self.text_rope.remove(start_char_indx..end_char_indx);
Ok(()) Ok(())
} }
pub fn get_selection(&self, raw_sel: RawSelection) -> EdResult<&str> {
let (start_char_indx, end_char_indx) = self.sel_to_tup(raw_sel)?;
self.check_bounds(end_char_indx)?;
let rope_slice = self.text_rope.slice(start_char_indx..end_char_indx);
if let Some(line_str_ref) = rope_slice.as_str() {
Ok(line_str_ref)
} else {
// happens very rarely
let line_str = rope_slice.chunks().collect::<String>();
let arena_str_ref = self.mem_arena.alloc(line_str);
Ok(arena_str_ref)
}
}
fn check_bounds(&self, char_indx: usize) -> EdResult<()> {
ensure!(
char_indx <= self.text_rope.len_chars(),
OutOfBounds {
index: char_indx,
collection_name: "Rope",
len: self.text_rope.len_chars()
}
);
Ok(())
}
pub fn line(&self, line_nr: usize) -> Option<&str> { pub fn line(&self, line_nr: usize) -> Option<&str> {
if line_nr < self.text_rope.len_lines() { if line_nr < self.nr_of_lines() {
let rope_slice = self.text_rope.line(line_nr); let rope_slice = self.text_rope.line(line_nr);
if let Some(line_str_ref) = rope_slice.as_str() { if let Some(line_str_ref) = rope_slice.as_str() {
Some(line_str_ref) Some(line_str_ref)
} else { } else {
@ -80,11 +101,7 @@ impl TextBuffer {
} }
pub fn line_len(&self, line_nr: usize) -> Option<usize> { pub fn line_len(&self, line_nr: usize) -> Option<usize> {
if line_nr < self.text_rope.len_lines() { self.line(line_nr).map(|line| line.len())
Some(self.text_rope.line(line_nr).len_chars())
} else {
None
}
} }
pub fn line_len_res(&self, line_nr: usize) -> EdResult<usize> { pub fn line_len_res(&self, line_nr: usize) -> EdResult<usize> {
@ -125,7 +142,6 @@ impl TextBuffer {
} }
pub fn from_path(path: &Path) -> EdResult<TextBuffer> { pub fn from_path(path: &Path) -> EdResult<TextBuffer> {
// TODO benchmark different file reading methods, see #886
let text_rope = rope_from_path(path)?; let text_rope = rope_from_path(path)?;
let path_str = path_to_string(path); let path_str = path_to_string(path);
let mem_arena = Bump::new(); let mem_arena = Bump::new();

View file

@ -5,6 +5,7 @@ app "nqueens"
main : Task.Task {} [] main : Task.Task {} []
main = main =
# Task.after Task.getInt \n ->
queens 6 queens 6
|> Str.fromInt |> Str.fromInt
|> Task.putLine |> Task.putLine
@ -18,10 +19,10 @@ length : ConsList a -> I64
length = \xs -> lengthHelp xs 0 length = \xs -> lengthHelp xs 0
lengthHelp : ConsList a, I64 -> I64 lengthHelp : ConsList a, I64 -> I64
lengthHelp = \xs, acc -> lengthHelp = \foobar, acc ->
when xs is when foobar is
Cons _ lrest -> lengthHelp lrest (1 + acc)
Nil -> acc Nil -> acc
Cons _ rest -> lengthHelp rest (1 + acc)
safe : I64, I64, ConsList I64 -> Bool safe : I64, I64, ConsList I64 -> Bool
safe = \queen, diagonal, xs -> safe = \queen, diagonal, xs ->
@ -41,8 +42,8 @@ appendSafe = \k, soln, solns ->
else else
appendSafe (k - 1) soln solns appendSafe (k - 1) soln solns
extend = \n, acc, solns -> extend = \n, acc, solutions ->
when solns is when solutions is
Nil -> acc Nil -> acc
Cons soln rest -> extend n (appendSafe n soln acc) rest Cons soln rest -> extend n (appendSafe n soln acc) rest

View file

@ -4,9 +4,10 @@ platform folkertdev/foo
packages {} packages {}
imports [ Task ] imports [ Task ]
provides [ mainForHost ] provides [ mainForHost ]
effects Effect effects fx.Effect
{ {
putLine : Str -> Effect {} putLine : Str -> Effect {},
getInt : Effect { value: I64, errorCode: [ A, B ], isError: Bool }
} }
mainForHost : Task.Task {} [] as Fx mainForHost : Task.Task {} [] as Fx

View file

@ -1,6 +1,6 @@
interface Task interface Task
exposes [ Task, succeed, fail, after, map, putLine ] exposes [ Task, succeed, fail, after, map, putLine, getInt ]
imports [ Effect ] imports [ fx.Effect ]
Task ok err : Effect.Effect (Result ok err) Task ok err : Effect.Effect (Result ok err)
@ -15,7 +15,6 @@ fail : err -> Task * err
fail = \val -> fail = \val ->
Effect.always (Err val) Effect.always (Err val)
after : Task a err, (a -> Task b err) -> Task b err after : Task a err, (a -> Task b err) -> Task b err
after = \effect, transform -> after = \effect, transform ->
Effect.after effect \result -> Effect.after effect \result ->
@ -32,3 +31,16 @@ map = \effect, transform ->
putLine : Str -> Task {} * putLine : Str -> Task {} *
putLine = \line -> Effect.map (Effect.putLine line) (\_ -> Ok {}) putLine = \line -> Effect.map (Effect.putLine line) (\_ -> Ok {})
getInt : Task I64 []
getInt =
Effect.after Effect.getInt \{ isError, value, errorCode } ->
when isError is
True ->
when errorCode is
# A -> Task.fail InvalidCharacter
# B -> Task.fail IOError
_ -> Task.succeed -1
False ->
Task.succeed value

View file

@ -4,6 +4,7 @@ const RocStr = str.RocStr;
const testing = std.testing; const testing = std.testing;
const expectEqual = testing.expectEqual; const expectEqual = testing.expectEqual;
const expect = testing.expect; const expect = testing.expect;
const maxInt = std.math.maxInt;
const mem = std.mem; const mem = std.mem;
const Allocator = mem.Allocator; const Allocator = mem.Allocator;
@ -96,3 +97,39 @@ pub export fn roc_fx_putLine(rocPath: str.RocStr) i64 {
return 0; return 0;
} }
const GetInt = extern struct {
value: i64,
error_code: u8,
is_error: bool,
};
pub export fn roc_fx_getInt() GetInt {
if (roc_fx_getInt_help()) |value| {
const get_int = GetInt{ .is_error = false, .value = value, .error_code = 0 };
return get_int;
} else |err| switch (err) {
error.InvalidCharacter => {
return GetInt{ .is_error = true, .value = 0, .error_code = 0 };
},
else => {
return GetInt{ .is_error = true, .value = 0, .error_code = 1 };
},
}
return 0;
}
fn roc_fx_getInt_help() !i64 {
const stdin = std.io.getStdIn().inStream();
var buf: [40]u8 = undefined;
const line: []u8 = (try stdin.readUntilDelimiterOrEof(&buf, '\n')) orelse "";
return std.fmt.parseInt(i64, line, 10);
}
fn readLine() []u8 {
const stdin = std.io.getStdIn().reader();
return (stdin.readUntilDelimiterOrEof(&line_buf, '\n') catch unreachable) orelse "";
}

View file

@ -4,7 +4,7 @@ platform folkertdev/foo
packages {} packages {}
imports [Task] imports [Task]
provides [ mainForHost ] provides [ mainForHost ]
effects Effect effects fx.Effect
{ {
putChar : I64 -> Effect {}, putChar : I64 -> Effect {},
putLine : Str -> Effect {}, putLine : Str -> Effect {},

View file

@ -1,23 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
[[package]]
name = "host"
version = "0.1.0"
dependencies = [
"roc_std 0.1.0",
]
[[package]]
name = "libc"
version = "0.2.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "roc_std"
version = "0.1.0"
dependencies = [
"libc 0.2.79 (registry+https://github.com/rust-lang/crates.io-index)",
]
[metadata]
"checksum libc 0.2.79 (registry+https://github.com/rust-lang/crates.io-index)" = "2448f6066e80e3bfc792e9c98bf705b4b0fc6e8ef5b43e5889aff0eaa9c58743"

View file

@ -1,13 +0,0 @@
[package]
name = "host"
version = "0.1.0"
authors = ["Richard Feldman <oss@rtfeldman.com>"]
edition = "2018"
[lib]
crate-type = ["staticlib"]
[dependencies]
roc_std = { path = "../../../roc_std" }
[workspace]

View file

@ -4,7 +4,7 @@ platform examples/hello-world
packages {} packages {}
imports [] imports []
provides [ mainForHost ] provides [ mainForHost ]
effects Effect {} effects fx.Effect {}
mainForHost : Str mainForHost : Str
mainForHost = main mainForHost = main

View file

@ -1,8 +0,0 @@
# Rebuilding the host from source
Run `build.sh` to manually rebuild this platform's host.
Note that the compiler currently has its own logic for rebuilding these hosts
(in `link.rs`). It's hardcoded for now, but the long-term goal is that
hosts will be precompiled by platform authors and distributed in packages,
at which point only package authors will need to think about rebuilding hosts.

View file

@ -1,12 +0,0 @@
#!/bin/bash
# compile c_host.o and rust_host.o
clang -c host.c -o c_host.o
rustc host.rs -o rust_host.o
# link them together into host.o
ld -r c_host.o rust_host.o -o host.o
# clean up
rm -f c_host.o
rm -f rust_host.o

View file

@ -1,7 +0,0 @@
#include <stdio.h>
extern int rust_main();
int main() {
return rust_main();
}

View file

@ -0,0 +1,49 @@
const std = @import("std");
const str = @import("str.zig");
const RocStr = str.RocStr;
const testing = std.testing;
const expectEqual = testing.expectEqual;
const expect = testing.expect;
const mem = std.mem;
const Allocator = mem.Allocator;
extern fn roc__mainForHost_1_exposed(*RocCallResult) void;
const RocCallResult = extern struct { flag: usize, content: RocStr };
const Unit = extern struct {};
pub export fn main() i32 {
const stdout = std.io.getStdOut().writer();
const stderr = std.io.getStdErr().writer();
// make space for the result
var callresult = RocCallResult{ .flag = 0, .content = RocStr.empty() };
// start time
var ts1: std.os.timespec = undefined;
std.os.clock_gettime(std.os.CLOCK_REALTIME, &ts1) catch unreachable;
// actually call roc to populate the callresult
roc__mainForHost_1_exposed(&callresult);
// stdout the result
stdout.print("{}\n", .{callresult.content.asSlice()}) catch unreachable;
callresult.content.deinit(std.heap.c_allocator);
// end time
var ts2: std.os.timespec = undefined;
std.os.clock_gettime(std.os.CLOCK_REALTIME, &ts2) catch unreachable;
const delta = to_seconds(ts2) - to_seconds(ts1);
stderr.print("runtime: {d:.3}ms\n", .{delta * 1000}) catch unreachable;
return 0;
}
fn to_seconds(tms: std.os.timespec) f64 {
return @intToFloat(f64, tms.tv_sec) + (@intToFloat(f64, tms.tv_nsec) / 1_000_000_000.0);
}

View file

@ -1,28 +0,0 @@
use roc_std::RocCallResult;
use roc_std::RocStr;
use std::str;
extern "C" {
#[link_name = "roc__mainForHost_1_exposed"]
fn say_hello(output: &mut RocCallResult<RocStr>) -> ();
}
#[no_mangle]
pub fn rust_main() -> isize {
let answer = unsafe {
use std::mem::MaybeUninit;
let mut output: MaybeUninit<RocCallResult<RocStr>> = MaybeUninit::uninit();
say_hello(&mut *output.as_mut_ptr());
match output.assume_init().into() {
Ok(value) => value,
Err(msg) => panic!("roc failed with message {}", msg),
}
};
println!("Roc says: {}", str::from_utf8(answer.as_slice()).unwrap());
// Exit code
0
}

View file

@ -0,0 +1,818 @@
const std = @import("std");
const mem = std.mem;
const always_inline = std.builtin.CallOptions.Modifier.always_inline;
const Allocator = mem.Allocator;
const unicode = std.unicode;
const testing = std.testing;
const expectEqual = testing.expectEqual;
const expect = testing.expect;
const InPlace = packed enum(u8) {
InPlace,
Clone,
};
pub const RocStr = extern struct {
str_bytes: ?[*]u8,
str_len: usize,
pub inline fn empty() RocStr {
return RocStr{
.str_len = 0,
.str_bytes = null,
};
}
// This clones the pointed-to bytes if they won't fit in a
// small string, and returns a (pointer, len) tuple which points to them.
pub fn init(allocator: *Allocator, bytes_ptr: [*]const u8, length: usize) RocStr {
const roc_str_size = @sizeOf(RocStr);
if (length < roc_str_size) {
var ret_small_str = RocStr.empty();
const target_ptr = @ptrToInt(&ret_small_str);
var index: u8 = 0;
// TODO isn't there a way to bulk-zero data in Zig?
// Zero out the data, just to be safe
while (index < roc_str_size) {
var offset_ptr = @intToPtr(*u8, target_ptr + index);
offset_ptr.* = 0;
index += 1;
}
// TODO rewrite this into a for loop
index = 0;
while (index < length) {
var offset_ptr = @intToPtr(*u8, target_ptr + index);
offset_ptr.* = bytes_ptr[index];
index += 1;
}
// set the final byte to be the length
const final_byte_ptr = @intToPtr(*u8, target_ptr + roc_str_size - 1);
final_byte_ptr.* = @truncate(u8, length) ^ 0b10000000;
return ret_small_str;
} else {
var result = RocStr.initBig(allocator, u64, InPlace.Clone, length);
@memcpy(@ptrCast([*]u8, result.str_bytes), bytes_ptr, length);
return result;
}
}
pub fn initBig(allocator: *Allocator, comptime T: type, in_place: InPlace, number_of_chars: u64) RocStr {
const length = @sizeOf(T) + number_of_chars;
var new_bytes: []T = allocator.alloc(T, length) catch unreachable;
if (in_place == InPlace.InPlace) {
new_bytes[0] = @intCast(T, number_of_chars);
} else {
new_bytes[0] = std.math.minInt(T);
}
var first_element = @ptrCast([*]align(@alignOf(T)) u8, new_bytes);
first_element += @sizeOf(usize);
return RocStr{
.str_bytes = first_element,
.str_len = number_of_chars,
};
}
pub fn deinit(self: RocStr, allocator: *Allocator) void {
if (!self.isSmallStr() and !self.isEmpty()) {
const str_bytes_ptr: [*]u8 = self.str_bytes orelse unreachable;
// must include refcount
const str_bytes: []u8 = (str_bytes_ptr - 8)[0 .. self.str_len + 8];
allocator.free(str_bytes);
}
}
// This takes ownership of the pointed-to bytes if they won't fit in a
// small string, and returns a (pointer, len) tuple which points to them.
pub fn withCapacity(length: usize) RocStr {
const roc_str_size = @sizeOf(RocStr);
if (length < roc_str_size) {
return RocStr.empty();
} else {
var new_bytes: []T = allocator.alloc(u8, length) catch unreachable;
var new_bytes_ptr: [*]u8 = @ptrCast([*]u8, &new_bytes);
return RocStr{
.str_bytes = new_bytes_ptr,
.str_len = length,
};
}
}
pub fn eq(self: RocStr, other: RocStr) bool {
const self_bytes_ptr: ?[*]const u8 = self.str_bytes;
const other_bytes_ptr: ?[*]const u8 = other.str_bytes;
// If they are byte-for-byte equal, they're definitely equal!
if (self_bytes_ptr == other_bytes_ptr and self.str_len == other.str_len) {
return true;
}
const self_len = self.len();
const other_len = other.len();
// If their lengths are different, they're definitely unequal.
if (self_len != other_len) {
return false;
}
const self_u8_ptr: [*]const u8 = @ptrCast([*]const u8, &self);
const other_u8_ptr: [*]const u8 = @ptrCast([*]const u8, &other);
const self_bytes: [*]const u8 = if (self.isSmallStr() or self.isEmpty()) self_u8_ptr else self_bytes_ptr orelse unreachable;
const other_bytes: [*]const u8 = if (other.isSmallStr() or other.isEmpty()) other_u8_ptr else other_bytes_ptr orelse unreachable;
var index: usize = 0;
// TODO rewrite this into a for loop
const length = self.len();
while (index < length) {
if (self_bytes[index] != other_bytes[index]) {
return false;
}
index = index + 1;
}
return true;
}
pub fn clone(allocator: *Allocator, comptime T: type, in_place: InPlace, str: RocStr) RocStr {
if (str.isSmallStr() or str.isEmpty()) {
// just return the bytes
return str;
} else {
var new_str = RocStr.initBig(allocator, T, in_place, str.str_len);
var old_bytes: [*]u8 = @ptrCast([*]u8, str.str_bytes);
var new_bytes: [*]u8 = @ptrCast([*]u8, new_str.str_bytes);
@memcpy(new_bytes, old_bytes, str.str_len);
return new_str;
}
}
pub fn isSmallStr(self: RocStr) bool {
return @bitCast(isize, self.str_len) < 0;
}
pub fn len(self: RocStr) usize {
const bytes: [*]const u8 = @ptrCast([*]const u8, &self);
const last_byte = bytes[@sizeOf(RocStr) - 1];
const small_len = @as(usize, last_byte ^ 0b1000_0000);
const big_len = self.str_len;
// Since this conditional would be prone to branch misprediction,
// make sure it will compile to a cmov.
return if (self.isSmallStr()) small_len else big_len;
}
pub fn isEmpty(self: RocStr) bool {
return self.len() == 0;
}
pub fn asSlice(self: RocStr) []u8 {
// Since this conditional would be prone to branch misprediction,
// make sure it will compile to a cmov.
return self.asU8ptr()[0..self.len()];
}
pub fn asU8ptr(self: RocStr) [*]u8 {
// Since this conditional would be prone to branch misprediction,
// make sure it will compile to a cmov.
return if (self.isSmallStr() or self.isEmpty()) (&@bitCast([16]u8, self)) else (@ptrCast([*]u8, self.str_bytes));
}
// Given a pointer to some bytes, write the first (len) bytes of this
// RocStr's contents into it.
//
// One use for this function is writing into an `alloca` for a C string that
// only needs to live long enough to be passed as an argument to
// a C function - like the file path argument to `fopen`.
pub fn memcpy(self: RocStr, dest: [*]u8, length: usize) void {
const src = self.asU8ptr();
@memcpy(dest, src, length);
}
test "RocStr.eq: equal" {
const str1_len = 3;
var str1: [str1_len]u8 = "abc".*;
const str1_ptr: [*]u8 = &str1;
var roc_str1 = RocStr.init(testing.allocator, str1_ptr, str1_len);
const str2_len = 3;
var str2: [str2_len]u8 = "abc".*;
const str2_ptr: [*]u8 = &str2;
var roc_str2 = RocStr.init(testing.allocator, str2_ptr, str2_len);
expect(roc_str1.eq(roc_str2));
roc_str1.deinit(testing.allocator);
roc_str2.deinit(testing.allocator);
}
test "RocStr.eq: not equal different length" {
const str1_len = 4;
var str1: [str1_len]u8 = "abcd".*;
const str1_ptr: [*]u8 = &str1;
var roc_str1 = RocStr.init(testing.allocator, str1_ptr, str1_len);
const str2_len = 3;
var str2: [str2_len]u8 = "abc".*;
const str2_ptr: [*]u8 = &str2;
var roc_str2 = RocStr.init(testing.allocator, str2_ptr, str2_len);
defer {
roc_str1.deinit(testing.allocator);
roc_str2.deinit(testing.allocator);
}
expect(!roc_str1.eq(roc_str2));
}
test "RocStr.eq: not equal same length" {
const str1_len = 3;
var str1: [str1_len]u8 = "acb".*;
const str1_ptr: [*]u8 = &str1;
var roc_str1 = RocStr.init(testing.allocator, str1_ptr, str1_len);
const str2_len = 3;
var str2: [str2_len]u8 = "abc".*;
const str2_ptr: [*]u8 = &str2;
var roc_str2 = RocStr.init(testing.allocator, str2_ptr, str2_len);
defer {
roc_str1.deinit(testing.allocator);
roc_str2.deinit(testing.allocator);
}
expect(!roc_str1.eq(roc_str2));
}
};
// Str.equal
pub fn strEqual(self: RocStr, other: RocStr) callconv(.C) bool {
return self.eq(other);
}
// Str.numberOfBytes
pub fn strNumberOfBytes(string: RocStr) callconv(.C) usize {
return string.len();
}
// Str.fromInt
// When we actually use this in Roc, libc will be linked so we have access to std.heap.c_allocator
pub fn strFromIntC(int: i64) callconv(.C) RocStr {
return strFromInt(std.heap.c_allocator, int);
}
fn strFromInt(allocator: *Allocator, int: i64) RocStr {
// prepare for having multiple integer types in the future
return @call(.{ .modifier = always_inline }, strFromIntHelp, .{ allocator, i64, int });
}
fn strFromIntHelp(allocator: *Allocator, comptime T: type, int: T) RocStr {
// determine maximum size for this T
comptime const size = comptime blk: {
// the string representation of the minimum i128 value uses at most 40 characters
var buf: [40]u8 = undefined;
var result = std.fmt.bufPrint(&buf, "{}", .{std.math.minInt(T)}) catch unreachable;
break :blk result.len;
};
var buf: [size]u8 = undefined;
const result = std.fmt.bufPrint(&buf, "{}", .{int}) catch unreachable;
return RocStr.init(allocator, &buf, result.len);
}
// Str.split
// When we actually use this in Roc, libc will be linked so we have access to std.heap.c_allocator
pub fn strSplitInPlaceC(array: [*]RocStr, string: RocStr, delimiter: RocStr) callconv(.C) void {
return @call(.{ .modifier = always_inline }, strSplitInPlace, .{ std.heap.c_allocator, array, string, delimiter });
}
fn strSplitInPlace(allocator: *Allocator, array: [*]RocStr, string: RocStr, delimiter: RocStr) void {
var ret_array_index: usize = 0;
var slice_start_index: usize = 0;
var str_index: usize = 0;
const str_bytes = string.asU8ptr();
const str_len = string.len();
const delimiter_bytes_ptrs = delimiter.asU8ptr();
const delimiter_len = delimiter.len();
if (str_len > delimiter_len) {
const end_index: usize = str_len - delimiter_len + 1;
while (str_index <= end_index) {
var delimiter_index: usize = 0;
var matches_delimiter = true;
while (delimiter_index < delimiter_len) {
var delimiterChar = delimiter_bytes_ptrs[delimiter_index];
var strChar = str_bytes[str_index + delimiter_index];
if (delimiterChar != strChar) {
matches_delimiter = false;
break;
}
delimiter_index += 1;
}
if (matches_delimiter) {
const segment_len: usize = str_index - slice_start_index;
array[ret_array_index] = RocStr.init(allocator, str_bytes + slice_start_index, segment_len);
slice_start_index = str_index + delimiter_len;
ret_array_index += 1;
str_index += delimiter_len;
} else {
str_index += 1;
}
}
}
array[ret_array_index] = RocStr.init(allocator, str_bytes + slice_start_index, str_len - slice_start_index);
}
test "strSplitInPlace: no delimiter" {
// Str.split "abc" "!" == [ "abc" ]
const str_arr = "abc";
const str = RocStr.init(testing.allocator, str_arr, str_arr.len);
const delimiter_arr = "!";
const delimiter = RocStr.init(testing.allocator, delimiter_arr, delimiter_arr.len);
var array: [1]RocStr = undefined;
const array_ptr: [*]RocStr = &array;
strSplitInPlace(testing.allocator, array_ptr, str, delimiter);
var expected = [1]RocStr{
str,
};
defer {
for (array) |roc_str| {
roc_str.deinit(testing.allocator);
}
for (expected) |roc_str| {
roc_str.deinit(testing.allocator);
}
str.deinit(testing.allocator);
delimiter.deinit(testing.allocator);
}
expectEqual(array.len, expected.len);
expect(array[0].eq(expected[0]));
}
test "strSplitInPlace: empty end" {
const str_arr = "1---- ---- ---- ---- ----2---- ---- ---- ---- ----";
const str = RocStr.init(testing.allocator, str_arr, str_arr.len);
const delimiter_arr = "---- ---- ---- ---- ----";
const delimiter = RocStr.init(testing.allocator, delimiter_arr, delimiter_arr.len);
const array_len: usize = 3;
var array: [array_len]RocStr = [_]RocStr{
undefined,
undefined,
undefined,
};
const array_ptr: [*]RocStr = &array;
strSplitInPlace(testing.allocator, array_ptr, str, delimiter);
const one = RocStr.init(testing.allocator, "1", 1);
const two = RocStr.init(testing.allocator, "2", 1);
var expected = [3]RocStr{
one, two, RocStr.empty(),
};
defer {
for (array) |rocStr| {
rocStr.deinit(testing.allocator);
}
for (expected) |rocStr| {
rocStr.deinit(testing.allocator);
}
str.deinit(testing.allocator);
delimiter.deinit(testing.allocator);
}
expectEqual(array.len, expected.len);
expect(array[0].eq(expected[0]));
expect(array[1].eq(expected[1]));
expect(array[2].eq(expected[2]));
}
test "strSplitInPlace: delimiter on sides" {
const str_arr = "tttghittt";
const str = RocStr.init(testing.allocator, str_arr, str_arr.len);
const delimiter_arr = "ttt";
const delimiter = RocStr.init(testing.allocator, delimiter_arr, delimiter_arr.len);
const array_len: usize = 3;
var array: [array_len]RocStr = [_]RocStr{
undefined,
undefined,
undefined,
};
const array_ptr: [*]RocStr = &array;
strSplitInPlace(testing.allocator, array_ptr, str, delimiter);
const ghi_arr = "ghi";
const ghi = RocStr.init(testing.allocator, ghi_arr, ghi_arr.len);
var expected = [3]RocStr{
RocStr.empty(), ghi, RocStr.empty(),
};
defer {
for (array) |rocStr| {
rocStr.deinit(testing.allocator);
}
for (expected) |rocStr| {
rocStr.deinit(testing.allocator);
}
str.deinit(testing.allocator);
delimiter.deinit(testing.allocator);
}
expectEqual(array.len, expected.len);
expect(array[0].eq(expected[0]));
expect(array[1].eq(expected[1]));
expect(array[2].eq(expected[2]));
}
test "strSplitInPlace: three pieces" {
// Str.split "a!b!c" "!" == [ "a", "b", "c" ]
const str_arr = "a!b!c";
const str = RocStr.init(testing.allocator, str_arr, str_arr.len);
const delimiter_arr = "!";
const delimiter = RocStr.init(testing.allocator, delimiter_arr, delimiter_arr.len);
const array_len: usize = 3;
var array: [array_len]RocStr = undefined;
const array_ptr: [*]RocStr = &array;
strSplitInPlace(testing.allocator, array_ptr, str, delimiter);
const a = RocStr.init(testing.allocator, "a", 1);
const b = RocStr.init(testing.allocator, "b", 1);
const c = RocStr.init(testing.allocator, "c", 1);
var expected_array = [array_len]RocStr{
a, b, c,
};
defer {
for (array) |roc_str| {
roc_str.deinit(testing.allocator);
}
for (expected_array) |roc_str| {
roc_str.deinit(testing.allocator);
}
str.deinit(testing.allocator);
delimiter.deinit(testing.allocator);
}
expectEqual(expected_array.len, array.len);
expect(array[0].eq(expected_array[0]));
expect(array[1].eq(expected_array[1]));
expect(array[2].eq(expected_array[2]));
}
// This is used for `Str.split : Str, Str -> Array Str
// It is used to count how many segments the input `_str`
// needs to be broken into, so that we can allocate a array
// of that size. It always returns at least 1.
pub fn countSegments(string: RocStr, delimiter: RocStr) callconv(.C) usize {
const str_bytes = string.asU8ptr();
const str_len = string.len();
const delimiter_bytes_ptrs = delimiter.asU8ptr();
const delimiter_len = delimiter.len();
var count: usize = 1;
if (str_len > delimiter_len) {
var str_index: usize = 0;
const end_cond: usize = str_len - delimiter_len + 1;
while (str_index < end_cond) {
var delimiter_index: usize = 0;
var matches_delimiter = true;
while (delimiter_index < delimiter_len) {
const delimiterChar = delimiter_bytes_ptrs[delimiter_index];
const strChar = str_bytes[str_index + delimiter_index];
if (delimiterChar != strChar) {
matches_delimiter = false;
break;
}
delimiter_index += 1;
}
if (matches_delimiter) {
count += 1;
}
str_index += 1;
}
}
return count;
}
test "countSegments: long delimiter" {
// Str.split "str" "delimiter" == [ "str" ]
// 1 segment
const str_arr = "str";
const str = RocStr.init(testing.allocator, str_arr, str_arr.len);
const delimiter_arr = "delimiter";
const delimiter = RocStr.init(testing.allocator, delimiter_arr, delimiter_arr.len);
defer {
str.deinit(testing.allocator);
delimiter.deinit(testing.allocator);
}
const segments_count = countSegments(str, delimiter);
expectEqual(segments_count, 1);
}
test "countSegments: delimiter at start" {
// Str.split "hello there" "hello" == [ "", " there" ]
// 2 segments
const str_arr = "hello there";
const str = RocStr.init(testing.allocator, str_arr, str_arr.len);
const delimiter_arr = "hello";
const delimiter = RocStr.init(testing.allocator, delimiter_arr, delimiter_arr.len);
defer {
str.deinit(testing.allocator);
delimiter.deinit(testing.allocator);
}
const segments_count = countSegments(str, delimiter);
expectEqual(segments_count, 2);
}
test "countSegments: delimiter interspered" {
// Str.split "a!b!c" "!" == [ "a", "b", "c" ]
// 3 segments
const str_arr = "a!b!c";
const str = RocStr.init(testing.allocator, str_arr, str_arr.len);
const delimiter_arr = "!";
const delimiter = RocStr.init(testing.allocator, delimiter_arr, delimiter_arr.len);
defer {
str.deinit(testing.allocator);
delimiter.deinit(testing.allocator);
}
const segments_count = countSegments(str, delimiter);
expectEqual(segments_count, 3);
}
fn rocStrFromLiteral(bytes_arr: *const []u8) RocStr {}
// Str.startsWith
pub fn startsWith(string: RocStr, prefix: RocStr) callconv(.C) bool {
const bytes_len = string.len();
const bytes_ptr = string.asU8ptr();
const prefix_len = prefix.len();
const prefix_ptr = prefix.asU8ptr();
if (prefix_len > bytes_len) {
return false;
}
// we won't exceed bytes_len due to the previous check
var i: usize = 0;
while (i < prefix_len) {
if (bytes_ptr[i] != prefix_ptr[i]) {
return false;
}
i += 1;
}
return true;
}
test "startsWith: foo starts with fo" {
const foo = RocStr.init(testing.allocator, "foo", 3);
const fo = RocStr.init(testing.allocator, "fo", 2);
expect(startsWith(foo, fo));
}
test "startsWith: 123456789123456789 starts with 123456789123456789" {
const str = RocStr.init(testing.allocator, "123456789123456789", 18);
defer str.deinit(testing.allocator);
expect(startsWith(str, str));
}
test "startsWith: 12345678912345678910 starts with 123456789123456789" {
const str = RocStr.init(testing.allocator, "12345678912345678910", 20);
defer str.deinit(testing.allocator);
const prefix = RocStr.init(testing.allocator, "123456789123456789", 18);
defer prefix.deinit(testing.allocator);
expect(startsWith(str, prefix));
}
// Str.endsWith
pub fn endsWith(string: RocStr, suffix: RocStr) callconv(.C) bool {
const bytes_len = string.len();
const bytes_ptr = string.asU8ptr();
const suffix_len = suffix.len();
const suffix_ptr = suffix.asU8ptr();
if (suffix_len > bytes_len) {
return false;
}
const offset: usize = bytes_len - suffix_len;
var i: usize = 0;
while (i < suffix_len) {
if (bytes_ptr[i + offset] != suffix_ptr[i]) {
return false;
}
i += 1;
}
return true;
}
test "endsWith: foo ends with oo" {
const foo = RocStr.init(testing.allocator, "foo", 3);
const oo = RocStr.init(testing.allocator, "oo", 2);
defer foo.deinit(testing.allocator);
defer oo.deinit(testing.allocator);
expect(endsWith(foo, oo));
}
test "endsWith: 123456789123456789 ends with 123456789123456789" {
const str = RocStr.init(testing.allocator, "123456789123456789", 18);
defer str.deinit(testing.allocator);
expect(endsWith(str, str));
}
test "endsWith: 12345678912345678910 ends with 345678912345678910" {
const str = RocStr.init(testing.allocator, "12345678912345678910", 20);
const suffix = RocStr.init(testing.allocator, "345678912345678910", 18);
defer str.deinit(testing.allocator);
defer suffix.deinit(testing.allocator);
expect(endsWith(str, suffix));
}
test "endsWith: hello world ends with world" {
const str = RocStr.init(testing.allocator, "hello world", 11);
const suffix = RocStr.init(testing.allocator, "world", 5);
defer str.deinit(testing.allocator);
defer suffix.deinit(testing.allocator);
expect(endsWith(str, suffix));
}
// Str.concat
// When we actually use this in Roc, libc will be linked so we have access to std.heap.c_allocator
pub fn strConcatC(ptr_size: u32, result_in_place: InPlace, arg1: RocStr, arg2: RocStr) callconv(.C) RocStr {
return @call(.{ .modifier = always_inline }, strConcat, .{ std.heap.c_allocator, ptr_size, result_in_place, arg1, arg2 });
}
fn strConcat(allocator: *Allocator, ptr_size: u32, result_in_place: InPlace, arg1: RocStr, arg2: RocStr) RocStr {
return switch (ptr_size) {
4 => strConcatHelp(allocator, i32, result_in_place, arg1, arg2),
8 => strConcatHelp(allocator, i64, result_in_place, arg1, arg2),
else => unreachable,
};
}
fn strConcatHelp(allocator: *Allocator, comptime T: type, result_in_place: InPlace, arg1: RocStr, arg2: RocStr) RocStr {
if (arg1.isEmpty()) {
return RocStr.clone(allocator, T, result_in_place, arg2);
} else if (arg2.isEmpty()) {
return RocStr.clone(allocator, T, result_in_place, arg1);
} else {
const combined_length = arg1.len() + arg2.len();
const small_str_bytes = 2 * @sizeOf(T);
const result_is_big = combined_length >= small_str_bytes;
if (result_is_big) {
var result = RocStr.initBig(allocator, T, result_in_place, combined_length);
{
const old_bytes = arg1.asU8ptr();
const new_bytes: [*]u8 = @ptrCast([*]u8, result.str_bytes);
@memcpy(new_bytes, old_bytes, arg1.len());
}
{
const old_bytes = arg2.asU8ptr();
const new_bytes = @ptrCast([*]u8, result.str_bytes) + arg1.len();
@memcpy(new_bytes, old_bytes, arg2.len());
}
return result;
} else {
var result = [16]u8{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
// if the result is small, then for sure arg1 and arg2 are also small
{
var old_bytes: [*]u8 = @ptrCast([*]u8, &@bitCast([16]u8, arg1));
var new_bytes: [*]u8 = @ptrCast([*]u8, &result);
@memcpy(new_bytes, old_bytes, arg1.len());
}
{
var old_bytes: [*]u8 = @ptrCast([*]u8, &@bitCast([16]u8, arg2));
var new_bytes = @ptrCast([*]u8, &result) + arg1.len();
@memcpy(new_bytes, old_bytes, arg2.len());
}
const mask: u8 = 0b1000_0000;
const final_byte = @truncate(u8, combined_length) | mask;
result[small_str_bytes - 1] = final_byte;
return @bitCast(RocStr, result);
}
return result;
}
}
test "RocStr.concat: small concat small" {
const str1_len = 3;
var str1: [str1_len]u8 = "foo".*;
const str1_ptr: [*]u8 = &str1;
var roc_str1 = RocStr.init(testing.allocator, str1_ptr, str1_len);
const str2_len = 3;
var str2: [str2_len]u8 = "abc".*;
const str2_ptr: [*]u8 = &str2;
var roc_str2 = RocStr.init(testing.allocator, str2_ptr, str2_len);
const str3_len = 6;
var str3: [str3_len]u8 = "fooabc".*;
const str3_ptr: [*]u8 = &str3;
var roc_str3 = RocStr.init(testing.allocator, str3_ptr, str3_len);
defer {
roc_str1.deinit(testing.allocator);
roc_str2.deinit(testing.allocator);
roc_str3.deinit(testing.allocator);
}
const result = strConcat(testing.allocator, 8, InPlace.Clone, roc_str1, roc_str2);
defer result.deinit(testing.allocator);
expect(roc_str3.eq(result));
}

View file

@ -1,23 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
[[package]]
name = "host"
version = "0.1.0"
dependencies = [
"roc_std 0.1.0",
]
[[package]]
name = "libc"
version = "0.2.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "roc_std"
version = "0.1.0"
dependencies = [
"libc 0.2.79 (registry+https://github.com/rust-lang/crates.io-index)",
]
[metadata]
"checksum libc 0.2.79 (registry+https://github.com/rust-lang/crates.io-index)" = "2448f6066e80e3bfc792e9c98bf705b4b0fc6e8ef5b43e5889aff0eaa9c58743"

View file

@ -1,13 +0,0 @@
[package]
name = "host"
version = "0.1.0"
authors = ["Richard Feldman <oss@rtfeldman.com>"]
edition = "2018"
[lib]
crate-type = ["staticlib"]
[dependencies]
roc_std = { path = "../../../roc_std" }
[workspace]

View file

@ -4,7 +4,7 @@ platform examples/quicksort
packages {} packages {}
imports [] imports []
provides [ mainForHost ] provides [ mainForHost ]
effects Effect {} effects fx.Effect {}
mainForHost : List I64 -> List I64 mainForHost : List I64 -> List I64
mainForHost = \list -> quicksort list mainForHost = \list -> quicksort list

View file

@ -1,8 +0,0 @@
# Rebuilding the host from source
Run `build.sh` to manually rebuild this platform's host.
Note that the compiler currently has its own logic for rebuilding these hosts
(in `link.rs`). It's hardcoded for now, but the long-term goal is that
hosts will be precompiled by platform authors and distributed in packages,
at which point only package authors will need to think about rebuilding hosts.

View file

@ -1,12 +0,0 @@
#!/bin/bash
# compile c_host.o and rust_host.o
clang -c host.c -o c_host.o
rustc host.rs -o rust_host.o
# link them together into host.o
ld -r c_host.o rust_host.o -o host.o
# clean up
rm -f c_host.o
rm -f rust_host.o

View file

@ -1,7 +0,0 @@
#include <stdio.h>
extern int rust_main();
int main() {
return rust_main();
}

View file

@ -0,0 +1,70 @@
const std = @import("std");
const testing = std.testing;
const expectEqual = testing.expectEqual;
const expect = testing.expect;
const mem = std.mem;
const Allocator = mem.Allocator;
extern fn roc__mainForHost_1_exposed(RocList, *RocCallResult) void;
// warning! the array is currently stack-allocated so don't make this too big
const NUM_NUMS = 100;
const RocList = extern struct { elements: [*]i64, length: usize };
const RocCallResult = extern struct { flag: usize, content: RocList };
const Unit = extern struct {};
pub export fn main() i32 {
const stdout = std.io.getStdOut().writer();
const stderr = std.io.getStdErr().writer();
var raw_numbers: [NUM_NUMS + 1]i64 = undefined;
var numbers = raw_numbers[1..];
for (numbers) |x, i| {
numbers[i] = @mod(@intCast(i64, i), 12);
}
const roc_list = RocList{ .elements = numbers, .length = NUM_NUMS };
// make space for the result
var callresult = RocCallResult{ .flag = 0, .content = undefined };
// start time
var ts1: std.os.timespec = undefined;
std.os.clock_gettime(std.os.CLOCK_REALTIME, &ts1) catch unreachable;
// actually call roc to populate the callresult
roc__mainForHost_1_exposed(roc_list, &callresult);
// stdout the result
const length = std.math.min(20, callresult.content.length);
var result = callresult.content.elements[0..length];
for (result) |x, i| {
if (i == 0) {
stdout.print("[{}, ", .{x}) catch unreachable;
} else if (i == length - 1) {
stdout.print("{}]\n", .{x}) catch unreachable;
} else {
stdout.print("{}, ", .{x}) catch unreachable;
}
}
// end time
var ts2: std.os.timespec = undefined;
std.os.clock_gettime(std.os.CLOCK_REALTIME, &ts2) catch unreachable;
const delta = to_seconds(ts2) - to_seconds(ts1);
stderr.print("runtime: {d:.3}ms\n", .{delta * 1000}) catch unreachable;
return 0;
}
fn to_seconds(tms: std.os.timespec) f64 {
return @intToFloat(f64, tms.tv_sec) + (@intToFloat(f64, tms.tv_nsec) / 1_000_000_000.0);
}

View file

@ -1,52 +0,0 @@
use roc_std::RocCallResult;
use roc_std::RocList;
use std::time::SystemTime;
extern "C" {
#[link_name = "roc__mainForHost_1_exposed"]
fn quicksort(list: RocList<i64>, output: &mut RocCallResult<RocList<i64>>) -> ();
}
const NUM_NUMS: usize = 100;
#[no_mangle]
pub fn rust_main() -> isize {
let nums: RocList<i64> = {
let mut nums = Vec::with_capacity(NUM_NUMS);
for index in 0..nums.capacity() {
let num = index as i64 % 12;
nums.push(num);
}
RocList::from_slice(&nums)
};
println!("Running Roc quicksort on {} numbers...", nums.len());
let start_time = SystemTime::now();
let answer = unsafe {
use std::mem::MaybeUninit;
let mut output = MaybeUninit::uninit();
quicksort(nums, &mut *output.as_mut_ptr());
match output.assume_init().into() {
Ok(value) => value,
Err(msg) => panic!("roc failed with message {}", msg),
}
};
let end_time = SystemTime::now();
let duration = end_time.duration_since(start_time).unwrap();
println!(
"Roc quicksort took {:.4} ms to compute this answer: {:?}",
duration.as_secs_f64() * 1000.0,
// truncate the answer, so stdout is not swamped
&answer.as_slice()[0..20]
);
// Exit code
0
}