Skip to content

Commit 20ea37b

Browse files
Merge pull request #529 from roc-lang/release/0.26-api-feedback
Prepare 0.26 runtime APIs and Linux allocator performance
2 parents cbd8b47 + 6ae7940 commit 20ea37b

79 files changed

Lines changed: 15609 additions & 4793 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/ci.yml‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,15 @@ jobs:
7171
run: python3 -m unittest scripts/test_check_release_version.py scripts/test_check_release_branch.py scripts/test_check_version_bump.py scripts/test_update_nix_release.py scripts/test_check_roc_nightly.py scripts/test_windows_runtime.py
7272
- name: Check Roc nightly pins are in sync
7373
run: python3 scripts/check_roc_nightly.py
74+
- name: Check OS witness API boundaries
75+
run: python3 scripts/test_system_api.py
7476
- name: Format, check, and test examples
7577
run: ./scripts/test.py --operation validate
78+
- name: Check musl allocator syscall churn
79+
run: |
80+
sudo apt-get update
81+
sudo apt-get install --yes strace
82+
python3 scripts/test_musl_allocator.py
7683
- name: Check native resource ownership with glibc and Valgrind
7784
run: |
7885
sudo apt-get update
@@ -219,3 +226,7 @@ jobs:
219226
if: matrix.valgrind
220227
shell: bash
221228
run: ./scripts/test.py --operation run --target "${{ matrix.target }}" --artifact-dir dist/example-binaries --valgrind
229+
230+
- name: Stop a real systemd service and verify application cleanup
231+
if: matrix.target == 'x64musl' || matrix.target == 'arm64musl'
232+
run: sudo python3 scripts/test_systemd_cleanup.py dist/example-binaries/${{ matrix.target }}/signal-cleanup

‎CONTRIBUTING.md‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,19 @@ decisions and assumptions, not open questions.
304304
unlimited is legal; exceeding one is `ReadLimitExceeded(U64)`. Only network
305305
reads have a default line limit.
306306

307+
### Cooperative stop requests and OS-specific effects
308+
309+
- Cooperative cancellation is bare `Interrupted`, beside the module's normal
310+
errors. It takes precedence over a simultaneously expired timeout. Pending
311+
caught events remain sticky until `Signal.poll!()` acknowledges them.
312+
- Register before acquiring resources. Reads preserve unread bytes across
313+
cancellation; writes can make partial progress. Return only after a worker
314+
acknowledges cancellation, so it cannot write later. Cleanup operations
315+
must remain available with a pending event.
316+
- Keep shared APIs portable. Obtain opaque OS evidence through `System.target!`
317+
and require it explicitly for a Unix-only operation; do not add pure witness
318+
factories, codecs, or environment-based target detection.
319+
307320
### End of input and streams
308321

309322
- A single read ends with `Err(EndOfInput)`; a short fixed-size read is

‎Cargo.lock‎

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Cargo.toml‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,16 +31,22 @@ hyper-rustls = { version = "=0.27.6", default-features = false, features = ["htt
3131
http-body-util = "=0.1.3"
3232
tokio = { version = "=1.45.0", default-features = false, features = ["rt", "net", "time", "process", "sync", "io-util", "macros"] }
3333
bytes = "=1.12.1"
34+
tower-service = "=0.3.3"
3435

3536
[dev-dependencies]
3637
# Exercise the production ring provider against a local TLS server.
3738
rustls = { version = "=0.23.45", default-features = false, features = ["std", "ring", "tls12"] }
3839

40+
# Keep freed small-allocation pages on the statically linked Linux hosts.
41+
# Generated Roc ABI helpers already allocate through Rust's global allocator.
42+
[target.'cfg(all(target_os = "linux", target_env = "musl"))'.dependencies]
43+
mimalloc = { version = "=0.1.52", default-features = false }
44+
3945
[target.'cfg(not(target_os = "macos"))'.dependencies]
4046
sys-locale = "=0.3.2"
4147

4248
[target.'cfg(windows)'.dependencies]
43-
windows-sys = { version = "=0.59.0", features = ["Win32_Foundation", "Win32_Networking_WinSock", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Console", "Win32_System_Memory"] }
49+
windows-sys = { version = "=0.59.0", features = ["Win32_Foundation", "Win32_Networking_WinSock", "Win32_Security", "Win32_Security_Authorization", "Win32_Storage_FileSystem", "Win32_System_Console", "Win32_System_Memory", "Win32_System_IO", "Win32_System_Threading", "Win32_System_SystemServices"] }
4450

4551
[profile.release]
4652
lto = true

‎README.md‎

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ build commands, hashes and licenses accompany the platform bundle in
2929

3030
## Host runtime behavior
3131

32-
These current host-level limitations may affect application design:
32+
These host behaviors may affect application design:
3333

3434
- HTTP and HTTPS use HTTP/1 only. [Issue #455](https://github.com/roc-lang/basic-cli/issues/455)
3535
tracks research into HTTP/2 support and its tradeoffs.
@@ -49,6 +49,24 @@ These current host-level limitations may affect application design:
4949
still be read; the `Request` deadline passing is final. Each streaming
5050
response has its own connection, closed when the body ends or fails, or when
5151
the response and its streams are released. Request bodies are not streamed.
52+
- HTTP redirects are opt-in through trailing options, `{ max_redirects: n }`;
53+
the default is zero. The request deadline covers every hop and the final
54+
body. Cross-origin hops strip credentials; HTTPS downgrades are refused.
55+
- `Signal.catch!` catches Ctrl-C and, with a `System.Unix` witness, SIGTERM
56+
(including default `systemctl stop`). Blocking effects return bare
57+
`Interrupted`. Call `Signal.poll!()` before effectful application cleanup;
58+
catching a signal does not clean up external resources itself. Forced
59+
termination and systemd's eventual stop-timeout escalation remain uncatchable.
60+
- `System.target!()` supplies opaque OS witnesses. `Cmd.exec!(unix, path, args)`
61+
replaces a Unix process at an exact path and bypasses Roc cleanup on success;
62+
`Cmd.call!(program, args)` runs a child and waits for success.
63+
- UnixSocket pathname streams work on Linux, macOS, and Windows 10 version
64+
1803 (build 17134) or newer, including Windows 11. Path limits and native
65+
filesystem name restrictions apply. Closing does not unlink the socket.
66+
- File writer options apply `OwnerOnly` or `PublicRead` permissions only when
67+
creating a new regular file. Existing files retain their access grants;
68+
use `RequireNew` with `OwnerOnly` for private files. Explicit permissions
69+
replace ACL grants and fail if the filesystem cannot enforce the policy.
5270
- Several network and TLS failures currently use a generic HTTP transport
5371
error ([issue #438](https://github.com/roc-lang/basic-cli/issues/438)).
5472
- TCP reads and writes always have a timeout: a `Tcp.Connection` holds the
@@ -57,7 +75,7 @@ These current host-level limitations may affect application design:
5775
to connect or accept is separate; only `accept!` may wait indefinitely
5876
(`NoTimeout`). `Duration.zero` expires immediately, and expiry is always
5977
`TcpErr(TimedOut)`.
60-
- A TCP read that times out consumes nothing: the bytes that had arrived stay
78+
- A TCP read that times out or is interrupted consumes nothing: the bytes that had arrived stay
6179
buffered for the next read. A write that times out may have sent any prefix.
6280
- `Tcp.Connection.read_until!`, `read_line!` and `lines` stop at the
6381
connection's line limit (1 MiB unless changed with `with_line_limit`),

‎THIRD_PARTY_LICENSES.md‎

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,65 @@ with the MSVC ABI for native
3737
C dependencies. It generates import archives from open-source definitions and
3838
does not copy Microsoft SDK or CRT archives into the platform.
3939

40+
## mimalloc (Linux musl hosts)
41+
42+
The Linux musl Rust host uses mimalloc for Rust and Roc general allocations.
43+
The Rust wrapper (mimalloc 0.1.52 / libmimalloc-sys 0.1.49) and bundled mimalloc
44+
are licensed under MIT. Sources: https://github.com/purpleprotocol/mimalloc_rust
45+
and https://github.com/microsoft/mimalloc.
46+
47+
```
48+
Copyright 2019 Octavian Oncescu
49+
50+
Permission is hereby granted, free of charge, to any
51+
person obtaining a copy of this software and associated
52+
documentation files (the "Software"), to deal in the
53+
Software without restriction, including without
54+
limitation the rights to use, copy, modify, merge,
55+
publish, distribute, sublicense, and/or sell copies of
56+
the Software, and to permit persons to whom the Software
57+
is furnished to do so, subject to the following
58+
conditions:
59+
60+
The above copyright notice and this permission notice
61+
shall be included in all copies or substantial portions
62+
of the Software.
63+
64+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
65+
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
66+
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
67+
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
68+
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
69+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
70+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
71+
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
72+
DEALINGS IN THE SOFTWARE.
73+
```
74+
75+
```
76+
MIT License
77+
78+
Copyright (c) 2018-2025 Microsoft Corporation, Daan Leijen
79+
80+
Permission is hereby granted, free of charge, to any person obtaining a copy
81+
of this software and associated documentation files (the "Software"), to deal
82+
in the Software without restriction, including without limitation the rights
83+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
84+
copies of the Software, and to permit persons to whom the Software is
85+
furnished to do so, subject to the following conditions:
86+
87+
The above copyright notice and this permission notice shall be included in all
88+
copies or substantial portions of the Software.
89+
90+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
91+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
92+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
93+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
94+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
95+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
96+
SOFTWARE.
97+
```
98+
4099
## musl libc
41100

42101
Files: `platform/targets/*/libc.a`, `platform/targets/*/crt1.o`

‎ci/regenerate_glue.sh‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ fi
9999
run_glue() {
100100
local out_dir=$1
101101
mkdir -p "$out_dir"
102-
"$ROC_BIN" glue "$GLUE_SPEC" "$out_dir" "$PLATFORM_FILE"
102+
"$ROC_BIN" glue --no-cache "$GLUE_SPEC" "$out_dir" "$PLATFORM_FILE"
103103
}
104104

105105
if [ "$MODE" = "check" ]; then

‎ci/rust_http_server/src/main.rs‎

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@ fn pattern(start: usize, len: usize) -> Bytes {
8383
}
8484

8585
async fn handle_request(req: Request<Incoming>) -> Result<Response<BoxBody>, GenericError> {
86+
if req.uri().path().starts_with("/redirect/") {
87+
return handle_redirect(req).await;
88+
}
8689
let response = match (req.method(), req.uri().path()) {
8790
(&Method::GET, "/utf8test") => {
8891
// UTF-8 encoded "Hello utf8"
@@ -169,6 +172,136 @@ async fn handle_request(req: Request<Incoming>) -> Result<Response<BoxBody>, Gen
169172
Ok(response)
170173
}
171174

175+
/// Redirects intentionally keep their bodies unfinished. Following a redirect
176+
/// must close that body and send the next request without collecting it.
177+
fn redirect(status: u16, locations: &[String]) -> Result<Response<BoxBody>, GenericError> {
178+
let mut response = Response::builder().status(status);
179+
for location in locations {
180+
response = response.header("Location", location);
181+
}
182+
Ok(response.body(parts([
183+
Part::Data(Bytes::from_static(b"discard this intermediate body")),
184+
Part::Pause(Duration::from_secs(60)),
185+
]))?)
186+
}
187+
188+
async fn handle_redirect(req: Request<Incoming>) -> Result<Response<BoxBody>, GenericError> {
189+
let path = req.uri().path().to_owned();
190+
if path == "/redirect/inspect" {
191+
let method = req.method().as_str().to_owned();
192+
let uri = req.uri().to_string();
193+
let fields = [
194+
"host",
195+
"authorization",
196+
"cookie",
197+
"proxy-authorization",
198+
"content-type",
199+
"content-length",
200+
"x-hop",
201+
"x-public",
202+
"expect",
203+
"digest",
204+
];
205+
let headers: String = fields
206+
.into_iter()
207+
.map(|name| {
208+
format!(
209+
"{name}: {}\n",
210+
req.headers()
211+
.get(name)
212+
.map_or("<absent>", |v| v.to_str().unwrap_or("<invalid>"))
213+
)
214+
})
215+
.collect();
216+
let body = req.into_body().collect().await?.to_bytes();
217+
let report = format!(
218+
"method: {method}\nuri: {uri}\nbody: {}\n{headers}",
219+
String::from_utf8_lossy(&body)
220+
);
221+
return Ok(Response::builder()
222+
.header("X-Request-Method", method)
223+
.body(full(report))?);
224+
}
225+
if path == "/redirect/final" {
226+
return Ok(Response::new(full("final")));
227+
}
228+
if let Some(left) = path.strip_prefix("/redirect/delayed/") {
229+
tokio::time::sleep(Duration::from_millis(250)).await;
230+
let left: u8 = left.parse()?;
231+
return if left == 0 {
232+
Ok(Response::new(parts([
233+
Part::Pause(Duration::from_millis(250)),
234+
Part::Data(Bytes::from_static(b"final")),
235+
])))
236+
} else {
237+
redirect(302, &[format!("/redirect/delayed/{}", left - 1)])
238+
};
239+
}
240+
if let Some(left) = path.strip_prefix("/redirect/chain/") {
241+
let left: u8 = left.parse()?;
242+
return if left == 0 {
243+
Ok(Response::new(full("final")))
244+
} else {
245+
redirect(302, &[format!("/redirect/chain/{}", left - 1)])
246+
};
247+
}
248+
match path.as_str() {
249+
"/redirect/non-utf8" => {
250+
return Ok(Response::builder()
251+
.status(302)
252+
.header(
253+
"Location",
254+
hyper::header::HeaderValue::from_bytes(b"/bad\xff")?,
255+
)
256+
.body(full("invalid location"))?)
257+
}
258+
"/redirect/unicode" => {
259+
return Ok(Response::builder()
260+
.status(302)
261+
.header(
262+
"Location",
263+
hyper::header::HeaderValue::from_bytes(
264+
"/redirect/inspect?name=caf\u{e9}".as_bytes(),
265+
)?,
266+
)
267+
.body(full("unicode location"))?)
268+
}
269+
"/redirect/missing" => {
270+
return Ok(Response::builder()
271+
.status(302)
272+
.body(full("missing-location"))?)
273+
}
274+
"/redirect/duplicate" => {
275+
return redirect(302, &["/redirect/final".into(), "/not-found".into()])
276+
}
277+
"/redirect/malformed" => return redirect(302, &["http://[".into()]),
278+
"/redirect/unsupported" => return redirect(302, &["ftp://example.com/file".into()]),
279+
"/redirect/not-found" => return redirect(302, &["/not-found".into()]),
280+
"/redirect/loop" => return redirect(302, &["/redirect/loop".into()]),
281+
"/redirect/fragment" => return redirect(302, &["/redirect/inspect#never-send".into()]),
282+
_ => {}
283+
}
284+
let segments: Vec<_> = path.trim_start_matches('/').split('/').collect();
285+
if let ["redirect", status, target] = segments.as_slice() {
286+
let status: u16 = status.parse()?;
287+
let location = match *target {
288+
"echo" => "/redirect/inspect",
289+
"relative" => "../inspect",
290+
"cross" => "http://localhost:9000/redirect/inspect",
291+
"network" => "//localhost:9000/redirect/inspect",
292+
_ => {
293+
return Ok(Response::builder()
294+
.status(404)
295+
.body(full("unknown redirect target"))?)
296+
}
297+
};
298+
return redirect(status, &[location.to_owned()]);
299+
}
300+
Ok(Response::builder()
301+
.status(404)
302+
.body(full("unknown redirect route"))?)
303+
}
304+
172305
#[tokio::main]
173306
async fn main() -> Result<(), GenericError> {
174307
// `examples/http-client.roc` reaches this server by host name so the platform

‎ci/valgrind.supp‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Statically linked x64musl binaries use musl's mallocng allocator directly.
1+
# C dependencies in static x64musl binaries can still use musl mallocng.
22
# Ignore its encoded-metadata check while retaining reports from every caller.
33
{
44
musl-mallocng-enframe-uninitialised

0 commit comments

Comments
 (0)