Skip to content

Commit ffa7b96

Browse files
committed
Add runnable Image runtime hooks
1 parent a394cd9 commit ffa7b96

5 files changed

Lines changed: 190 additions & 1 deletion

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`, and `Csv` 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`, and `Image` 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

examples/image_pipeline.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 main() -> Result<Unit, ImageError> {
4+
let input = "rsscript-image-input.bin"
5+
let output = "rsscript-image-output.bin"
6+
7+
with File.open_write(path: read input) as file {
8+
File.write(file: mut file, data: read "raw image bytes")?
9+
}
10+
11+
local image = Image.load(path: read input)?
12+
Image.resize(image: mut image, width: 320, height: 240)
13+
Image.normalize(image: mut image)
14+
Image.sharpen(image: mut image)
15+
Image.inspect(image: read image)
16+
Image.save(image: read image, path: read output)?
17+
18+
Log.write(message: read "image pipeline ran")
19+
return Ok(Unit)
20+
}

runtime/src/lib.rs

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,41 @@ pub struct File {
1818

1919
impl Resource for File {}
2020

21+
#[derive(Debug, Clone)]
22+
pub struct Image {
23+
bytes: Vec<u8>,
24+
width: Option<i64>,
25+
height: Option<i64>,
26+
operations: Vec<&'static str>,
27+
}
28+
29+
#[derive(Debug, Clone, PartialEq, Eq)]
30+
pub struct ImageError {
31+
message: String,
32+
}
33+
34+
impl ImageError {
35+
fn new(message: impl Into<String>) -> Self {
36+
Self {
37+
message: message.into(),
38+
}
39+
}
40+
}
41+
42+
impl fmt::Display for ImageError {
43+
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
44+
write!(formatter, "{}", self.message)
45+
}
46+
}
47+
48+
impl std::error::Error for ImageError {}
49+
50+
impl From<std::io::Error> for ImageError {
51+
fn from(error: std::io::Error) -> Self {
52+
Self::new(error.to_string())
53+
}
54+
}
55+
2156
#[derive(Debug, Clone)]
2257
pub struct JsonValue {
2358
inner: serde_json::Value,
@@ -171,6 +206,55 @@ pub fn file_write<B: RuntimeBytes + ?Sized>(file: &mut File, data: &B) -> std::i
171206
file.inner.write_all(data.as_bytes_slice())
172207
}
173208

209+
pub fn image_load<P: RuntimePath + ?Sized>(path: &P) -> Result<Image, ImageError> {
210+
let bytes = std::fs::read(path.as_path())?;
211+
Ok(Image {
212+
bytes,
213+
width: None,
214+
height: None,
215+
operations: vec!["load"],
216+
})
217+
}
218+
219+
pub fn image_resize(image: &mut Image, width: i64, height: i64) {
220+
image.width = Some(width);
221+
image.height = Some(height);
222+
image.operations.push("resize");
223+
}
224+
225+
pub fn image_normalize(image: &mut Image) {
226+
image.operations.push("normalize");
227+
}
228+
229+
pub fn image_sharpen(image: &mut Image) {
230+
image.operations.push("sharpen");
231+
}
232+
233+
pub fn image_save<P: RuntimePath + ?Sized>(image: &Image, path: &P) -> Result<(), ImageError> {
234+
let mut bytes = image.bytes.clone();
235+
bytes.extend_from_slice(b"\n# rsscript-image-ops:");
236+
bytes.extend_from_slice(image.operations.join(",").as_bytes());
237+
if let (Some(width), Some(height)) = (image.width, image.height) {
238+
bytes.extend_from_slice(format!(";size={width}x{height}").as_bytes());
239+
}
240+
std::fs::write(path.as_path(), bytes)?;
241+
Ok(())
242+
}
243+
244+
pub fn image_inspect(image: &Image) {
245+
let size = image
246+
.width
247+
.zip(image.height)
248+
.map(|(width, height)| format!("{width}x{height}"))
249+
.unwrap_or_else(|| "unknown".to_string());
250+
println!(
251+
"image bytes={} size={} ops={}",
252+
image.bytes.len(),
253+
size,
254+
image.operations.join(",")
255+
);
256+
}
257+
174258
pub fn json_parse(text: &str) -> Result<JsonValue, JsonError> {
175259
serde_json::from_str(text)
176260
.map(|inner| JsonValue { inner })
@@ -640,4 +724,29 @@ mod tests {
640724
assert_eq!(name, "RSScript");
641725
let _ = std::fs::remove_file(path);
642726
}
727+
728+
#[test]
729+
fn image_runtime_hooks_load_transform_and_save() {
730+
let input = std::env::temp_dir().join(format!(
731+
"rsscript-runtime-image-input-{}.bin",
732+
std::process::id()
733+
));
734+
let output = std::env::temp_dir().join(format!(
735+
"rsscript-runtime-image-output-{}.bin",
736+
std::process::id()
737+
));
738+
std::fs::write(&input, b"image-bytes").expect("input image should be writable");
739+
740+
let mut image = super::image_load(&input).expect("image should load");
741+
super::image_resize(&mut image, 320, 240);
742+
super::image_normalize(&mut image);
743+
super::image_sharpen(&mut image);
744+
super::image_save(&image, &output).expect("image should save");
745+
746+
let saved = std::fs::read_to_string(&output).expect("saved image should be readable");
747+
assert!(saved.contains("rsscript-image-ops:load,resize,normalize,sharpen;size=320x240"));
748+
749+
let _ = std::fs::remove_file(input);
750+
let _ = std::fs::remove_file(output);
751+
}
643752
}

src/rust_lower.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -783,6 +783,8 @@ impl<'a> RustLowerer<'a> {
783783
"Path" => "std::path::PathBuf".to_string(),
784784
"File" => "rsscript_runtime::File".to_string(),
785785
"FileError" | "IOError" => "std::io::Error".to_string(),
786+
"Image" => "rsscript_runtime::Image".to_string(),
787+
"ImageError" => "rsscript_runtime::ImageError".to_string(),
786788
"JsonValue" => "rsscript_runtime::JsonValue".to_string(),
787789
"JsonError" => "rsscript_runtime::JsonError".to_string(),
788790
"RowBuffer" => "rsscript_runtime::RowBuffer".to_string(),
@@ -1064,6 +1066,14 @@ fn lower_callee(callee: &Callee) -> String {
10641066
}
10651067
callee if is_file_read_all_callee(callee) => "rsscript_runtime::file_read_all".to_string(),
10661068
callee if is_file_write_callee(callee) => "rsscript_runtime::file_write".to_string(),
1069+
callee if is_image_load_callee(callee) => "rsscript_runtime::image_load".to_string(),
1070+
callee if is_image_resize_callee(callee) => "rsscript_runtime::image_resize".to_string(),
1071+
callee if is_image_normalize_callee(callee) => {
1072+
"rsscript_runtime::image_normalize".to_string()
1073+
}
1074+
callee if is_image_sharpen_callee(callee) => "rsscript_runtime::image_sharpen".to_string(),
1075+
callee if is_image_save_callee(callee) => "rsscript_runtime::image_save".to_string(),
1076+
callee if is_image_inspect_callee(callee) => "rsscript_runtime::image_inspect".to_string(),
10671077
callee if is_json_parse_callee(callee) => "rsscript_runtime::json_parse".to_string(),
10681078
callee if is_json_field_callee(callee) => "rsscript_runtime::json_field".to_string(),
10691079
callee if is_json_field_string_callee(callee) => {
@@ -1119,6 +1129,30 @@ fn is_file_write_callee(callee: &Callee) -> bool {
11191129
matches!(callee, Callee::Qualified { namespace, name } if type_root_name(namespace) == "File" && name == "write")
11201130
}
11211131

1132+
fn is_image_load_callee(callee: &Callee) -> bool {
1133+
matches!(callee, Callee::Qualified { namespace, name } if type_root_name(namespace) == "Image" && name == "load")
1134+
}
1135+
1136+
fn is_image_resize_callee(callee: &Callee) -> bool {
1137+
matches!(callee, Callee::Qualified { namespace, name } if type_root_name(namespace) == "Image" && name == "resize")
1138+
}
1139+
1140+
fn is_image_normalize_callee(callee: &Callee) -> bool {
1141+
matches!(callee, Callee::Qualified { namespace, name } if type_root_name(namespace) == "Image" && name == "normalize")
1142+
}
1143+
1144+
fn is_image_sharpen_callee(callee: &Callee) -> bool {
1145+
matches!(callee, Callee::Qualified { namespace, name } if type_root_name(namespace) == "Image" && name == "sharpen")
1146+
}
1147+
1148+
fn is_image_save_callee(callee: &Callee) -> bool {
1149+
matches!(callee, Callee::Qualified { namespace, name } if type_root_name(namespace) == "Image" && name == "save")
1150+
}
1151+
1152+
fn is_image_inspect_callee(callee: &Callee) -> bool {
1153+
matches!(callee, Callee::Qualified { namespace, name } if type_root_name(namespace) == "Image" && name == "inspect")
1154+
}
1155+
11221156
fn is_json_parse_callee(callee: &Callee) -> bool {
11231157
matches!(callee, Callee::Qualified { namespace, name } if type_root_name(namespace) == "Json" && name == "parse")
11241158
}

tests/checker.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,32 @@ fn read_name(path: read Path) -> Result<String, CsvError> {
379379
assert!(rust.contains("return rsscript_runtime::row_field_string(&row, 0);"));
380380
}
381381

382+
#[test]
383+
fn rust_lowering_maps_image_core_calls_to_runtime_hooks() {
384+
let source = r#"
385+
features: local
386+
387+
fn process(input: read Path, output: read Path) -> Result<Unit, ImageError> {
388+
local image = Image.load(path: read input)?
389+
Image.resize(image: mut image, width: 320, height: 240)
390+
Image.normalize(image: mut image)
391+
Image.sharpen(image: mut image)
392+
Image.inspect(image: read image)
393+
Image.save(image: read image, path: read output)?
394+
return Ok(Unit)
395+
}
396+
"#;
397+
let rust = lower_source_to_rust("image.rss", source).expect("source should lower");
398+
399+
assert!(rust.contains("-> Result<(), rsscript_runtime::ImageError>"));
400+
assert!(rust.contains("let mut image = rsscript_runtime::image_load(&input)?;"));
401+
assert!(rust.contains("rsscript_runtime::image_resize(&mut image, 320, 240);"));
402+
assert!(rust.contains("rsscript_runtime::image_normalize(&mut image);"));
403+
assert!(rust.contains("rsscript_runtime::image_sharpen(&mut image);"));
404+
assert!(rust.contains("rsscript_runtime::image_inspect(&image);"));
405+
assert!(rust.contains("rsscript_runtime::image_save(&image, &output)?;"));
406+
}
407+
382408
#[test]
383409
fn rust_lowering_decodes_string_escape_sequences() {
384410
let source = r#"

0 commit comments

Comments
 (0)