Skip to content

Commit 77384cd

Browse files
olwangclaude
andcommitted
Fix VM async select/task_group parity; add http-server and sqlx native packages
VM (reg_vm + hir): - Cancel losing select arm tasks once a winner is chosen, so they stop running, produce no further side effects, and cannot abort the program with a late error. - Drain un-awaited task_group async-lets at scope exit (synthesized awaits), matching the compiled backend's structured-concurrency drain. - Regression tests in tests/vm.rs. Packages: - rss-core-http-server: native tiny_http static-route server (was an empty virtual scaffold), interface + bindings + native crate + example. - rss-sqlx: sqlx Any adapter (SQLite + Postgres), URL-based, async exposed as blocking native fns via a current-thread Tokio runtime. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 98053f7 commit 77384cd

19 files changed

Lines changed: 1430 additions & 24 deletions

File tree

Cargo.lock

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

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@ members = [
1212
"reir",
1313
"rss/core/cli/native/rust",
1414
"rss/core/crypto/native/rust",
15+
"rss/core/http-server/native/rust",
1516
"rss/rayon/native/rust",
1617
"rss/adapters/sqlite/native/rust",
18+
"rss/adapters/sqlx/native/rust",
1719
]
1820

1921
[[bin]]
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
features: native
2+
3+
// Async SQL access via sqlx, exposed as blocking calls. `url` is a sqlx
4+
// connection string whose scheme selects the backend (`sqlite:` or
5+
// `postgres:`), e.g. `sqlite:///tmp/app.db?mode=rwc` or
6+
// `postgres://user:pw@host/db`. Each call opens a connection, runs the SQL, and
7+
// closes it. Queries read the first column of each row as text, so that column
8+
// must be a text/varchar value.
9+
10+
native fn Sqlx.execute(url: read String, sql: read String) -> Result<Unit, String>
11+
effects(native)
12+
13+
native fn Sqlx.query_strings(url: read String, sql: read String) -> Result<fresh List<String>, String>
14+
effects(native)
15+
16+
native fn Sqlx.query_one_string(url: read String, sql: read String) -> Result<Option<String>, String>
17+
effects(native)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[bindings]
2+
"Sqlx.execute" = "rss_sqlx_native::execute"
3+
"Sqlx.query_strings" = "rss_sqlx_native::query_strings"
4+
"Sqlx.query_one_string" = "rss_sqlx_native::query_one_string"
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[package]
2+
name = "rss_sqlx_native"
3+
version = "0.1.0"
4+
edition = "2024"
5+
6+
[dependencies]
7+
sqlx = { version = "0.8", default-features = false, features = [
8+
"runtime-tokio",
9+
"tls-rustls-ring",
10+
"any",
11+
"sqlite",
12+
"postgres",
13+
] }
14+
tokio = { version = "1", features = ["rt", "net", "time"] }
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
use std::sync::Once;
2+
3+
use sqlx::{AnyConnection, Connection, Row};
4+
use tokio::runtime::Builder;
5+
6+
static INSTALL_DRIVERS: Once = Once::new();
7+
8+
/// Run a sqlx future to completion on a fresh current-thread Tokio runtime, so
9+
/// the async sqlx API can be exposed through synchronous `native fn` bindings.
10+
/// sqlx's `Any` backend drivers are installed once on first use.
11+
fn block_on<T, F>(future: F) -> Result<T, String>
12+
where
13+
F: std::future::Future<Output = Result<T, String>>,
14+
{
15+
INSTALL_DRIVERS.call_once(sqlx::any::install_default_drivers);
16+
let runtime = Builder::new_current_thread()
17+
.enable_all()
18+
.build()
19+
.map_err(|error| error.to_string())?;
20+
runtime.block_on(future)
21+
}
22+
23+
/// Run one or more SQL statements that produce no rows (DDL, `INSERT`, ...).
24+
pub fn execute(url: &str, sql: &str) -> Result<(), String> {
25+
block_on(async {
26+
let mut conn = AnyConnection::connect(url)
27+
.await
28+
.map_err(|error| error.to_string())?;
29+
let result = sqlx::raw_sql(sql)
30+
.execute(&mut conn)
31+
.await
32+
.map(|_| ())
33+
.map_err(|error| error.to_string());
34+
let _ = conn.close().await;
35+
result
36+
})
37+
}
38+
39+
/// Run a query and collect the first column of every row as a string.
40+
pub fn query_strings(url: &str, sql: &str) -> Result<Vec<String>, String> {
41+
block_on(async {
42+
let mut conn = AnyConnection::connect(url)
43+
.await
44+
.map_err(|error| error.to_string())?;
45+
let result = collect_first_column(&mut conn, sql).await;
46+
let _ = conn.close().await;
47+
result
48+
})
49+
}
50+
51+
/// Run a query and return the first column of the first row, if any.
52+
pub fn query_one_string(url: &str, sql: &str) -> Result<Option<String>, String> {
53+
let values = query_strings(url, sql)?;
54+
Ok(values.into_iter().next())
55+
}
56+
57+
async fn collect_first_column(conn: &mut AnyConnection, sql: &str) -> Result<Vec<String>, String> {
58+
let rows = sqlx::query(sql)
59+
.fetch_all(conn)
60+
.await
61+
.map_err(|error| error.to_string())?;
62+
let mut values = Vec::with_capacity(rows.len());
63+
for row in &rows {
64+
let value: String = row.try_get(0).map_err(|error| error.to_string())?;
65+
values.push(value);
66+
}
67+
Ok(values)
68+
}
69+
70+
#[cfg(test)]
71+
mod tests {
72+
#[test]
73+
fn executes_and_queries_over_sqlite() {
74+
let path = std::env::temp_dir().join(format!("rss-sqlx-{}.db", std::process::id()));
75+
let _ = std::fs::remove_file(&path);
76+
let url = format!("sqlite://{}?mode=rwc", path.display());
77+
78+
super::execute(
79+
&url,
80+
"create table item(name text); insert into item values ('a'), ('b');",
81+
)
82+
.expect("sqlite setup should work");
83+
84+
assert_eq!(
85+
super::query_strings(&url, "select name from item order by name")
86+
.expect("query should work"),
87+
vec!["a".to_string(), "b".to_string()]
88+
);
89+
assert_eq!(
90+
super::query_one_string(&url, "select name from item order by name")
91+
.expect("query should work"),
92+
Some("a".to_string())
93+
);
94+
95+
let _ = std::fs::remove_file(path);
96+
}
97+
98+
#[test]
99+
fn reports_connection_errors_as_strings() {
100+
let error = super::execute("postgres://127.0.0.1:1/missing", "select 1")
101+
.expect_err("connecting to a dead postgres should fail");
102+
assert!(!error.is_empty());
103+
}
104+
}

rss/adapters/sqlx/rsspkg.lock

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
version = 1
2+
3+
[[package]]
4+
name = "rss-sqlx"
5+
version = "0.1.0"
6+
source = "path+rss/adapters/sqlx"
7+
checksum = "sha256:c908eebf5eef6ba7b2b5579fdc0716547738801194ae661f1632b971bcd76050"
8+
interface_hash = "sha256:b77070da595cb161ea15b085dedc7d789f906d9f3cf81f843d440e1395d247e5"
9+
review_hash = "sha256:65bffcd286c36aed5edcfebb254d4d1c183c5c36c1f06e95de13b2a5521947a6"
10+
native_hash = "sha256:0427cf5af1e2140e6db183da37b9e46ccf9b3791298f2d95b289639301bfe6ef"
11+
features = []
12+
13+
[metadata]
14+
rss_version = "0.1.0"
15+
created_by = "rsscript pkg"

rss/adapters/sqlx/rsspkg.toml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
[package]
2+
name = "rss-sqlx"
3+
version = "0.1.0"
4+
edition = "2026"
5+
6+
[interfaces]
7+
paths = ["interface"]
8+
9+
[native.rust]
10+
enabled = true
11+
path = "native/rust"
12+
crate = "rss_sqlx_native"
13+
build_scripts = "forbid"
14+
proc_macros = "forbid"
15+
unsafe = "forbid"
16+
17+
[[review.capability_bindings]]
18+
symbol = "Sqlx.execute"
19+
category = "database.write"
20+
provider = "sqlx"
21+
service = "sql"
22+
action = "execute"
23+
resource = "url"
24+
25+
[[review.capability_bindings]]
26+
symbol = "Sqlx.query_strings"
27+
category = "database.read"
28+
provider = "sqlx"
29+
service = "sql"
30+
action = "select"
31+
resource = "url"
32+
33+
[[review.capability_bindings]]
34+
symbol = "Sqlx.query_one_string"
35+
category = "database.read"
36+
provider = "sqlx"
37+
service = "sql"
38+
action = "select"
39+
resource = "url"
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[package]
2+
name = "rss-core-http-server-serve-hello"
3+
version = "0.1.0"
4+
edition = "2026"
5+
6+
[sources]
7+
paths = ["src"]
8+
9+
[dependencies]
10+
rss-core-http-server = { path = "../.." }
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
features: native, local
2+
3+
// Serve two fixed routes on localhost:8080. `serve_static` blocks, serving
4+
// requests until a fatal error, so a successful bind never returns `Ok`.
5+
6+
fn build_paths() -> fresh List<String> {
7+
local paths = List.new<String>()
8+
List.push<String>(list: mut paths, value: read "/hello")
9+
List.push<String>(list: mut paths, value: read "/health")
10+
return paths
11+
}
12+
13+
fn build_bodies() -> fresh List<String> {
14+
local bodies = List.new<String>()
15+
List.push<String>(list: mut bodies, value: read "Hello, world!")
16+
List.push<String>(list: mut bodies, value: read "ok")
17+
return bodies
18+
}
19+
20+
fn main() -> Result<Unit, String> {
21+
local paths = build_paths()
22+
local bodies = build_bodies()
23+
return HttpServer.serve_static(
24+
address: read "127.0.0.1:8080",
25+
paths: read paths,
26+
bodies: read bodies,
27+
)
28+
}

0 commit comments

Comments
 (0)