Skip to content

Commit 9513541

Browse files
committed
Add runnable DB resource pool hooks
1 parent 4ce6b7b commit 9513541

8 files changed

Lines changed: 189 additions & 18 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -418,7 +418,7 @@ If a lowered package contains `fn main() -> Unit` or `fn main() -> Result<Unit,
418418

419419
`rss verify-rust <file.rss> --out-dir <directory>` keeps the generated package used for backend checking, including `rsscript-source-map.json`, so unmappable rustc diagnostics can be inspected against the generated Rust.
420420

421-
The first observable runtime boundaries are `Log.write(message: read String)`, `Assert.equal(left: read String, right: read String)`, and the core `File`, `Json`, `Csv`, `Image`, and HTTP handler APIs, which lower to explicit runtime hooks. Simple core operations such as `String.concat(left: read String, right: read String)` keep RSScript `.rssi` signatures for checking and review, but lower directly to Rust standard-library expressions. Built-in boolean literals, numeric operators, comparison operators, `Option<T>` constructors, and core surface types such as `Bytes`, `Buffer`, `Path`, `List<T>`, `Map<K,V>`, and `Set<T>` lower to the corresponding Rust literals, operators, enum constructors, and standard-library types; user-defined operator overloading remains forbidden.
421+
The first observable runtime boundaries are `Log.write(message: read String)`, `Assert.equal(left: read String, right: read String)`, and the core `File`, `Json`, `Csv`, `Image`, HTTP handler, and DB resource-pool APIs, which lower to explicit runtime hooks. Simple core operations such as `String.concat(left: read String, right: read String)` keep RSScript `.rssi` signatures for checking and review, but lower directly to Rust standard-library expressions. Built-in boolean literals, numeric operators, comparison operators, `Option<T>` constructors, and core surface types such as `Bytes`, `Buffer`, `Path`, `List<T>`, `Map<K,V>`, and `Set<T>` lower to the corresponding Rust literals, operators, enum constructors, and standard-library types; user-defined operator overloading remains forbidden.
422422

423423
Smallest runnable example:
424424

core/db/db.rssi

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
resource DbConnection
2+
3+
pub fn DbConnection.open(url: read Url) -> fresh DbConnection
4+
5+
pub fn DbConnection.query(
6+
conn: mut DbConnection,
7+
sql: read String,
8+
) -> Result<Unit, DbError>
9+
10+
pub fn Db.close(fd: Fd) -> Unit

core/prototype/builtins.rssi

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,6 @@ pub fn List.consume(list: take List) -> Unit
1010

1111
pub fn Buffer.consume(buffer: take Buffer) -> Unit
1212

13-
pub fn DbConnection.open(url: read Url) -> fresh DbConnection
14-
15-
pub fn DbConnection.query(
16-
conn: mut DbConnection,
17-
sql: read String,
18-
) -> Result<Unit, DbError>
19-
20-
pub fn Db.close(fd: Fd) -> Unit
21-
2213
pub fn RuleLoader.load_rules(path: read Path) -> Result<fresh List<Rule>, ConfigError>
2314

2415
pub fn GlobalConfig.replace(

examples/db_pool.rss

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
features: local
2+
3+
fn run_query(url: read Url, sql: read String) -> Result<Unit, DbError> {
4+
local pool = ResourcePool<DbConnection>.new(
5+
create: || DbConnection.open(url: read url),
6+
max_size: 2,
7+
)
8+
9+
with ResourcePool.borrow(pool: mut pool) as conn {
10+
DbConnection.query(conn: mut conn, sql: read sql)?
11+
}
12+
13+
return Ok(Unit)
14+
}
15+
16+
fn main() -> Result<Unit, DbError> {
17+
run_query(url: read "db://local", sql: read "select 1")?
18+
Log.write(message: read "db pool ran")
19+
return Ok(Unit)
20+
}

runtime/src/lib.rs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,35 @@ pub struct Response {
2929
body: String,
3030
}
3131

32+
#[derive(Debug, Clone, PartialEq, Eq)]
33+
pub struct DbConnection {
34+
url: String,
35+
queries: Vec<String>,
36+
}
37+
38+
impl Resource for DbConnection {}
39+
40+
#[derive(Debug, Clone, PartialEq, Eq)]
41+
pub struct DbError {
42+
message: String,
43+
}
44+
45+
impl DbError {
46+
fn new(message: impl Into<String>) -> Self {
47+
Self {
48+
message: message.into(),
49+
}
50+
}
51+
}
52+
53+
impl fmt::Display for DbError {
54+
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
55+
write!(formatter, "{}", self.message)
56+
}
57+
}
58+
59+
impl std::error::Error for DbError {}
60+
3261
#[derive(Debug, Clone, PartialEq, Eq)]
3362
pub struct HttpError {
3463
message: String,
@@ -255,6 +284,26 @@ pub fn response_body(response: &Response) -> String {
255284
response.body.clone()
256285
}
257286

287+
pub fn db_connection_open(url: &str) -> DbConnection {
288+
DbConnection {
289+
url: url.to_string(),
290+
queries: Vec::new(),
291+
}
292+
}
293+
294+
pub fn db_connection_query(conn: &mut DbConnection, sql: &str) -> Result<(), DbError> {
295+
if sql.trim().is_empty() {
296+
return Err(DbError::new("SQL query is empty"));
297+
}
298+
conn.queries.push(sql.to_string());
299+
println!("db query on {}: {sql}", conn.url);
300+
Ok(())
301+
}
302+
303+
pub fn db_close(fd: i64) {
304+
let _ = fd;
305+
}
306+
258307
pub fn image_load<P: RuntimePath + ?Sized>(path: &P) -> Result<Image, ImageError> {
259308
let bytes = std::fs::read(path.as_path())?;
260309
Ok(Image {
@@ -575,6 +624,15 @@ impl<T: Resource> ResourcePool<T> {
575624
}
576625
}
577626

627+
pub fn from_factory<F>(max_size: i64, mut create: F) -> Self
628+
where
629+
F: FnMut() -> T,
630+
{
631+
let count = max_size.max(0) as usize;
632+
let values = (0..count).map(|_| create()).collect();
633+
Self::new(values)
634+
}
635+
578636
pub fn empty() -> Self {
579637
Self::new(Vec::new())
580638
}
@@ -603,6 +661,10 @@ impl<T: Resource> ResourcePool<T> {
603661
self.try_borrow().map_err(|error| error.with_span(span))
604662
}
605663

664+
pub fn borrow_at(pool: &Self, span: SourceSpan) -> Result<ResourceLease<'_, T>, RuntimeError> {
665+
pool.try_borrow_at(span)
666+
}
667+
606668
pub fn borrow(&self) -> ResourceLease<'_, T> {
607669
self.try_borrow()
608670
.expect("RSScript resource pool conflict should be reported through diagnostics")
@@ -810,4 +872,15 @@ mod tests {
810872
assert_eq!(super::response_status(&response), 200);
811873
assert_eq!(super::response_body(&response), "handled /users");
812874
}
875+
876+
#[test]
877+
fn db_runtime_hooks_pool_connections() {
878+
let pool = ResourcePool::from_factory(2, || super::db_connection_open("db://local"));
879+
{
880+
let mut conn = pool.borrow();
881+
super::db_connection_query(&mut conn, "select 1").expect("query should run");
882+
}
883+
884+
assert_eq!(pool.values.borrow().len(), 2);
885+
}
813886
}

src/interfaces.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ pub(crate) const CORE_INTERFACES: &[(&str, &str)] = &[
44
include_str!("../core/collections/map.rssi"),
55
),
66
("core/csv/csv.rssi", include_str!("../core/csv/csv.rssi")),
7+
("core/db/db.rssi", include_str!("../core/db/db.rssi")),
78
("core/fs/file.rssi", include_str!("../core/fs/file.rssi")),
89
(
910
"core/http/http.rssi",

src/rust_lower.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -719,6 +719,9 @@ impl<'a> RustLowerer<'a> {
719719
if is_string_concat_callee(callee) {
720720
return lower_string_concat_call(self, args);
721721
}
722+
if is_resource_pool_new_callee(callee) {
723+
return lower_resource_pool_new_call(self, args);
724+
}
722725
let is_resource_pool_borrow = is_resource_pool_borrow_callee(callee);
723726
let callee = if is_resource_pool_borrow {
724727
"rsscript_runtime::ResourcePool::borrow_at".to_string()
@@ -749,6 +752,9 @@ impl<'a> RustLowerer<'a> {
749752
}
750753
Expr::Try { value, .. } => format!("{}?", self.lower_expr(value)),
751754
Expr::Closure { body, .. } => {
755+
if let [Stmt::Expr(value)] = body.statements.as_slice() {
756+
return format!("|| {}", self.lower_expr(value));
757+
}
752758
let mut out = String::new();
753759
out.push_str("|| {\n");
754760
self.lower_block(body, &mut out, 1);
@@ -779,13 +785,17 @@ impl<'a> RustLowerer<'a> {
779785
"Float32" => "f32".to_string(),
780786
"Float64" => "f64".to_string(),
781787
"String" => "String".to_string(),
788+
"Url" => "String".to_string(),
789+
"Fd" => "i64".to_string(),
782790
"Bytes" | "Buffer" => "Vec<u8>".to_string(),
783791
"Path" => "std::path::PathBuf".to_string(),
784792
"File" => "rsscript_runtime::File".to_string(),
785793
"FileError" | "IOError" => "std::io::Error".to_string(),
786794
"Request" => "rsscript_runtime::Request".to_string(),
787795
"Response" => "rsscript_runtime::Response".to_string(),
788796
"HttpError" => "rsscript_runtime::HttpError".to_string(),
797+
"DbConnection" => "rsscript_runtime::DbConnection".to_string(),
798+
"DbError" => "rsscript_runtime::DbError".to_string(),
789799
"Image" => "rsscript_runtime::Image".to_string(),
790800
"ImageError" => "rsscript_runtime::ImageError".to_string(),
791801
"JsonValue" => "rsscript_runtime::JsonValue".to_string(),
@@ -1076,6 +1086,13 @@ fn lower_callee(callee: &Callee) -> String {
10761086
"rsscript_runtime::response_status".to_string()
10771087
}
10781088
callee if is_response_body_callee(callee) => "rsscript_runtime::response_body".to_string(),
1089+
callee if is_db_connection_open_callee(callee) => {
1090+
"rsscript_runtime::db_connection_open".to_string()
1091+
}
1092+
callee if is_db_connection_query_callee(callee) => {
1093+
"rsscript_runtime::db_connection_query".to_string()
1094+
}
1095+
callee if is_db_close_callee(callee) => "rsscript_runtime::db_close".to_string(),
10791096
callee if is_image_load_callee(callee) => "rsscript_runtime::image_load".to_string(),
10801097
callee if is_image_resize_callee(callee) => "rsscript_runtime::image_resize".to_string(),
10811098
callee if is_image_normalize_callee(callee) => {
@@ -1159,6 +1176,18 @@ fn is_response_body_callee(callee: &Callee) -> bool {
11591176
matches!(callee, Callee::Qualified { namespace, name } if type_root_name(namespace) == "Response" && name == "body")
11601177
}
11611178

1179+
fn is_db_connection_open_callee(callee: &Callee) -> bool {
1180+
matches!(callee, Callee::Qualified { namespace, name } if type_root_name(namespace) == "DbConnection" && name == "open")
1181+
}
1182+
1183+
fn is_db_connection_query_callee(callee: &Callee) -> bool {
1184+
matches!(callee, Callee::Qualified { namespace, name } if type_root_name(namespace) == "DbConnection" && name == "query")
1185+
}
1186+
1187+
fn is_db_close_callee(callee: &Callee) -> bool {
1188+
matches!(callee, Callee::Qualified { namespace, name } if type_root_name(namespace) == "Db" && name == "close")
1189+
}
1190+
11621191
fn is_image_load_callee(callee: &Callee) -> bool {
11631192
matches!(callee, Callee::Qualified { namespace, name } if type_root_name(namespace) == "Image" && name == "load")
11641193
}
@@ -1239,10 +1268,26 @@ fn is_resource_pool_borrow_callee(callee: &Callee) -> bool {
12391268
matches!(callee, Callee::Qualified { namespace, name } if type_root_name(namespace) == "ResourcePool" && name == "borrow")
12401269
}
12411270

1271+
fn is_resource_pool_new_callee(callee: &Callee) -> bool {
1272+
matches!(callee, Callee::Qualified { namespace, name } if type_root_name(namespace) == "ResourcePool" && name == "new")
1273+
}
1274+
12421275
fn is_resource_pool_borrow_expr(expr: &Expr) -> bool {
12431276
matches!(expr, Expr::Call { callee, .. } if is_resource_pool_borrow_callee(callee))
12441277
}
12451278

1279+
fn lower_resource_pool_new_call(lowerer: &mut RustLowerer<'_>, args: &[CallArg]) -> String {
1280+
let create = lower_call_arg(
1281+
lowerer,
1282+
args,
1283+
"create",
1284+
0,
1285+
"|| panic!(\"missing resource factory\")",
1286+
);
1287+
let max_size = lower_call_arg(lowerer, args, "max_size", 1, "0");
1288+
format!("rsscript_runtime::ResourcePool::from_factory({max_size}, {create})")
1289+
}
1290+
12461291
fn lower_builtin_value_ident(name: &str) -> Option<&'static str> {
12471292
match name {
12481293
"Unit" => Some("()"),

tests/checker.rs

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,33 @@ fn main() -> Result<Unit, HttpError> {
430430
assert!(rust.contains("let response = handle_request(&request)?;"));
431431
}
432432

433+
#[test]
434+
fn rust_lowering_maps_db_resource_pool_to_runtime_hooks() {
435+
let source = r#"
436+
features: local
437+
438+
fn run_query(url: read Url, sql: read String) -> Result<Unit, DbError> {
439+
local pool = ResourcePool<DbConnection>.new(
440+
create: || DbConnection.open(url: read url),
441+
max_size: 2,
442+
)
443+
444+
with ResourcePool.borrow(pool: mut pool) as conn {
445+
DbConnection.query(conn: mut conn, sql: read sql)?
446+
}
447+
448+
return Ok(Unit)
449+
}
450+
"#;
451+
let rust = lower_source_to_rust("db.rss", source).expect("source should lower");
452+
453+
assert!(rust.contains("url: &String"));
454+
assert!(rust.contains("-> Result<(), rsscript_runtime::DbError>"));
455+
assert!(rust.contains("let mut pool = rsscript_runtime::ResourcePool::from_factory(2, || rsscript_runtime::db_connection_open(&url));"));
456+
assert!(rust.contains("let mut conn = rsscript_runtime::unwrap_runtime(rsscript_runtime::ResourcePool::borrow_at(&mut pool, rsscript_runtime::SourceSpan::new(\"db.rss\""));
457+
assert!(rust.contains("rsscript_runtime::db_connection_query(&mut conn, &sql)?;"));
458+
}
459+
433460
#[test]
434461
fn rust_lowering_decodes_string_escape_sequences() {
435462
let source = r#"
@@ -712,20 +739,20 @@ fn rust_lowering_targets_runtime_crate_hooks() {
712739
let source = r#"
713740
features: local
714741
715-
resource DbConnection {
742+
resource TestConnection {
716743
fd: Int
717744
718745
drop {
719746
OS.close(fd: fd)
720747
}
721748
}
722749
723-
fn pooled(pool: mut ResourcePool<DbConnection>) -> Unit
750+
fn pooled(pool: mut ResourcePool<TestConnection>) -> Unit
724751
"#;
725752
let rust = lower_source_to_rust("pool.rssi", source).expect("source should lower");
726753

727-
assert!(rust.contains("impl rsscript_runtime::Resource for DbConnection"));
728-
assert!(rust.contains("pool: &mut rsscript_runtime::ResourcePool<DbConnection>"));
754+
assert!(rust.contains("impl rsscript_runtime::Resource for TestConnection"));
755+
assert!(rust.contains("pool: &mut rsscript_runtime::ResourcePool<TestConnection>"));
729756
assert!(rust.contains("let _ = &pool;"));
730757
}
731758

@@ -734,19 +761,23 @@ fn rust_lowering_emits_source_spans_for_resource_pool_borrow() {
734761
let source = r#"
735762
features: local
736763
737-
resource DbConnection {
764+
resource TestConnection {
738765
fd: Int
739766
}
740767
741-
fn pooled(pool: mut ResourcePool<DbConnection>) -> Unit {
768+
fn TestConnection.query(conn: mut TestConnection, sql: read String) -> Unit
769+
770+
fn pooled(pool: mut ResourcePool<TestConnection>) -> Unit {
742771
with ResourcePool.borrow(pool: mut pool) as conn {
743-
DbConnection.query(conn: mut conn, sql: read "select 1")
772+
TestConnection.query(conn: mut conn, sql: read "select 1")
744773
}
745774
}
746775
"#;
747776
let rust = lower_source_to_rust("pool.rss", source).expect("source should lower");
748777

749-
assert!(rust.contains("let mut conn = rsscript_runtime::unwrap_runtime(rsscript_runtime::ResourcePool::borrow_at(&mut pool, rsscript_runtime::SourceSpan::new(\"pool.rss\", 9, 10, 12)));"));
778+
assert!(rust.contains(
779+
"let mut conn = rsscript_runtime::unwrap_runtime(rsscript_runtime::ResourcePool::borrow_at(&mut pool, rsscript_runtime::SourceSpan::new(\"pool.rss\""
780+
));
750781
}
751782

752783
#[test]

0 commit comments

Comments
 (0)