Skip to content

Commit 3f4391e

Browse files
committed
Add runnable ImageCache runtime hooks
1 parent 08a2671 commit 3f4391e

8 files changed

Lines changed: 191 additions & 27 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -412,13 +412,13 @@ rss verify-rust [--json] <file.rss> --out-dir <directory>
412412

413413
`rss check` loads bundled core `.rssi` signatures by default. Use `--no-core` only when testing a file against user-supplied interfaces in isolation.
414414

415-
The checker prototype also keeps a small `core/prototype` interface bundle for real-scenario fixtures such as CSV, DB, cache, and config examples. Those signatures are still `.rssi` sources, not a second Rust-side builtin table.
415+
The checker prototype also keeps a small `core/prototype` interface bundle for real-scenario fixtures that have not yet moved into `core/`. Those signatures are still `.rssi` sources, not a second Rust-side builtin table.
416416

417417
If a lowered package contains `fn main() -> Unit` or `fn main() -> Result<Unit, E>`, `rss lower --rust --out-dir` also emits a Rust `src/main.rs` harness. `rss run <file.rss>` uses the same lowering path, writes a temporary Rust package, and delegates execution to `cargo run`. Use `rss run <file.rss> --out-dir <directory>` to keep the generated Rust package for inspection.
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`, HTTP handler, DB resource-pool, config reload, and `Counter` 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`, `ImageCache`, HTTP handler, DB resource-pool, config reload, and `Counter` APIs, which lower to explicit runtime hooks. `ImageCache` is the first small retained managed-container hook: it stores managed image handles and enforces a capacity limit. 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/cache/image_cache.rssi

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
struct ImageCache
2+
3+
pub fn ImageCache.new(capacity: Int) -> fresh ImageCache
4+
5+
pub fn ImageCache.store(
6+
cache: mut ImageCache,
7+
image: read Image,
8+
) -> Unit
9+
effects(retains(image))
10+
11+
pub fn ImageCache.len(cache: read ImageCache) -> Int

core/prototype/builtins.rssi

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,5 @@
11
pub fn OS.close(fd: Fd) -> Unit
22

3-
pub fn ImageCache.store(
4-
cache: mut ImageCache,
5-
image: read Image,
6-
) -> Unit
7-
effects(retains(image))
8-
93
pub fn List.consume(list: take List) -> Unit
104

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

examples/image_cache.rss

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
features: local
2+
3+
fn main() -> Result<Unit, ImageError> {
4+
let input = "rsscript-cache-image-input.bin"
5+
let output = "rsscript-cache-image-output.bin"
6+
7+
with File.open_write(path: read input) as file {
8+
File.write(file: mut file, data: read "cached image bytes")?
9+
}
10+
11+
local cache = ImageCache.new(capacity: 1)
12+
local image = Image.load(path: read input)?
13+
Image.resize(image: mut image, width: 128, height: 128)
14+
15+
let shared = manage image
16+
ImageCache.store(cache: mut cache, image: read shared)
17+
Image.save(image: read shared, path: read output)?
18+
19+
let count = ImageCache.len(cache: read cache)
20+
if count == 1 {
21+
Log.write(message: read "image cache retained managed image")
22+
}
23+
24+
return Ok(Unit)
25+
}

runtime/src/lib.rs

Lines changed: 95 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use std::cell::{BorrowError, BorrowMutError, Ref, RefCell, RefMut};
2+
use std::collections::VecDeque;
23
use std::fmt;
34
use std::io::{Read, Write};
45
use std::ops::{Deref, DerefMut};
@@ -121,6 +122,12 @@ pub struct Image {
121122
operations: Vec<&'static str>,
122123
}
123124

125+
#[derive(Debug, Clone)]
126+
pub struct ImageCache {
127+
capacity: usize,
128+
entries: VecDeque<Gc<Image>>,
129+
}
130+
124131
#[derive(Debug, Clone, PartialEq, Eq)]
125132
pub struct ImageError {
126133
message: String,
@@ -148,6 +155,23 @@ impl From<std::io::Error> for ImageError {
148155
}
149156
}
150157

158+
pub trait RuntimeImageRef {
159+
fn with_image<R>(&self, f: impl FnOnce(&Image) -> R) -> R;
160+
}
161+
162+
impl RuntimeImageRef for Image {
163+
fn with_image<R>(&self, f: impl FnOnce(&Image) -> R) -> R {
164+
f(self)
165+
}
166+
}
167+
168+
impl RuntimeImageRef for Gc<Image> {
169+
fn with_image<R>(&self, f: impl FnOnce(&Image) -> R) -> R {
170+
let image = self.read();
171+
f(&image)
172+
}
173+
}
174+
151175
#[derive(Debug, Clone)]
152176
pub struct JsonValue {
153177
inner: serde_json::Value,
@@ -411,29 +435,59 @@ pub fn image_sharpen(image: &mut Image) {
411435
image.operations.push("sharpen");
412436
}
413437

414-
pub fn image_save<P: RuntimePath + ?Sized>(image: &Image, path: &P) -> Result<(), ImageError> {
415-
let mut bytes = image.bytes.clone();
416-
bytes.extend_from_slice(b"\n# rsscript-image-ops:");
417-
bytes.extend_from_slice(image.operations.join(",").as_bytes());
418-
if let (Some(width), Some(height)) = (image.width, image.height) {
419-
bytes.extend_from_slice(format!(";size={width}x{height}").as_bytes());
420-
}
438+
pub fn image_save<I: RuntimeImageRef + ?Sized, P: RuntimePath + ?Sized>(
439+
image: &I,
440+
path: &P,
441+
) -> Result<(), ImageError> {
442+
let bytes = image.with_image(|image| {
443+
let mut bytes = image.bytes.clone();
444+
bytes.extend_from_slice(b"\n# rsscript-image-ops:");
445+
bytes.extend_from_slice(image.operations.join(",").as_bytes());
446+
if let (Some(width), Some(height)) = (image.width, image.height) {
447+
bytes.extend_from_slice(format!(";size={width}x{height}").as_bytes());
448+
}
449+
bytes
450+
});
421451
std::fs::write(path.as_path(), bytes)?;
422452
Ok(())
423453
}
424454

425-
pub fn image_inspect(image: &Image) {
426-
let size = image
427-
.width
428-
.zip(image.height)
429-
.map(|(width, height)| format!("{width}x{height}"))
430-
.unwrap_or_else(|| "unknown".to_string());
431-
println!(
432-
"image bytes={} size={} ops={}",
433-
image.bytes.len(),
434-
size,
435-
image.operations.join(",")
436-
);
455+
pub fn image_inspect<I: RuntimeImageRef + ?Sized>(image: &I) {
456+
let summary = image.with_image(|image| {
457+
let size = image
458+
.width
459+
.zip(image.height)
460+
.map(|(width, height)| format!("{width}x{height}"))
461+
.unwrap_or_else(|| "unknown".to_string());
462+
format!(
463+
"image bytes={} size={} ops={}",
464+
image.bytes.len(),
465+
size,
466+
image.operations.join(",")
467+
)
468+
});
469+
println!("{summary}");
470+
}
471+
472+
pub fn image_cache_new(capacity: i64) -> ImageCache {
473+
ImageCache {
474+
capacity: capacity.max(0) as usize,
475+
entries: VecDeque::new(),
476+
}
477+
}
478+
479+
pub fn image_cache_store(cache: &mut ImageCache, image: &Gc<Image>) {
480+
if cache.capacity == 0 {
481+
return;
482+
}
483+
while cache.entries.len() >= cache.capacity {
484+
cache.entries.pop_front();
485+
}
486+
cache.entries.push_back(image.clone());
487+
}
488+
489+
pub fn image_cache_len(cache: &ImageCache) -> i64 {
490+
cache.entries.len() as i64
437491
}
438492

439493
pub fn json_parse(text: &str) -> Result<JsonValue, JsonError> {
@@ -953,6 +1007,28 @@ mod tests {
9531007
let _ = std::fs::remove_file(output);
9541008
}
9551009

1010+
#[test]
1011+
fn image_cache_runtime_hooks_retain_managed_images_with_capacity() {
1012+
let mut cache = super::image_cache_new(1);
1013+
let first = super::manage(super::Image {
1014+
bytes: b"first".to_vec(),
1015+
width: None,
1016+
height: None,
1017+
operations: vec!["test"],
1018+
});
1019+
let second = super::manage(super::Image {
1020+
bytes: b"second".to_vec(),
1021+
width: None,
1022+
height: None,
1023+
operations: vec!["test"],
1024+
});
1025+
1026+
super::image_cache_store(&mut cache, &first);
1027+
super::image_cache_store(&mut cache, &second);
1028+
1029+
assert_eq!(super::image_cache_len(&cache), 1);
1030+
}
1031+
9561032
#[test]
9571033
fn http_runtime_hooks_create_request_and_response() {
9581034
let request = super::request_new("/users");

src/interfaces.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
pub(crate) const CORE_INTERFACES: &[(&str, &str)] = &[
2+
(
3+
"core/cache/image_cache.rssi",
4+
include_str!("../core/cache/image_cache.rssi"),
5+
),
26
(
37
"core/collections/map.rssi",
48
include_str!("../core/collections/map.rssi"),

src/rust_lower.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -824,6 +824,7 @@ impl<'a> RustLowerer<'a> {
824824
"DbConnection" => "rsscript_runtime::DbConnection".to_string(),
825825
"DbError" => "rsscript_runtime::DbError".to_string(),
826826
"Image" => "rsscript_runtime::Image".to_string(),
827+
"ImageCache" => "rsscript_runtime::ImageCache".to_string(),
827828
"ImageError" => "rsscript_runtime::ImageError".to_string(),
828829
"JsonValue" => "rsscript_runtime::JsonValue".to_string(),
829830
"JsonError" => "rsscript_runtime::JsonError".to_string(),
@@ -1228,6 +1229,15 @@ fn lower_callee(callee: &Callee) -> String {
12281229
callee if is_image_sharpen_callee(callee) => "rsscript_runtime::image_sharpen".to_string(),
12291230
callee if is_image_save_callee(callee) => "rsscript_runtime::image_save".to_string(),
12301231
callee if is_image_inspect_callee(callee) => "rsscript_runtime::image_inspect".to_string(),
1232+
callee if is_image_cache_new_callee(callee) => {
1233+
"rsscript_runtime::image_cache_new".to_string()
1234+
}
1235+
callee if is_image_cache_store_callee(callee) => {
1236+
"rsscript_runtime::image_cache_store".to_string()
1237+
}
1238+
callee if is_image_cache_len_callee(callee) => {
1239+
"rsscript_runtime::image_cache_len".to_string()
1240+
}
12311241
callee if is_json_parse_callee(callee) => "rsscript_runtime::json_parse".to_string(),
12321242
callee if is_json_field_callee(callee) => "rsscript_runtime::json_field".to_string(),
12331243
callee if is_json_field_string_callee(callee) => {
@@ -1371,6 +1381,18 @@ fn is_image_inspect_callee(callee: &Callee) -> bool {
13711381
matches!(callee, Callee::Qualified { namespace, name } if type_root_name(namespace) == "Image" && name == "inspect")
13721382
}
13731383

1384+
fn is_image_cache_new_callee(callee: &Callee) -> bool {
1385+
matches!(callee, Callee::Qualified { namespace, name } if type_root_name(namespace) == "ImageCache" && name == "new")
1386+
}
1387+
1388+
fn is_image_cache_store_callee(callee: &Callee) -> bool {
1389+
matches!(callee, Callee::Qualified { namespace, name } if type_root_name(namespace) == "ImageCache" && name == "store")
1390+
}
1391+
1392+
fn is_image_cache_len_callee(callee: &Callee) -> bool {
1393+
matches!(callee, Callee::Qualified { namespace, name } if type_root_name(namespace) == "ImageCache" && name == "len")
1394+
}
1395+
13741396
fn is_json_parse_callee(callee: &Callee) -> bool {
13751397
matches!(callee, Callee::Qualified { namespace, name } if type_root_name(namespace) == "Json" && name == "parse")
13761398
}

tests/checker.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,11 @@ fn bundled_core_interfaces_are_available_to_checker() {
8787
.iter()
8888
.any(|(path, _)| *path == "core/log/log.rssi")
8989
);
90+
assert!(
91+
core_interfaces()
92+
.iter()
93+
.any(|(path, _)| *path == "core/cache/image_cache.rssi")
94+
);
9095
assert!(
9196
core_interfaces()
9297
.iter()
@@ -410,6 +415,33 @@ fn process(input: read Path, output: read Path) -> Result<Unit, ImageError> {
410415
assert!(rust.contains("rsscript_runtime::image_save(&image, &output)?;"));
411416
}
412417

418+
#[test]
419+
fn rust_lowering_maps_image_cache_core_calls_to_runtime_hooks() {
420+
let source = r#"
421+
features: local
422+
423+
fn cache_image(input: read Path, output: read Path) -> Result<Unit, ImageError> {
424+
local cache = ImageCache.new(capacity: 1)
425+
local image = Image.load(path: read input)?
426+
let shared = manage image
427+
ImageCache.store(cache: mut cache, image: read shared)
428+
Image.save(image: read shared, path: read output)?
429+
let count = ImageCache.len(cache: read cache)
430+
if count == 1 {
431+
Log.write(message: read "cached")
432+
}
433+
return Ok(Unit)
434+
}
435+
"#;
436+
let rust = lower_source_to_rust("image-cache.rss", source).expect("source should lower");
437+
438+
assert!(rust.contains("let mut cache = rsscript_runtime::image_cache_new(1);"));
439+
assert!(rust.contains("let shared = rsscript_runtime::manage_at(image,"));
440+
assert!(rust.contains("rsscript_runtime::image_cache_store(&mut cache, &shared);"));
441+
assert!(rust.contains("rsscript_runtime::image_save(&shared, &output)?;"));
442+
assert!(rust.contains("let count = rsscript_runtime::image_cache_len(&cache);"));
443+
}
444+
413445
#[test]
414446
fn rust_lowering_maps_http_handler_core_calls_to_runtime_hooks() {
415447
let source = r#"

0 commit comments

Comments
 (0)