Skip to content

Commit f29d0b5

Browse files
committed
Add runnable rules config reload hooks
1 parent 018d94e commit f29d0b5

9 files changed

Lines changed: 247 additions & 31 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 that have not yet moved into `core/`. 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 the remaining cache 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)`, `OS.close(fd: Fd)`, `List.consume(list: take List<T>)`, `Buffer.consume(buffer: take Buffer)`, and the core `File`, `Json`, `Csv`, `Image`, `ImageCache`, HTTP handler, DB resource-pool, config reload, interpreter object-cycle, 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. The interpreter hooks model `Environment` and `FunctionObject` as managed handles, including an environment/function closure cycle. 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)`, `OS.close(fd: Fd)`, `List.consume(list: take List<T>)`, `Buffer.consume(buffer: take Buffer)`, and the core `File`, `Json`, `Csv`, `Image`, `ImageCache`, HTTP handler, DB resource-pool, config reload, rules config reload, interpreter object-cycle, 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. The interpreter hooks model `Environment` and `FunctionObject` as managed handles, including an environment/function closure cycle. 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/rules.rssi

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
struct Rule
2+
3+
struct Config
4+
5+
struct GlobalConfig
6+
7+
pub fn RuleLoader.load_rules(path: read Path) -> Result<fresh List<Rule>, ConfigError>
8+
9+
pub fn Config.new(name: read String, rules: read List<Rule>) -> fresh Config
10+
11+
pub fn Config.rule_count(config: read Config) -> Int
12+
13+
pub fn GlobalConfig.new(value: read Config) -> fresh GlobalConfig
14+
effects(retains(value))
15+
16+
pub fn GlobalConfig.replace(
17+
global: mut GlobalConfig,
18+
value: read Config,
19+
) -> Unit
20+
effects(retains(value))
21+
22+
pub fn GlobalConfig.rule_count(global: read GlobalConfig) -> Int

core/prototype/builtins.rssi

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,3 @@
1-
pub fn RuleLoader.load_rules(path: read Path) -> Result<fresh List<Rule>, ConfigError>
2-
3-
pub fn GlobalConfig.replace(
4-
global: mut GlobalConfig,
5-
value: read Config,
6-
) -> Unit
7-
effects(retains(value))
8-
91
pub fn Cache.lookup(cache: read Cache, key: read String)
102

113
pub fn Cache.get(cache: read Cache)

examples/rules_config_reload.rss

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
fn load_rules_config(path: read String) -> Result<fresh Config, ConfigError> {
2+
let rules = RuleLoader.load_rules(path: read path)?
3+
return Ok(Config.new(name: read "rules", rules: read rules))
4+
}
5+
6+
fn reload_rules_config(path: read String, global: mut GlobalConfig) -> Result<Unit, ConfigError> {
7+
let next = load_rules_config(path: read path)?
8+
GlobalConfig.replace(global: mut global, value: read next)
9+
return Ok(Unit)
10+
}
11+
12+
fn main() -> Result<Unit, ConfigError> {
13+
let first = "rsscript-rules-first.txt"
14+
let second = "rsscript-rules-second.txt"
15+
16+
with File.open_write(path: read first) as file {
17+
File.write(file: mut file, data: read "alpha\nbeta\n")?
18+
}
19+
with File.open_write(path: read second) as file {
20+
File.write(file: mut file, data: read "gamma\n")?
21+
}
22+
23+
let initial = load_rules_config(path: read first)?
24+
let global = GlobalConfig.new(value: read initial)
25+
reload_rules_config(path: read second, global: mut global)?
26+
27+
let count = GlobalConfig.rule_count(global: read global)
28+
if count == 1 {
29+
Log.write(message: read "rules config reloaded")
30+
}
31+
32+
return Ok(Unit)
33+
}

runtime/src/lib.rs

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,22 @@ pub struct ConfigStore {
4040
current: ConfigValue,
4141
}
4242

43+
#[derive(Debug, Clone, PartialEq, Eq)]
44+
pub struct Rule {
45+
name: String,
46+
}
47+
48+
#[derive(Debug, Clone, PartialEq, Eq)]
49+
pub struct Config {
50+
name: String,
51+
rules: Vec<Rule>,
52+
}
53+
54+
#[derive(Debug, Clone, PartialEq, Eq)]
55+
pub struct GlobalConfig {
56+
current: Config,
57+
}
58+
4359
#[derive(Debug, Clone, PartialEq, Eq)]
4460
pub struct Counter {
4561
value: i64,
@@ -421,6 +437,44 @@ pub fn config_store_name(store: &ConfigStore) -> String {
421437
store.current.name.clone()
422438
}
423439

440+
pub fn rule_loader_load_rules<P: RuntimePath + ?Sized>(path: &P) -> Result<Vec<Rule>, ConfigError> {
441+
let text = std::fs::read_to_string(path.as_path())?;
442+
let rules = text
443+
.lines()
444+
.map(str::trim)
445+
.filter(|line| !line.is_empty())
446+
.map(|name| Rule {
447+
name: name.to_string(),
448+
})
449+
.collect();
450+
Ok(rules)
451+
}
452+
453+
pub fn config_new(name: &str, rules: &[Rule]) -> Config {
454+
Config {
455+
name: name.to_string(),
456+
rules: rules.to_owned(),
457+
}
458+
}
459+
460+
pub fn config_rule_count(config: &Config) -> i64 {
461+
config.rules.len() as i64
462+
}
463+
464+
pub fn global_config_new(value: &Config) -> GlobalConfig {
465+
GlobalConfig {
466+
current: value.clone(),
467+
}
468+
}
469+
470+
pub fn global_config_replace(global: &mut GlobalConfig, value: &Config) {
471+
global.current = value.clone();
472+
}
473+
474+
pub fn global_config_rule_count(global: &GlobalConfig) -> i64 {
475+
global.current.rules.len() as i64
476+
}
477+
424478
pub fn counter_new(value: i64) -> Counter {
425479
Counter { value }
426480
}
@@ -1175,4 +1229,31 @@ mod tests {
11751229
let _ = std::fs::remove_file(first);
11761230
let _ = std::fs::remove_file(second);
11771231
}
1232+
1233+
#[test]
1234+
fn rules_config_runtime_hooks_load_and_replace_global() {
1235+
let first = std::env::temp_dir().join(format!(
1236+
"rsscript-runtime-rules-first-{}.txt",
1237+
std::process::id()
1238+
));
1239+
let second = std::env::temp_dir().join(format!(
1240+
"rsscript-runtime-rules-second-{}.txt",
1241+
std::process::id()
1242+
));
1243+
std::fs::write(&first, "alpha\nbeta\n").expect("first rules should write");
1244+
std::fs::write(&second, "gamma\n").expect("second rules should write");
1245+
1246+
let first_rules = super::rule_loader_load_rules(&first).expect("first rules should load");
1247+
let initial = super::config_new("initial", &first_rules);
1248+
let mut global = super::global_config_new(&initial);
1249+
let second_rules =
1250+
super::rule_loader_load_rules(&second).expect("second rules should load");
1251+
let next = super::config_new("next", &second_rules);
1252+
super::global_config_replace(&mut global, &next);
1253+
1254+
assert_eq!(super::global_config_rule_count(&global), 1);
1255+
1256+
let _ = std::fs::remove_file(first);
1257+
let _ = std::fs::remove_file(second);
1258+
}
11781259
}

src/hir.rs

Lines changed: 28 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1336,28 +1336,35 @@ fn split_top_level_type_args(args: &str) -> Vec<&str> {
13361336
fn classify_return_expr(hir: &Hir, expr: &Expr) -> HirReturnProof {
13371337
match expr {
13381338
Expr::Ident(name, _) => HirReturnProof::Ident { name: name.clone() },
1339-
Expr::Call { callee, .. } => match hir.resolve_call(callee) {
1340-
CallResolution::Resolved {
1341-
signature,
1342-
kind:
1343-
ResolvedCalleeKind::Constructor {
1344-
type_kind: HirTypeKind::Struct,
1345-
},
1346-
} if signature.returns_fresh => HirReturnProof::StructConstructor,
1347-
CallResolution::Resolved { signature, .. } if signature.returns_fresh => {
1348-
HirReturnProof::FreshCall
1339+
Expr::Call { callee, args, .. } => {
1340+
if matches!(callee_name(callee), "Ok" | "Some")
1341+
&& let Some(arg) = args.first()
1342+
{
1343+
return classify_return_expr(hir, &arg.value);
13491344
}
1350-
CallResolution::Resolved {
1351-
kind:
1352-
ResolvedCalleeKind::Constructor {
1353-
type_kind: HirTypeKind::Struct,
1354-
},
1355-
..
1356-
} => HirReturnProof::StructConstructor,
1357-
CallResolution::Resolved { .. }
1358-
| CallResolution::EnumVariant
1359-
| CallResolution::Unknown => HirReturnProof::Unknown,
1360-
},
1345+
match hir.resolve_call(callee) {
1346+
CallResolution::Resolved {
1347+
signature,
1348+
kind:
1349+
ResolvedCalleeKind::Constructor {
1350+
type_kind: HirTypeKind::Struct,
1351+
},
1352+
} if signature.returns_fresh => HirReturnProof::StructConstructor,
1353+
CallResolution::Resolved { signature, .. } if signature.returns_fresh => {
1354+
HirReturnProof::FreshCall
1355+
}
1356+
CallResolution::Resolved {
1357+
kind:
1358+
ResolvedCalleeKind::Constructor {
1359+
type_kind: HirTypeKind::Struct,
1360+
},
1361+
..
1362+
} => HirReturnProof::StructConstructor,
1363+
CallResolution::Resolved { .. }
1364+
| CallResolution::EnumVariant
1365+
| CallResolution::Unknown => HirReturnProof::Unknown,
1366+
}
1367+
}
13611368
Expr::Effect { value, .. } | Expr::Manage { value, .. } | Expr::Try { value, .. } => {
13621369
classify_return_expr(hir, value)
13631370
}

src/interfaces.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ pub(crate) const CORE_INTERFACES: &[(&str, &str)] = &[
1919
"core/config/config.rssi",
2020
include_str!("../core/config/config.rssi"),
2121
),
22+
(
23+
"core/config/rules.rssi",
24+
include_str!("../core/config/rules.rssi"),
25+
),
2226
(
2327
"core/counter/counter.rssi",
2428
include_str!("../core/counter/counter.rssi"),

src/rust_lower.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -811,6 +811,13 @@ impl<'a> RustLowerer<'a> {
811811
"Fd" => "i64".to_string(),
812812
"Bytes" | "Buffer" => "Vec<u8>".to_string(),
813813
"Path" => "std::path::PathBuf".to_string(),
814+
"Rule" if !self.type_kinds.contains_key("Rule") => "rsscript_runtime::Rule".to_string(),
815+
"Config" if !self.type_kinds.contains_key("Config") => {
816+
"rsscript_runtime::Config".to_string()
817+
}
818+
"GlobalConfig" if !self.type_kinds.contains_key("GlobalConfig") => {
819+
"rsscript_runtime::GlobalConfig".to_string()
820+
}
814821
"Environment" => "rsscript_runtime::Environment".to_string(),
815822
"FunctionObject" => "rsscript_runtime::FunctionObject".to_string(),
816823
"Counter" => "rsscript_runtime::Counter".to_string(),
@@ -1217,6 +1224,22 @@ fn lower_callee(callee: &Callee) -> String {
12171224
callee if is_config_store_name_callee(callee) => {
12181225
"rsscript_runtime::config_store_name".to_string()
12191226
}
1227+
callee if is_rule_loader_load_rules_callee(callee) => {
1228+
"rsscript_runtime::rule_loader_load_rules".to_string()
1229+
}
1230+
callee if is_rules_config_new_callee(callee) => "rsscript_runtime::config_new".to_string(),
1231+
callee if is_rules_config_rule_count_callee(callee) => {
1232+
"rsscript_runtime::config_rule_count".to_string()
1233+
}
1234+
callee if is_global_config_new_callee(callee) => {
1235+
"rsscript_runtime::global_config_new".to_string()
1236+
}
1237+
callee if is_global_config_replace_callee(callee) => {
1238+
"rsscript_runtime::global_config_replace".to_string()
1239+
}
1240+
callee if is_global_config_rule_count_callee(callee) => {
1241+
"rsscript_runtime::global_config_rule_count".to_string()
1242+
}
12201243
callee if is_counter_new_callee(callee) => "rsscript_runtime::counter_new".to_string(),
12211244
callee if is_counter_add_callee(callee) => "rsscript_runtime::counter_add".to_string(),
12221245
callee if is_counter_value_callee(callee) => "rsscript_runtime::counter_value".to_string(),
@@ -1372,6 +1395,30 @@ fn is_config_store_name_callee(callee: &Callee) -> bool {
13721395
matches!(callee, Callee::Qualified { namespace, name } if type_root_name(namespace) == "ConfigStore" && name == "name")
13731396
}
13741397

1398+
fn is_rule_loader_load_rules_callee(callee: &Callee) -> bool {
1399+
matches!(callee, Callee::Qualified { namespace, name } if type_root_name(namespace) == "RuleLoader" && name == "load_rules")
1400+
}
1401+
1402+
fn is_rules_config_new_callee(callee: &Callee) -> bool {
1403+
matches!(callee, Callee::Qualified { namespace, name } if type_root_name(namespace) == "Config" && name == "new")
1404+
}
1405+
1406+
fn is_rules_config_rule_count_callee(callee: &Callee) -> bool {
1407+
matches!(callee, Callee::Qualified { namespace, name } if type_root_name(namespace) == "Config" && name == "rule_count")
1408+
}
1409+
1410+
fn is_global_config_new_callee(callee: &Callee) -> bool {
1411+
matches!(callee, Callee::Qualified { namespace, name } if type_root_name(namespace) == "GlobalConfig" && name == "new")
1412+
}
1413+
1414+
fn is_global_config_replace_callee(callee: &Callee) -> bool {
1415+
matches!(callee, Callee::Qualified { namespace, name } if type_root_name(namespace) == "GlobalConfig" && name == "replace")
1416+
}
1417+
1418+
fn is_global_config_rule_count_callee(callee: &Callee) -> bool {
1419+
matches!(callee, Callee::Qualified { namespace, name } if type_root_name(namespace) == "GlobalConfig" && name == "rule_count")
1420+
}
1421+
13751422
fn is_counter_new_callee(callee: &Callee) -> bool {
13761423
matches!(callee, Callee::Qualified { namespace, name } if type_root_name(namespace) == "Counter" && name == "new")
13771424
}

tests/checker.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,11 @@ fn bundled_core_interfaces_are_available_to_checker() {
112112
.iter()
113113
.any(|(path, _)| *path == "core/counter/counter.rssi")
114114
);
115+
assert!(
116+
core_interfaces()
117+
.iter()
118+
.any(|(path, _)| *path == "core/config/rules.rssi")
119+
);
115120
assert!(
116121
core_interfaces()
117122
.iter()
@@ -565,6 +570,31 @@ fn reload_config(path: read String, store: mut ConfigStore) -> Result<Unit, Conf
565570
assert!(rust.contains("rsscript_runtime::config_store_replace(store, &next);"));
566571
}
567572

573+
#[test]
574+
fn rust_lowering_maps_rules_config_reload_to_runtime_hooks() {
575+
let source = r#"
576+
fn load_rules_config(path: read String) -> Result<fresh Config, ConfigError> {
577+
let rules = RuleLoader.load_rules(path: read path)?
578+
return Ok(Config.new(name: read "rules", rules: read rules))
579+
}
580+
581+
fn reload_rules_config(path: read String, global: mut GlobalConfig) -> Result<Unit, ConfigError> {
582+
let next = load_rules_config(path: read path)?
583+
GlobalConfig.replace(global: mut global, value: read next)
584+
return Ok(Unit)
585+
}
586+
"#;
587+
let rust = lower_source_to_rust("rules-config.rss", source).expect("source should lower");
588+
589+
assert!(rust.contains("-> Result<rsscript_runtime::Config, rsscript_runtime::ConfigError>"));
590+
assert!(rust.contains("let rules = rsscript_runtime::rule_loader_load_rules(&path)?;"));
591+
assert!(
592+
rust.contains("return Ok(rsscript_runtime::config_new(&\"rules\".to_string(), &rules));")
593+
);
594+
assert!(rust.contains("global: &mut rsscript_runtime::GlobalConfig"));
595+
assert!(rust.contains("rsscript_runtime::global_config_replace(global, &next);"));
596+
}
597+
568598
#[test]
569599
fn rust_lowering_maps_counter_core_calls_to_runtime_hooks() {
570600
let source = r#"

0 commit comments

Comments
 (0)