Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,5 @@ build

# sqlite database temp file
*.db-shm

basic-cli/
88 changes: 88 additions & 0 deletions ci/check-sync-basic-cli.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
#!/bin/bash

# Exit on any error
set -e

# Repository URL
REPO_URL="https://github.com/roc-lang/basic-cli"
CLONE_DIR="basic-cli"

# List of files to compare
FILES=("Cmd.roc" "Dir.roc" "Env.roc" "EnvDecoding.roc" "File.roc" "Http.roc" "InternalCmd.roc" "InternalDateTime.roc" "InternalHttp.roc" "InternalIOErr.roc" "InternalPath.roc" "InternalSqlite.roc" "Path.roc" "Sleep.roc" "Sqlite.roc" "Stderr.roc" "Stdout.roc" "Tcp.roc" "Url.roc" "Utc.roc")

# Track differing files
DIFFERING_FILES=()

# Function to normalize file endings (remove trailing newlines)
normalize_file() {
local file="$1"
local temp_file=$(mktemp)
# Remove trailing newlines - much faster approach
printf '%s' "$(cat "$file")" > "$temp_file"
echo "$temp_file"
}

# Remove existing clone directory if it exists
if [[ -d "$CLONE_DIR" ]]; then
echo "Removing existing $CLONE_DIR directory..."
rm -rf "$CLONE_DIR"
fi

# Clone the repository with depth 1
echo "Cloning repository..."
git clone --depth 1 "$REPO_URL" "$CLONE_DIR"
echo "Done cloning."

echo "Comparing files..."

# Compare each file
for file in "${FILES[@]}"; do
LOCAL_FILE="./platform/$file"
REMOTE_FILE="./$CLONE_DIR/platform/$file"

# Check if both files exist
if [[ ! -f "$LOCAL_FILE" ]]; then
echo "Warning: Local file $LOCAL_FILE does not exist"
DIFFERING_FILES+=("$file")
continue
fi

if [[ ! -f "$REMOTE_FILE" ]]; then
echo "Warning: Remote file $REMOTE_FILE does not exist"
DIFFERING_FILES+=("$file")
continue
fi

# Create normalized versions of both files
LOCAL_NORMALIZED=$(normalize_file "$LOCAL_FILE")
REMOTE_NORMALIZED=$(normalize_file "$REMOTE_FILE")

# Perform diff on normalized files and check if files differ
if ! diff -q "$LOCAL_NORMALIZED" "$REMOTE_NORMALIZED" > /dev/null; then
echo "=== Start of diff for $file ==="
diff "$LOCAL_NORMALIZED" "$REMOTE_NORMALIZED" || true
echo "=== End of diff for $file ==="
echo
DIFFERING_FILES+=("$file")
else
echo "Files match: $file"
fi

# Clean up temporary files
rm -f "$LOCAL_NORMALIZED" "$REMOTE_NORMALIZED"
done

# Clean up cloned directory
rm -rf "$CLONE_DIR"

# Output results
if [[ ${#DIFFERING_FILES[@]} -gt 0 ]]; then
echo
echo "Some files differ with what's in basic-cli/platform, they should probably be identical:"
printf '%s\n' "${DIFFERING_FILES[@]}"
exit 1
else
echo
echo "All files match!"
exit 0
fi
10 changes: 10 additions & 0 deletions crates/roc_host/src/roc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,11 @@ pub extern "C" fn roc_fx_stdout_write(text: &RocStr) -> RocResult<(), roc_io_err
roc_stdio::stdout_write(text)
}

#[no_mangle]
pub extern "C" fn roc_fx_stdout_write_bytes(bytes: &RocList<u8>) -> RocResult<(), roc_io_error::IOErr> {
roc_stdio::stdout_write_bytes(bytes)
}

#[no_mangle]
pub extern "C" fn roc_fx_stderr_line(line: &RocStr) -> RocResult<(), roc_io_error::IOErr> {
roc_stdio::stderr_line(line)
Expand All @@ -253,6 +258,11 @@ pub extern "C" fn roc_fx_stderr_write(text: &RocStr) -> RocResult<(), roc_io_err
roc_stdio::stderr_write(text)
}

#[no_mangle]
pub extern "C" fn roc_fx_stderr_write_bytes(bytes: &RocList<u8>) -> RocResult<(), roc_io_error::IOErr> {
roc_stdio::stderr_write_bytes(bytes)
}

#[no_mangle]
pub extern "C" fn roc_fx_posix_time() -> roc_std::U128 {
roc_env::posix_time()
Expand Down
3 changes: 1 addition & 2 deletions platform/Dir.roc
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ delete_empty! = |path|
## This may fail if:
## - the path doesn't exist
## - the path is not a directory
## - the directory is not empty
## - the user lacks permission to remove the directory.
##
## > [Path.delete_all!] does the same thing, except it takes a [Path] instead of a [Str].
Expand Down Expand Up @@ -70,4 +69,4 @@ create! = |path|
## > [Path.create_all!] does the same thing, except it takes a [Path] instead of a [Str].
create_all! : Str => Result {} [DirErr IOErr]
create_all! = |path|
Path.create_all!(Path.from_str(path))
Path.create_all!(Path.from_str(path))
6 changes: 3 additions & 3 deletions platform/EnvDecoding.roc
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ env_list = |decode_elem|
# exercised, and the solver can find an ambient lambda set for the
# specialization.
env_record : _, (_, _ -> [Keep (Decoder _ _), Skip]), (_, _ -> _) -> Decoder _ _
env_record = |_initialState, _stepField, _finalizer|
env_record = |_initial_state, _step_field, _finalizer|
Decode.custom(
|bytes, @EnvFormat({})|
{ result: Err(TooShort), rest: bytes },
Expand All @@ -114,8 +114,8 @@ env_record = |_initialState, _stepField, _finalizer|
# exercised, and the solver can find an ambient lambda set for the
# specialization.
env_tuple : _, (_, _ -> [Next (Decoder _ _), TooLong]), (_ -> _) -> Decoder _ _
env_tuple = |_initialState, _stepElem, _finalizer|
env_tuple = |_initial_state, _step_elem, _finalizer|
Decode.custom(
|bytes, @EnvFormat({})|
{ result: Err(TooShort), rest: bytes },
)
)
4 changes: 4 additions & 0 deletions platform/Host.roc
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ hosted Host
exe_path!,
stdout_line!,
stdout_write!,
stdout_write_bytes!,
stderr_line!,
stderr_write!,
stderr_write_bytes!,
tty_mode_canonical!,
tty_mode_raw!,
send_request!,
Expand Down Expand Up @@ -117,8 +119,10 @@ temp_dir! : {} => List U8
# STDIO
stdout_line! : Str => Result {} InternalIOErr.IOErrFromHost
stdout_write! : Str => Result {} InternalIOErr.IOErrFromHost
stdout_write_bytes! : List U8 => Result {} InternalIOErr.IOErrFromHost
stderr_line! : Str => Result {} InternalIOErr.IOErrFromHost
stderr_write! : Str => Result {} InternalIOErr.IOErrFromHost
stderr_write_bytes! : List U8 => Result {} InternalIOErr.IOErrFromHost

# TCP
send_request! : InternalHttp.RequestToAndFromHost => InternalHttp.ResponseToAndFromHost
Expand Down
65 changes: 52 additions & 13 deletions platform/Http.roc
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,54 @@ module [
import InternalHttp
import Host

## Represents an HTTP method.
## Represents an HTTP method: `[OPTIONS, GET, POST, PUT, DELETE, HEAD, TRACE, CONNECT, PATCH, EXTENSION Str]`
Method : InternalHttp.Method

## Represents an HTTP header e.g. `Content-Type: application/json`
## Represents an HTTP header e.g. `Content-Type: application/json`.
## Header is a `{ name : Str, value : Str }`.
Header : InternalHttp.Header

## Represents an HTTP request.
## Request is a record:
## ```
## {
## method : Method,
## headers : List Header,
## uri : Str,
## body : List U8,
## timeout_ms : [TimeoutMilliseconds U64, NoTimeout],
## }
## ```
Request : InternalHttp.Request

## Represents an HTTP response.
##
## Response is a record with the following fields:
## ```
## {
## status : U16,
## headers : List Header,
## body : List U8
## }
## ```
Response : InternalHttp.Response

## A default [Request] value.
## A default [Request] value with the following values:
## ```
## {
## method: GET
## headers: []
## uri: ""
## body: []
## timeout_ms: NoTimeout
## }
## ```
##
## Example:
## ```
## # GET "roc-lang.org"
## { Http.default_request &
## url: "https://www.roc-lang.org",
## uri: "https://www.roc-lang.org",
## }
## ```
##
Expand All @@ -47,20 +77,20 @@ default_request = {
##
## See common headers [here](https://en.wikipedia.org/wiki/List_of_HTTP_header_fields).
##
## Example: `header(("Content-Type", "application/json"))`
##
header : (Str, Str) -> Header
header = |(name, value)| { name, value }

## Send an HTTP request, succeeds with a value of [Str] or fails with an
## [Err].
## Send an HTTP request, succeeds with a [Response] or fails with a [HttpErr _].
##
## ```
## # Prints out the HTML of the Roc-lang website.
## response = ||
## Http.send!({ Http.default_request & url: "https://www.roc-lang.org" })?
##
## response : Response
## response =
## Http.send!({ Http.default_request & uri: "https://www.roc-lang.org" })?
##
## Str.from_utf8(response.body) ?? "Invalid UTF-8"
## |> Stdout.line
## Stdout.line!(Str.from_utf8(response.body))?
## ```
send! : Request => Result Response [HttpErr [Timeout, NetworkError, BadBody, Other (List U8)]]
send! = |request|
Expand Down Expand Up @@ -88,7 +118,7 @@ send! = |request|
## ```
## import json.Json
##
## # On the server side we send `Encode.to_bytes {foo: "Hello Json!"} Json.utf8`
## # On the server side we send `Encode.to_bytes({foo: "Hello Json!"}, Json.utf8)`
## { foo } = Http.get!("http://localhost:8000", Json.utf8)?
## ```
get! : Str, fmt => Result body [HttpDecodingFailed, HttpErr _] where body implements Decoding, fmt implements DecoderFormatting
Expand All @@ -98,10 +128,19 @@ get! = |uri, fmt|
Decode.from_bytes(response.body, fmt)
|> Result.map_err(|_| HttpDecodingFailed)

# Contributor note: Trying to use BadUtf8 { problem : Str.Utf8Problem, index : U64 } in the error here results in a "Alias `6.IdentId(11)` not registered in delayed aliases!".
## Try to perform an HTTP get request and convert the received bytes (in the body) into a UTF-8 string.
##
## ```
## # On the server side we, send `Str.to_utf8("Hello utf8")`
##
## hello_str : Str
## hello_str = Http.get_utf8!("http://localhost:8000")?
## ```
get_utf8! : Str => Result Str [BadBody Str, HttpErr _]
get_utf8! = |uri|
response = send!({ default_request & uri })?

response.body
|> Str.from_utf8
|> Result.map_err(|_| BadBody("Invalid UTF-8"))
|> Result.map_err(|err| BadBody("Error in get_utf8!: failed to convert received body bytes into utf8:\n\t${Inspect.to_str(err)}"))
18 changes: 9 additions & 9 deletions platform/InternalDateTime.roc
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ epoch_millis_to_datetime = |millis|
month = 1
year = 1970

epoch_millis_to_datetimeHelp(
epoch_millis_to_datetime_help(
{
year,
month,
Expand All @@ -111,8 +111,8 @@ epoch_millis_to_datetime = |millis|
},
)

epoch_millis_to_datetimeHelp : DateTime -> DateTime
epoch_millis_to_datetimeHelp = |current|
epoch_millis_to_datetime_help : DateTime -> DateTime
epoch_millis_to_datetime_help = |current|
count_days_in_month = days_in_month(current.year, current.month)
count_days_in_prev_month =
if current.month == 1 then
Expand All @@ -121,36 +121,36 @@ epoch_millis_to_datetimeHelp = |current|
days_in_month(current.year, (current.month - 1))

if current.day < 1 then
epoch_millis_to_datetimeHelp(
epoch_millis_to_datetime_help(
{ current &
year: if current.month == 1 then current.year - 1 else current.year,
month: if current.month == 1 then 12 else current.month - 1,
day: current.day + count_days_in_prev_month,
},
)
else if current.hours < 0 then
epoch_millis_to_datetimeHelp(
epoch_millis_to_datetime_help(
{ current &
day: current.day - 1,
hours: current.hours + 24,
},
)
else if current.minutes < 0 then
epoch_millis_to_datetimeHelp(
epoch_millis_to_datetime_help(
{ current &
hours: current.hours - 1,
minutes: current.minutes + 60,
},
)
else if current.seconds < 0 then
epoch_millis_to_datetimeHelp(
epoch_millis_to_datetime_help(
{ current &
minutes: current.minutes - 1,
seconds: current.seconds + 60,
},
)
else if current.day > count_days_in_month then
epoch_millis_to_datetimeHelp(
epoch_millis_to_datetime_help(
{ current &
year: if current.month == 12 then current.year + 1 else current.year,
month: if current.month == 12 then 1 else current.month + 1,
Expand Down Expand Up @@ -218,4 +218,4 @@ expect
# test 1_600_005_179_000 ms past epoch
expect
str = 1_600_005_179_000 |> epoch_millis_to_datetime |> to_iso_8601
str == "2020-09-13T13:52:59Z"
str == "2020-09-13T13:52:59Z"
6 changes: 3 additions & 3 deletions platform/InternalPath.roc
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,12 @@ UnwrappedPath : [
# aren't nul-terminated, while also being able to be passed directly to OS APIs.
FromOperatingSystem (List U8),

# These come from userspace (e.g. Path.fromBytes), so they need to be checked for interior
# These come from userspace (e.g. Path.from_bytes), so they need to be checked for interior
# nuls and then nul-terminated before the host can pass them to OS APIs.
ArbitraryBytes (List U8),

# This was created as a RocStr, so it might have interior nul bytes but it's definitely UTF-8.
# That means we can `toStr` it trivially, but have to validate before sending it to OS
# That means we can `to_str` it trivially, but have to validate before sending it to OS
# APIs that expect a nul-terminated `char*`.
#
# Note that both UNIX and Windows APIs will accept UTF-8, because on Windows the host calls
Expand Down Expand Up @@ -75,4 +75,4 @@ from_arbitrary_bytes = |bytes|

from_os_bytes : List U8 -> InternalPath
from_os_bytes = |bytes|
@InternalPath(FromOperatingSystem(bytes))
@InternalPath(FromOperatingSystem(bytes))
Loading