Skip to content

Commit 10027b7

Browse files
committed
Add runnable File runtime hooks
1 parent d811c2b commit 10027b7

5 files changed

Lines changed: 191 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)` and `Assert.equal(left: read String, right: read String)`, 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` 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/file_copy.rss

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
pub fn copy_file(input: read Path, output: read Path) -> Result<Unit, IOError> {
2+
with File.open_read(path: read input) as reader {
3+
with File.open_write(path: read output) as writer {
4+
let bytes = File.read_all(file: mut reader)?
5+
File.write(file: mut writer, data: read bytes)?
6+
}
7+
}
8+
return Ok(Unit)
9+
}
10+
11+
fn main() -> Result<Unit, IOError> {
12+
let input = "rsscript-file-copy-input.txt"
13+
let output = "rsscript-file-copy-output.txt"
14+
15+
with File.open_write(path: read input) as file {
16+
File.write(file: mut file, data: read "file copy ran")?
17+
}
18+
19+
with File.open_read(path: read input) as reader {
20+
with File.open_write(path: read output) as writer {
21+
let bytes = File.read_all(file: mut reader)?
22+
File.write(file: mut writer, data: read bytes)?
23+
}
24+
}
25+
26+
Log.write(message: read "file copy ran")
27+
return Ok(Unit)
28+
}

runtime/src/lib.rs

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
use std::cell::{BorrowError, BorrowMutError, Ref, RefCell, RefMut};
22
use std::fmt;
3+
use std::io::{Read, Write};
34
use std::ops::{Deref, DerefMut};
5+
use std::path::PathBuf;
46
use std::rc::Rc;
57

68
pub trait Managed {}
@@ -9,6 +11,90 @@ impl<T: 'static> Managed for T {}
911

1012
pub trait Resource {}
1113

14+
pub struct File {
15+
inner: std::fs::File,
16+
}
17+
18+
impl Resource for File {}
19+
20+
pub trait RuntimePath {
21+
fn as_path(&self) -> &std::path::Path;
22+
}
23+
24+
impl RuntimePath for PathBuf {
25+
fn as_path(&self) -> &std::path::Path {
26+
self.as_path()
27+
}
28+
}
29+
30+
impl RuntimePath for String {
31+
fn as_path(&self) -> &std::path::Path {
32+
std::path::Path::new(self)
33+
}
34+
}
35+
36+
impl RuntimePath for str {
37+
fn as_path(&self) -> &std::path::Path {
38+
std::path::Path::new(self)
39+
}
40+
}
41+
42+
impl<T: RuntimePath + ?Sized> RuntimePath for &T {
43+
fn as_path(&self) -> &std::path::Path {
44+
(*self).as_path()
45+
}
46+
}
47+
48+
pub trait RuntimeBytes {
49+
fn as_bytes_slice(&self) -> &[u8];
50+
}
51+
52+
impl RuntimeBytes for Vec<u8> {
53+
fn as_bytes_slice(&self) -> &[u8] {
54+
self.as_slice()
55+
}
56+
}
57+
58+
impl RuntimeBytes for String {
59+
fn as_bytes_slice(&self) -> &[u8] {
60+
self.as_bytes()
61+
}
62+
}
63+
64+
impl RuntimeBytes for str {
65+
fn as_bytes_slice(&self) -> &[u8] {
66+
self.as_bytes()
67+
}
68+
}
69+
70+
impl<T: RuntimeBytes + ?Sized> RuntimeBytes for &T {
71+
fn as_bytes_slice(&self) -> &[u8] {
72+
(*self).as_bytes_slice()
73+
}
74+
}
75+
76+
pub fn file_open<P: RuntimePath + ?Sized>(path: &P) -> std::io::Result<File> {
77+
file_open_read(path)
78+
}
79+
80+
pub fn file_open_read<P: RuntimePath + ?Sized>(path: &P) -> std::io::Result<File> {
81+
std::fs::File::open(path.as_path()).map(|inner| File { inner })
82+
}
83+
84+
pub fn file_open_write<P: RuntimePath + ?Sized>(path: &P) -> std::io::Result<File> {
85+
std::fs::File::create(path.as_path()).map(|inner| File { inner })
86+
}
87+
88+
pub fn file_read_all(file: &mut File) -> std::io::Result<Vec<u8>> {
89+
let mut bytes = Vec::new();
90+
file.inner.read_to_end(&mut bytes)?;
91+
Ok(bytes)
92+
}
93+
94+
pub fn file_write<B: RuntimeBytes + ?Sized>(file: &mut File, data: &B) -> std::io::Result<()> {
95+
file.inner.write_all(data.as_bytes_slice())
96+
}
97+
1298
#[derive(Clone)]
1399
pub struct Gc<T> {
14100
inner: Rc<RefCell<T>>,
@@ -365,4 +451,21 @@ mod tests {
365451

366452
assert_eq!(error.kind, RuntimeErrorKind::ResourcePoolBorrowConflict);
367453
}
454+
455+
#[test]
456+
fn file_runtime_hooks_write_and_read_bytes() {
457+
let path =
458+
std::env::temp_dir().join(format!("rsscript-runtime-file-{}.txt", std::process::id()));
459+
460+
{
461+
let mut file = super::file_open_write(&path).expect("file should open for write");
462+
super::file_write(&mut file, &"hello file").expect("write should succeed");
463+
}
464+
465+
let mut file = super::file_open_read(&path).expect("file should open for read");
466+
let bytes = super::file_read_all(&mut file).expect("read should succeed");
467+
468+
assert_eq!(bytes, b"hello file");
469+
let _ = std::fs::remove_file(path);
470+
}
368471
}

src/rust_lower.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,8 @@ impl<'a> RustLowerer<'a> {
496496
let resource = self.lower_expr(&stmt.resource);
497497
let resource = if is_resource_pool_borrow_expr(&stmt.resource) {
498498
format!("rsscript_runtime::unwrap_runtime({resource})")
499+
} else if is_file_open_expr(&stmt.resource) {
500+
format!("{resource}?")
499501
} else {
500502
resource
501503
};
@@ -779,6 +781,8 @@ impl<'a> RustLowerer<'a> {
779781
"String" => "String".to_string(),
780782
"Bytes" | "Buffer" => "Vec<u8>".to_string(),
781783
"Path" => "std::path::PathBuf".to_string(),
784+
"File" => "rsscript_runtime::File".to_string(),
785+
"FileError" | "IOError" => "std::io::Error".to_string(),
782786
"Result" if ty.args.len() == 2 => format!(
783787
"Result<{}, {}>",
784788
self.lower_type_ref(&ty.args[0], ManagedPosition::Nested),
@@ -1046,6 +1050,15 @@ fn lower_callee(callee: &Callee) -> String {
10461050
match callee {
10471051
callee if is_log_write_callee(callee) => "rsscript_runtime::log_write".to_string(),
10481052
callee if is_assert_equal_callee(callee) => "rsscript_runtime::assert_equal".to_string(),
1053+
callee if is_file_open_callee(callee) => "rsscript_runtime::file_open".to_string(),
1054+
callee if is_file_open_read_callee(callee) => {
1055+
"rsscript_runtime::file_open_read".to_string()
1056+
}
1057+
callee if is_file_open_write_callee(callee) => {
1058+
"rsscript_runtime::file_open_write".to_string()
1059+
}
1060+
callee if is_file_read_all_callee(callee) => "rsscript_runtime::file_read_all".to_string(),
1061+
callee if is_file_write_callee(callee) => "rsscript_runtime::file_write".to_string(),
10491062
Callee::Name(name) => rust_ident(name),
10501063
Callee::Qualified { namespace, name } if type_root_name(namespace) == "ResourcePool" => {
10511064
format!("rsscript_runtime::ResourcePool::{}", rust_ident(name))
@@ -1068,6 +1081,30 @@ fn is_string_concat_callee(callee: &Callee) -> bool {
10681081
matches!(callee, Callee::Qualified { namespace, name } if type_root_name(namespace) == "String" && name == "concat")
10691082
}
10701083

1084+
fn is_file_open_callee(callee: &Callee) -> bool {
1085+
matches!(callee, Callee::Qualified { namespace, name } if type_root_name(namespace) == "File" && name == "open")
1086+
}
1087+
1088+
fn is_file_open_read_callee(callee: &Callee) -> bool {
1089+
matches!(callee, Callee::Qualified { namespace, name } if type_root_name(namespace) == "File" && name == "open_read")
1090+
}
1091+
1092+
fn is_file_open_write_callee(callee: &Callee) -> bool {
1093+
matches!(callee, Callee::Qualified { namespace, name } if type_root_name(namespace) == "File" && name == "open_write")
1094+
}
1095+
1096+
fn is_file_read_all_callee(callee: &Callee) -> bool {
1097+
matches!(callee, Callee::Qualified { namespace, name } if type_root_name(namespace) == "File" && name == "read_all")
1098+
}
1099+
1100+
fn is_file_write_callee(callee: &Callee) -> bool {
1101+
matches!(callee, Callee::Qualified { namespace, name } if type_root_name(namespace) == "File" && name == "write")
1102+
}
1103+
1104+
fn is_file_open_expr(expr: &Expr) -> bool {
1105+
matches!(expr, Expr::Call { callee, .. } if is_file_open_callee(callee) || is_file_open_read_callee(callee) || is_file_open_write_callee(callee))
1106+
}
1107+
10711108
fn lower_string_concat_call(lowerer: &mut RustLowerer<'_>, args: &[CallArg]) -> String {
10721109
let left = lower_call_arg(lowerer, args, "left", 0, "\"\".to_string()");
10731110
let right = lower_call_arg(lowerer, args, "right", 1, "\"\".to_string()");

tests/checker.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,28 @@ pub fn inspect_core(
311311
assert!(rust.contains("-> Result<Vec<u8>, CoreError>"));
312312
}
313313

314+
#[test]
315+
fn rust_lowering_maps_file_core_calls_to_runtime_hooks() {
316+
let source = r#"
317+
fn copy_file(input: read Path, output: read Path) -> Result<Unit, IOError> {
318+
with File.open_read(path: read input) as reader {
319+
with File.open_write(path: read output) as writer {
320+
let bytes = File.read_all(file: mut reader)?
321+
File.write(file: mut writer, data: read bytes)?
322+
}
323+
}
324+
return Ok(Unit)
325+
}
326+
"#;
327+
let rust = lower_source_to_rust("file.rss", source).expect("source should lower");
328+
329+
assert!(rust.contains("-> Result<(), std::io::Error>"));
330+
assert!(rust.contains("let mut reader = rsscript_runtime::file_open_read(&input)?;"));
331+
assert!(rust.contains("let mut writer = rsscript_runtime::file_open_write(&output)?;"));
332+
assert!(rust.contains("let bytes = rsscript_runtime::file_read_all(&mut reader)?;"));
333+
assert!(rust.contains("rsscript_runtime::file_write(&mut writer, &bytes)?;"));
334+
}
335+
314336
#[test]
315337
fn rust_lowering_maps_log_write_to_runtime_output_hook() {
316338
let source = r#"

0 commit comments

Comments
 (0)