Skip to content

Commit a394cd9

Browse files
committed
Add runnable Csv runtime hooks
1 parent 3136f1b commit a394cd9

8 files changed

Lines changed: 193 additions & 10 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` and `Json` 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`, 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.
422422

423423
Smallest runnable example:
424424

core/csv/csv.rssi

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
struct RowBuffer
2+
3+
struct Row
4+
5+
pub fn RowBuffer.new(size: Int) -> fresh RowBuffer
6+
7+
pub fn Csv.read_into(
8+
file: mut File,
9+
buffer: mut RowBuffer,
10+
) -> Result<Unit, CsvError>
11+
12+
pub fn Csv.parse_row(buffer: read RowBuffer) -> Result<fresh Row, CsvError>
13+
14+
pub fn Row.field_string(
15+
row: read Row,
16+
index: Int,
17+
) -> Result<String, CsvError>

core/prototype/builtins.rssi

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,6 @@ pub fn ImageCache.store(
66
) -> Unit
77
effects(retains(image))
88

9-
pub fn RowBuffer.new(size: Int) -> fresh RowBuffer
10-
11-
pub fn Csv.read_into(
12-
file: mut File,
13-
buffer: mut RowBuffer,
14-
) -> Result<Unit, CsvError>
15-
16-
pub fn Csv.parse_row(buffer: read RowBuffer) -> Result<fresh Row, CsvError>
17-
189
pub fn List.consume(list: take List) -> Unit
1910

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

examples/csv_read.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, CsvError> {
4+
let path = "rsscript-csv-example.csv"
5+
6+
with File.open_write(path: read path) as file {
7+
File.write(file: mut file, data: read "name,amount\nRSScript,42\n")?
8+
}
9+
10+
local buffer = RowBuffer.new(size: 4096)
11+
with File.open_read(path: read path) as file {
12+
Csv.read_into(file: mut file, buffer: mut buffer)?
13+
let row = Csv.parse_row(buffer: read buffer)?
14+
let name = Row.field_string(row: read row, index: 0)?
15+
Assert.equal(left: read name, right: read "RSScript")
16+
Log.write(message: read name)
17+
}
18+
19+
return Ok(Unit)
20+
}

runtime/src/lib.rs

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use std::io::{Read, Write};
44
use std::ops::{Deref, DerefMut};
55
use std::path::PathBuf;
66
use std::rc::Rc;
7+
use std::str::Utf8Error;
78

89
pub trait Managed {}
910

@@ -49,6 +50,49 @@ impl From<serde_json::Error> for JsonError {
4950
}
5051
}
5152

53+
#[derive(Debug, Clone)]
54+
pub struct RowBuffer {
55+
bytes: Vec<u8>,
56+
}
57+
58+
#[derive(Debug, Clone)]
59+
pub struct Row {
60+
fields: Vec<String>,
61+
}
62+
63+
#[derive(Debug, Clone, PartialEq, Eq)]
64+
pub struct CsvError {
65+
message: String,
66+
}
67+
68+
impl CsvError {
69+
fn new(message: impl Into<String>) -> Self {
70+
Self {
71+
message: message.into(),
72+
}
73+
}
74+
}
75+
76+
impl fmt::Display for CsvError {
77+
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
78+
write!(formatter, "{}", self.message)
79+
}
80+
}
81+
82+
impl std::error::Error for CsvError {}
83+
84+
impl From<std::io::Error> for CsvError {
85+
fn from(error: std::io::Error) -> Self {
86+
Self::new(error.to_string())
87+
}
88+
}
89+
90+
impl From<Utf8Error> for CsvError {
91+
fn from(error: Utf8Error) -> Self {
92+
Self::new(error.to_string())
93+
}
94+
}
95+
5296
pub trait RuntimePath {
5397
fn as_path(&self) -> &std::path::Path;
5498
}
@@ -152,6 +196,45 @@ pub fn json_field_string(value: &JsonValue, name: &str) -> Result<String, JsonEr
152196
Ok(text.to_string())
153197
}
154198

199+
pub fn row_buffer_new(size: i64) -> RowBuffer {
200+
RowBuffer {
201+
bytes: Vec::with_capacity(size.max(0) as usize),
202+
}
203+
}
204+
205+
pub fn csv_read_into(file: &mut File, buffer: &mut RowBuffer) -> Result<(), CsvError> {
206+
buffer.bytes.clear();
207+
file.inner.read_to_end(&mut buffer.bytes)?;
208+
Ok(())
209+
}
210+
211+
pub fn csv_parse_row(buffer: &RowBuffer) -> Result<Row, CsvError> {
212+
let text = std::str::from_utf8(&buffer.bytes)?;
213+
let Some(line) = text
214+
.lines()
215+
.map(str::trim)
216+
.filter(|line| !line.is_empty())
217+
.nth(1)
218+
.or_else(|| text.lines().map(str::trim).find(|line| !line.is_empty()))
219+
else {
220+
return Err(CsvError::new("CSV buffer is empty"));
221+
};
222+
Ok(Row {
223+
fields: line
224+
.split(',')
225+
.map(|field| field.trim().to_string())
226+
.collect(),
227+
})
228+
}
229+
230+
pub fn row_field_string(row: &Row, index: i64) -> Result<String, CsvError> {
231+
let index = usize::try_from(index).map_err(|_| CsvError::new("negative CSV field index"))?;
232+
row.fields
233+
.get(index)
234+
.cloned()
235+
.ok_or_else(|| CsvError::new(format!("CSV field index `{index}` is out of bounds")))
236+
}
237+
155238
#[derive(Clone)]
156239
pub struct Gc<T> {
157240
inner: Rc<RefCell<T>>,
@@ -536,4 +619,25 @@ mod tests {
536619

537620
assert_eq!(name, "RSScript");
538621
}
622+
623+
#[test]
624+
fn csv_runtime_hooks_read_and_parse_row() {
625+
let path =
626+
std::env::temp_dir().join(format!("rsscript-runtime-csv-{}.csv", std::process::id()));
627+
628+
{
629+
let mut file = super::file_open_write(&path).expect("file should open for write");
630+
super::file_write(&mut file, &"name,amount\nRSScript,42\n")
631+
.expect("write should succeed");
632+
}
633+
634+
let mut file = super::file_open_read(&path).expect("file should open for read");
635+
let mut buffer = super::row_buffer_new(4096);
636+
super::csv_read_into(&mut file, &mut buffer).expect("CSV read should succeed");
637+
let row = super::csv_parse_row(&buffer).expect("CSV row should parse");
638+
let name = super::row_field_string(&row, 0).expect("field should exist");
639+
640+
assert_eq!(name, "RSScript");
641+
let _ = std::fs::remove_file(path);
642+
}
539643
}

src/interfaces.rs

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

src/rust_lower.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -785,6 +785,9 @@ impl<'a> RustLowerer<'a> {
785785
"FileError" | "IOError" => "std::io::Error".to_string(),
786786
"JsonValue" => "rsscript_runtime::JsonValue".to_string(),
787787
"JsonError" => "rsscript_runtime::JsonError".to_string(),
788+
"RowBuffer" => "rsscript_runtime::RowBuffer".to_string(),
789+
"Row" => "rsscript_runtime::Row".to_string(),
790+
"CsvError" => "rsscript_runtime::CsvError".to_string(),
788791
"Result" if ty.args.len() == 2 => format!(
789792
"Result<{}, {}>",
790793
self.lower_type_ref(&ty.args[0], ManagedPosition::Nested),
@@ -1066,6 +1069,14 @@ fn lower_callee(callee: &Callee) -> String {
10661069
callee if is_json_field_string_callee(callee) => {
10671070
"rsscript_runtime::json_field_string".to_string()
10681071
}
1072+
callee if is_row_buffer_new_callee(callee) => {
1073+
"rsscript_runtime::row_buffer_new".to_string()
1074+
}
1075+
callee if is_csv_read_into_callee(callee) => "rsscript_runtime::csv_read_into".to_string(),
1076+
callee if is_csv_parse_row_callee(callee) => "rsscript_runtime::csv_parse_row".to_string(),
1077+
callee if is_row_field_string_callee(callee) => {
1078+
"rsscript_runtime::row_field_string".to_string()
1079+
}
10691080
Callee::Name(name) => rust_ident(name),
10701081
Callee::Qualified { namespace, name } if type_root_name(namespace) == "ResourcePool" => {
10711082
format!("rsscript_runtime::ResourcePool::{}", rust_ident(name))
@@ -1120,6 +1131,22 @@ fn is_json_field_string_callee(callee: &Callee) -> bool {
11201131
matches!(callee, Callee::Qualified { namespace, name } if type_root_name(namespace) == "Json" && name == "field_string")
11211132
}
11221133

1134+
fn is_row_buffer_new_callee(callee: &Callee) -> bool {
1135+
matches!(callee, Callee::Qualified { namespace, name } if type_root_name(namespace) == "RowBuffer" && name == "new")
1136+
}
1137+
1138+
fn is_csv_read_into_callee(callee: &Callee) -> bool {
1139+
matches!(callee, Callee::Qualified { namespace, name } if type_root_name(namespace) == "Csv" && name == "read_into")
1140+
}
1141+
1142+
fn is_csv_parse_row_callee(callee: &Callee) -> bool {
1143+
matches!(callee, Callee::Qualified { namespace, name } if type_root_name(namespace) == "Csv" && name == "parse_row")
1144+
}
1145+
1146+
fn is_row_field_string_callee(callee: &Callee) -> bool {
1147+
matches!(callee, Callee::Qualified { namespace, name } if type_root_name(namespace) == "Row" && name == "field_string")
1148+
}
1149+
11231150
fn is_file_open_expr(expr: &Expr) -> bool {
11241151
matches!(expr, Expr::Call { callee, .. } if is_file_open_callee(callee) || is_file_open_read_callee(callee) || is_file_open_write_callee(callee))
11251152
}

tests/checker.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,29 @@ fn read_name(text: read String) -> Result<String, JsonError> {
356356
);
357357
}
358358

359+
#[test]
360+
fn rust_lowering_maps_csv_core_calls_to_runtime_hooks() {
361+
let source = r#"
362+
features: local
363+
364+
fn read_name(path: read Path) -> Result<String, CsvError> {
365+
local buffer = RowBuffer.new(size: 4096)
366+
with File.open_read(path: read path) as file {
367+
Csv.read_into(file: mut file, buffer: mut buffer)?
368+
let row = Csv.parse_row(buffer: read buffer)?
369+
return Row.field_string(row: read row, index: 0)
370+
}
371+
}
372+
"#;
373+
let rust = lower_source_to_rust("csv.rss", source).expect("source should lower");
374+
375+
assert!(rust.contains("-> Result<String, rsscript_runtime::CsvError>"));
376+
assert!(rust.contains("let mut buffer = rsscript_runtime::row_buffer_new(4096);"));
377+
assert!(rust.contains("rsscript_runtime::csv_read_into(&mut file, &mut buffer)?;"));
378+
assert!(rust.contains("let row = rsscript_runtime::csv_parse_row(&buffer)?;"));
379+
assert!(rust.contains("return rsscript_runtime::row_field_string(&row, 0);"));
380+
}
381+
359382
#[test]
360383
fn rust_lowering_decodes_string_escape_sequences() {
361384
let source = r#"

0 commit comments

Comments
 (0)