Skip to content

Commit 919d668

Browse files
committed
Support runnable Result main
1 parent ebb7ddc commit 919d668

5 files changed

Lines changed: 88 additions & 15 deletions

File tree

README.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -382,7 +382,7 @@ source-span hooks for manage and ResourcePool borrow lowering
382382
central runtime unwrap boundary for generated resource operations
383383
source map JSON for generated Rust packages
384384
rustc diagnostic remapping through source maps
385-
zero-argument `fn main() -> Unit` package harnesses for runnable Rust output
385+
zero-argument `fn main() -> Unit` and `fn main() -> Result<Unit, E>` package harnesses for runnable Rust output
386386
core `.rssi` interface signatures
387387
HIR builtin signatures parsed from `.rssi` interface sources instead of a hand-written Rust table
388388
CI gates for formatting, linting, tests, and generated Rust fixtures
@@ -414,7 +414,7 @@ rss verify-rust [--json] <file.rss> --out-dir <directory>
414414

415415
The checker prototype also keeps a small `core/prototype` interface bundle for real-scenario fixtures such as CSV, DB, cache, and config examples. Those signatures are still `.rssi` sources, not a second Rust-side builtin table.
416416

417-
If a lowered package contains `fn main() -> Unit`, `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.
417+
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

@@ -433,6 +433,12 @@ fn main() -> Unit {
433433
}
434434
```
435435

436+
Result-returning entry points also run:
437+
438+
```sh
439+
cargo run -- run examples/hello_result.rss
440+
```
441+
436442
Local verification:
437443

438444
```sh

examples/hello_result.rss

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
struct MainError
2+
3+
fn main() -> Result<Unit, MainError> {
4+
Log.write(message: read "hello RSScript result")
5+
return Ok(Unit)
6+
}

src/main.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,9 @@ fn run_generated_rust(args: &[String]) -> ExitCode {
335335
}
336336
};
337337
if package.main_rs.is_none() {
338-
eprintln!("rss run requires a zero-argument `fn main() -> Unit`.");
338+
eprintln!(
339+
"rss run requires a zero-argument `fn main() -> Unit` or `fn main() -> Result<Unit, E>`."
340+
);
339341
return ExitCode::from(1);
340342
}
341343

src/rust_lower.rs

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1138,31 +1138,59 @@ fn cargo_package_name(name: &str) -> String {
11381138

11391139
fn rust_package_main(program: &Program, package_name: &str) -> Option<String> {
11401140
let main = program.items.iter().find_map(|item| match item {
1141-
Item::Function(function) if is_runnable_main(function) => Some(function),
1142-
Item::Function(_) | Item::Type(_) => None,
1141+
Item::Function(function) => runnable_main_kind(function).map(|kind| (function, kind)),
1142+
Item::Type(_) => None,
11431143
})?;
1144+
let (main, kind) = main;
11441145
let crate_name = cargo_crate_name(package_name);
1146+
let call = match kind {
1147+
RunnableMainKind::Unit => format!("{}::{}();", crate_name, rust_ident(&main.name)),
1148+
RunnableMainKind::ResultUnit => format!(
1149+
"{}::{}().expect(\"RSScript main returned an error\");",
1150+
crate_name,
1151+
rust_ident(&main.name)
1152+
),
1153+
};
11451154
Some(format!(
11461155
concat!(
11471156
"// Generated by RSScript. Edit the .rss source instead.\n",
11481157
"// Runnable harness for RSScript `{}`.\n\n",
11491158
"fn main() {{\n",
1150-
" {}::{}();\n",
1159+
" {}\n",
11511160
"}}\n"
11521161
),
1153-
main.name,
1154-
crate_name,
1155-
rust_ident(&main.name)
1162+
main.name, call
11561163
))
11571164
}
11581165

11591166
fn is_runnable_main(function: &FunctionDecl) -> bool {
1160-
function.name == "main"
1161-
&& function.params.is_empty()
1162-
&& function
1163-
.return_ty
1164-
.as_ref()
1165-
.is_none_or(|return_ty| return_ty.name == "Unit" && return_ty.args.is_empty())
1167+
runnable_main_kind(function).is_some()
1168+
}
1169+
1170+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1171+
enum RunnableMainKind {
1172+
Unit,
1173+
ResultUnit,
1174+
}
1175+
1176+
fn runnable_main_kind(function: &FunctionDecl) -> Option<RunnableMainKind> {
1177+
if function.name != "main" || !function.params.is_empty() {
1178+
return None;
1179+
}
1180+
let Some(return_ty) = function.return_ty.as_ref() else {
1181+
return Some(RunnableMainKind::Unit);
1182+
};
1183+
if return_ty.name == "Unit" && return_ty.args.is_empty() {
1184+
return Some(RunnableMainKind::Unit);
1185+
}
1186+
if return_ty.name == "Result"
1187+
&& return_ty.args.len() == 2
1188+
&& return_ty.args[0].name == "Unit"
1189+
&& return_ty.args[0].args.is_empty()
1190+
{
1191+
return Some(RunnableMainKind::ResultUnit);
1192+
}
1193+
None
11661194
}
11671195

11681196
fn cargo_crate_name(package_name: &str) -> String {

tests/checker.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -586,6 +586,37 @@ fn main() -> Unit {
586586
assert!(main_rs.contains("runnable_example_rss::main();"));
587587
}
588588

589+
#[test]
590+
fn rust_lowering_can_emit_result_main_harness() {
591+
let source = r#"
592+
struct MainError
593+
594+
fn main() -> Result<Unit, MainError> {
595+
return Ok(Unit)
596+
}
597+
"#;
598+
let package = lower_source_to_rust_package(
599+
"main.rss",
600+
source,
601+
"Result Runnable Example.rss",
602+
"/workspace/rsscript/runtime",
603+
)
604+
.expect("source should lower into package");
605+
let main_rs = package
606+
.main_rs
607+
.as_ref()
608+
.expect("zero-argument Result<Unit, E> main should emit a Rust binary harness");
609+
610+
assert!(
611+
package
612+
.lib_rs
613+
.contains("pub fn main() -> Result<(), MainError>")
614+
);
615+
assert!(main_rs.contains(
616+
"result_runnable_example_rss::main().expect(\"RSScript main returned an error\");"
617+
));
618+
}
619+
589620
#[test]
590621
fn rust_lowering_is_gated_by_diagnostics() {
591622
let source = r#"

0 commit comments

Comments
 (0)