Skip to content

Commit 7d8ca91

Browse files
committed
Add runnable config reload hooks
1 parent 9513541 commit 7d8ca91

7 files changed

Lines changed: 222 additions & 3 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`, 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.
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, and config reload 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/config/config.rssi

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
struct ConfigValue
2+
3+
struct ConfigStore
4+
5+
pub fn Config.load(path: read Path) -> Result<fresh ConfigValue, ConfigError>
6+
7+
pub fn Config.name(value: read ConfigValue) -> String
8+
9+
pub fn ConfigStore.new(value: read ConfigValue) -> fresh ConfigStore
10+
11+
pub fn ConfigStore.replace(
12+
store: mut ConfigStore,
13+
value: read ConfigValue,
14+
) -> Unit
15+
effects(retains(value))
16+
17+
pub fn ConfigStore.name(store: read ConfigStore) -> String

examples/config_reload.rss

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
features: local
2+
3+
fn load_config(path: read String) -> Result<fresh ConfigValue, ConfigError> {
4+
return Config.load(path: read path)
5+
}
6+
7+
fn reload_config(path: read String, store: mut ConfigStore) -> Result<Unit, ConfigError> {
8+
let next = load_config(path: read path)?
9+
ConfigStore.replace(store: mut store, value: read next)
10+
return Ok(Unit)
11+
}
12+
13+
fn main() -> Result<Unit, ConfigError> {
14+
let first = "rsscript-config-first.txt"
15+
let second = "rsscript-config-second.txt"
16+
17+
with File.open_write(path: read first) as file {
18+
File.write(file: mut file, data: read "initial\n")?
19+
}
20+
with File.open_write(path: read second) as file {
21+
File.write(file: mut file, data: read "reloaded\n")?
22+
}
23+
24+
let initial = load_config(path: read first)?
25+
local store = ConfigStore.new(value: read initial)
26+
reload_config(path: read second, store: mut store)?
27+
28+
let name = ConfigStore.name(store: read store)
29+
Assert.equal(left: read name, right: read "reloaded")
30+
Log.write(message: read name)
31+
return Ok(Unit)
32+
}

runtime/src/lib.rs

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

32+
#[derive(Debug, Clone, PartialEq, Eq)]
33+
pub struct ConfigValue {
34+
name: String,
35+
}
36+
37+
#[derive(Debug, Clone, PartialEq, Eq)]
38+
pub struct ConfigStore {
39+
current: ConfigValue,
40+
}
41+
42+
#[derive(Debug, Clone, PartialEq, Eq)]
43+
pub struct ConfigError {
44+
message: String,
45+
}
46+
47+
impl ConfigError {
48+
fn new(message: impl Into<String>) -> Self {
49+
Self {
50+
message: message.into(),
51+
}
52+
}
53+
}
54+
55+
impl fmt::Display for ConfigError {
56+
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
57+
write!(formatter, "{}", self.message)
58+
}
59+
}
60+
61+
impl std::error::Error for ConfigError {}
62+
63+
impl From<std::io::Error> for ConfigError {
64+
fn from(error: std::io::Error) -> Self {
65+
Self::new(error.to_string())
66+
}
67+
}
68+
3269
#[derive(Debug, Clone, PartialEq, Eq)]
3370
pub struct DbConnection {
3471
url: String,
@@ -284,6 +321,35 @@ pub fn response_body(response: &Response) -> String {
284321
response.body.clone()
285322
}
286323

324+
pub fn config_load<P: RuntimePath + ?Sized>(path: &P) -> Result<ConfigValue, ConfigError> {
325+
let text = std::fs::read_to_string(path.as_path())?;
326+
let name = text
327+
.lines()
328+
.map(str::trim)
329+
.find(|line| !line.is_empty())
330+
.unwrap_or("default")
331+
.to_string();
332+
Ok(ConfigValue { name })
333+
}
334+
335+
pub fn config_name(value: &ConfigValue) -> String {
336+
value.name.clone()
337+
}
338+
339+
pub fn config_store_new(value: &ConfigValue) -> ConfigStore {
340+
ConfigStore {
341+
current: value.clone(),
342+
}
343+
}
344+
345+
pub fn config_store_replace(store: &mut ConfigStore, value: &ConfigValue) {
346+
store.current = value.clone();
347+
}
348+
349+
pub fn config_store_name(store: &ConfigStore) -> String {
350+
store.current.name.clone()
351+
}
352+
287353
pub fn db_connection_open(url: &str) -> DbConnection {
288354
DbConnection {
289355
url: url.to_string(),
@@ -883,4 +949,28 @@ mod tests {
883949

884950
assert_eq!(pool.values.borrow().len(), 2);
885951
}
952+
953+
#[test]
954+
fn config_runtime_hooks_load_and_replace_store() {
955+
let first = std::env::temp_dir().join(format!(
956+
"rsscript-runtime-config-first-{}.txt",
957+
std::process::id()
958+
));
959+
let second = std::env::temp_dir().join(format!(
960+
"rsscript-runtime-config-second-{}.txt",
961+
std::process::id()
962+
));
963+
std::fs::write(&first, "initial\n").expect("first config should write");
964+
std::fs::write(&second, "reloaded\n").expect("second config should write");
965+
966+
let initial = super::config_load(&first).expect("initial config should load");
967+
let mut store = super::config_store_new(&initial);
968+
let reloaded = super::config_load(&second).expect("reloaded config should load");
969+
super::config_store_replace(&mut store, &reloaded);
970+
971+
assert_eq!(super::config_store_name(&store), "reloaded");
972+
973+
let _ = std::fs::remove_file(first);
974+
let _ = std::fs::remove_file(second);
975+
}
886976
}

src/interfaces.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ pub(crate) const CORE_INTERFACES: &[(&str, &str)] = &[
33
"core/collections/map.rssi",
44
include_str!("../core/collections/map.rssi"),
55
),
6+
(
7+
"core/config/config.rssi",
8+
include_str!("../core/config/config.rssi"),
9+
),
610
("core/csv/csv.rssi", include_str!("../core/csv/csv.rssi")),
711
("core/db/db.rssi", include_str!("../core/db/db.rssi")),
812
("core/fs/file.rssi", include_str!("../core/fs/file.rssi")),

src/rust_lower.rs

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,7 @@ pub fn check_generated_rust_package(package_dir: &Path) -> Result<RustBackendChe
287287
struct RustLowerer<'a> {
288288
program: &'a Program,
289289
type_kinds: BTreeMap<String, TypeKind>,
290+
param_effects: BTreeMap<String, DataEffect>,
290291
source_map: Vec<RustSourceMapEntry>,
291292
}
292293

@@ -304,6 +305,7 @@ impl<'a> RustLowerer<'a> {
304305
Self {
305306
program,
306307
type_kinds,
308+
param_effects: BTreeMap::new(),
307309
source_map: Vec::new(),
308310
}
309311
}
@@ -399,6 +401,12 @@ impl<'a> RustLowerer<'a> {
399401
}
400402

401403
fn lower_function(&mut self, function: &FunctionDecl, out: &mut String) {
404+
let previous_param_effects = std::mem::take(&mut self.param_effects);
405+
self.param_effects = function
406+
.params
407+
.iter()
408+
.filter_map(|param| param.effect.map(|effect| (param.name.clone(), effect)))
409+
.collect();
402410
self.record_source_marker(out, 0, "function", &function.span);
403411
let async_prefix = if function.is_async { "async " } else { "" };
404412
let is_public = function.is_public || is_runnable_main(function);
@@ -437,6 +445,7 @@ impl<'a> RustLowerer<'a> {
437445
self.lower_block(&function.body, out, 1);
438446
}
439447
out.push_str("}\n");
448+
self.param_effects = previous_param_effects;
440449
}
441450

442451
fn lower_param(&self, param: &Param) -> String {
@@ -740,7 +749,15 @@ impl<'a> RustLowerer<'a> {
740749
}
741750
Expr::Effect { effect, value, .. } => match effect {
742751
DataEffect::Read => format!("&{}", self.lower_expr(value)),
743-
DataEffect::Mut => format!("&mut {}", self.lower_expr(value)),
752+
DataEffect::Mut => {
753+
if let Expr::Ident(name, _) = &**value
754+
&& self.param_effects.get(name) == Some(&DataEffect::Mut)
755+
{
756+
rust_ident(name)
757+
} else {
758+
format!("&mut {}", self.lower_expr(value))
759+
}
760+
}
744761
DataEffect::Take => self.lower_expr(value),
745762
},
746763
Expr::Manage { value, span } => {
@@ -794,6 +811,9 @@ impl<'a> RustLowerer<'a> {
794811
"Request" => "rsscript_runtime::Request".to_string(),
795812
"Response" => "rsscript_runtime::Response".to_string(),
796813
"HttpError" => "rsscript_runtime::HttpError".to_string(),
814+
"ConfigValue" => "rsscript_runtime::ConfigValue".to_string(),
815+
"ConfigStore" => "rsscript_runtime::ConfigStore".to_string(),
816+
"ConfigError" => "rsscript_runtime::ConfigError".to_string(),
797817
"DbConnection" => "rsscript_runtime::DbConnection".to_string(),
798818
"DbError" => "rsscript_runtime::DbError".to_string(),
799819
"Image" => "rsscript_runtime::Image".to_string(),
@@ -1086,6 +1106,17 @@ fn lower_callee(callee: &Callee) -> String {
10861106
"rsscript_runtime::response_status".to_string()
10871107
}
10881108
callee if is_response_body_callee(callee) => "rsscript_runtime::response_body".to_string(),
1109+
callee if is_config_load_callee(callee) => "rsscript_runtime::config_load".to_string(),
1110+
callee if is_config_name_callee(callee) => "rsscript_runtime::config_name".to_string(),
1111+
callee if is_config_store_new_callee(callee) => {
1112+
"rsscript_runtime::config_store_new".to_string()
1113+
}
1114+
callee if is_config_store_replace_callee(callee) => {
1115+
"rsscript_runtime::config_store_replace".to_string()
1116+
}
1117+
callee if is_config_store_name_callee(callee) => {
1118+
"rsscript_runtime::config_store_name".to_string()
1119+
}
10891120
callee if is_db_connection_open_callee(callee) => {
10901121
"rsscript_runtime::db_connection_open".to_string()
10911122
}
@@ -1176,6 +1207,26 @@ fn is_response_body_callee(callee: &Callee) -> bool {
11761207
matches!(callee, Callee::Qualified { namespace, name } if type_root_name(namespace) == "Response" && name == "body")
11771208
}
11781209

1210+
fn is_config_load_callee(callee: &Callee) -> bool {
1211+
matches!(callee, Callee::Qualified { namespace, name } if type_root_name(namespace) == "Config" && name == "load")
1212+
}
1213+
1214+
fn is_config_name_callee(callee: &Callee) -> bool {
1215+
matches!(callee, Callee::Qualified { namespace, name } if type_root_name(namespace) == "Config" && name == "name")
1216+
}
1217+
1218+
fn is_config_store_new_callee(callee: &Callee) -> bool {
1219+
matches!(callee, Callee::Qualified { namespace, name } if type_root_name(namespace) == "ConfigStore" && name == "new")
1220+
}
1221+
1222+
fn is_config_store_replace_callee(callee: &Callee) -> bool {
1223+
matches!(callee, Callee::Qualified { namespace, name } if type_root_name(namespace) == "ConfigStore" && name == "replace")
1224+
}
1225+
1226+
fn is_config_store_name_callee(callee: &Callee) -> bool {
1227+
matches!(callee, Callee::Qualified { namespace, name } if type_root_name(namespace) == "ConfigStore" && name == "name")
1228+
}
1229+
11791230
fn is_db_connection_open_callee(callee: &Callee) -> bool {
11801231
matches!(callee, Callee::Qualified { namespace, name } if type_root_name(namespace) == "DbConnection" && name == "open")
11811232
}

tests/checker.rs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,31 @@ fn run_query(url: read Url, sql: read String) -> Result<Unit, DbError> {
457457
assert!(rust.contains("rsscript_runtime::db_connection_query(&mut conn, &sql)?;"));
458458
}
459459

460+
#[test]
461+
fn rust_lowering_maps_config_reload_to_runtime_hooks() {
462+
let source = r#"
463+
features: local
464+
465+
fn load_config(path: read String) -> Result<fresh ConfigValue, ConfigError> {
466+
return Config.load(path: read path)
467+
}
468+
469+
fn reload_config(path: read String, store: mut ConfigStore) -> Result<Unit, ConfigError> {
470+
let next = load_config(path: read path)?
471+
ConfigStore.replace(store: mut store, value: read next)
472+
return Ok(Unit)
473+
}
474+
"#;
475+
let rust = lower_source_to_rust("config.rss", source).expect("source should lower");
476+
477+
assert!(
478+
rust.contains("-> Result<rsscript_runtime::ConfigValue, rsscript_runtime::ConfigError>")
479+
);
480+
assert!(rust.contains("return rsscript_runtime::config_load(&path);"));
481+
assert!(rust.contains("store: &mut rsscript_runtime::ConfigStore"));
482+
assert!(rust.contains("rsscript_runtime::config_store_replace(store, &next);"));
483+
}
484+
460485
#[test]
461486
fn rust_lowering_decodes_string_escape_sequences() {
462487
let source = r#"
@@ -776,7 +801,7 @@ fn pooled(pool: mut ResourcePool<TestConnection>) -> Unit {
776801
let rust = lower_source_to_rust("pool.rss", source).expect("source should lower");
777802

778803
assert!(rust.contains(
779-
"let mut conn = rsscript_runtime::unwrap_runtime(rsscript_runtime::ResourcePool::borrow_at(&mut pool, rsscript_runtime::SourceSpan::new(\"pool.rss\""
804+
"let mut conn = rsscript_runtime::unwrap_runtime(rsscript_runtime::ResourcePool::borrow_at(pool, rsscript_runtime::SourceSpan::new(\"pool.rss\""
780805
));
781806
}
782807

0 commit comments

Comments
 (0)