Skip to main content

ruso_script/
lib.rs

1//! Parse Ruso Scripting Language (RSL) source into an AST and compile to
2//! `ruso-runtime` bytecode.
3//!
4//! # Developer documentation
5//!
6//! - [Language reference](https://docs.ruso.hopeless-labs.com/rsl/reference.html)
7//! - [Compiler](https://docs.ruso.hopeless-labs.com/internals/compiler.html)
8//! - [Examples](https://docs.ruso.hopeless-labs.com/rsl/examples.html)
9
10// `ParseError::Pest` wraps `pest::error::Error`, which carries spans + rule
11// chain for useful diagnostics and is naturally large. Boxing it would obscure
12// the public error type at call sites; the size lint is noise here.
13#![allow(clippy::result_large_err)]
14
15mod compile;
16pub mod script;
17mod spec_build;
18
19pub use compile::{CompileError, compile};
20pub use ruso_runtime::{
21    BytecodeProgram, EvidenceKind, ExtractSource, QualifiedMatch, Severity, encode_bytecode,
22};
23pub use script::ast::{self, Program, Stmt};
24pub use script::{ParseError, parse};
25
26use std::path::Path;
27
28#[derive(Debug, thiserror::Error)]
29pub enum LoadError {
30    #[error("failed to read {}: {source}", path.display())]
31    Io {
32        path: std::path::PathBuf,
33        source: std::io::Error,
34    },
35    #[error("failed to parse {}: {source}", path.display())]
36    Parse {
37        path: std::path::PathBuf,
38        source: ParseError,
39    },
40}
41
42pub fn load_program(path: &Path) -> Result<Program, LoadError> {
43    let source = std::fs::read_to_string(path).map_err(|err| LoadError::Io {
44        path: path.to_path_buf(),
45        source: err,
46    })?;
47    parse(&source).map_err(|err| LoadError::Parse {
48        path: path.to_path_buf(),
49        source: err,
50    })
51}
52
53pub fn compile_program(program: &Program) -> Result<BytecodeProgram, CompileError> {
54    compile(program)
55}
56
57pub fn compile_to_bytes(program: &Program) -> Result<Vec<u8>, CompileError> {
58    Ok(encode_bytecode(&compile_program(program)?))
59}
60
61pub async fn run(
62    program: &Program,
63    config: ruso_runtime::ExecutorConfig,
64) -> Result<ruso_runtime::ExecutionResult, ruso_runtime::RuntimeError> {
65    let bytecode =
66        compile_program(program).map_err(|e| ruso_runtime::RuntimeError::Other(e.to_string()))?;
67    ruso_runtime::Executor::from_bytecode(config, bytecode)?
68        .run()
69        .await
70}
71
72pub async fn run_bytecode(
73    bytecode: &BytecodeProgram,
74    config: ruso_runtime::ExecutorConfig,
75) -> Result<ruso_runtime::ExecutionResult, ruso_runtime::RuntimeError> {
76    ruso_runtime::Executor::from_bytecode(config, bytecode.clone())?
77        .run()
78        .await
79}
80
81/// Run a pre-shared [`Arc<BytecodeProgram>`] against a single target.
82///
83/// Prefer this over [`run_bytecode`] when running the same compiled script
84/// against many targets — the program is cloned via `Arc::clone` (a
85/// reference-count bump) instead of being deep-copied for each run.
86pub async fn run_program(
87    bytecode: std::sync::Arc<BytecodeProgram>,
88    config: ruso_runtime::ExecutorConfig,
89) -> Result<ruso_runtime::ExecutionResult, ruso_runtime::RuntimeError> {
90    ruso_runtime::Executor::from_program(config, bytecode)?
91        .run()
92        .await
93}
94
95pub async fn run_bytes(
96    bytes: &[u8],
97    config: ruso_runtime::ExecutorConfig,
98) -> Result<ruso_runtime::ExecutionResult, ruso_runtime::RuntimeError> {
99    ruso_runtime::Executor::from_bytes(config, bytes)?
100        .run()
101        .await
102}
103
104#[cfg(test)]
105mod tests {
106    use std::path::PathBuf;
107
108    use ruso_runtime::{bytes_to_hex, decode_bytecode, hex_to_bytes};
109
110    use super::*;
111    use crate::script::ast::Stmt;
112
113    #[test]
114    fn load_program_valid_example() {
115        let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("examples/http_status_ok.rsl");
116        let program = load_program(&path).expect("example script should parse");
117        assert!(!program.statements.is_empty());
118    }
119
120    #[test]
121    fn bytecode_roundtrip_http_example() {
122        let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("examples/http_status_ok.rsl");
123        let program = load_program(&path).unwrap();
124        let original = compile_program(&program).unwrap();
125        let bytes = encode_bytecode(&original);
126        let restored = decode_bytecode(&bytes).unwrap();
127        assert_eq!(original.code, restored.code);
128        assert_eq!(original.strings, restored.strings);
129        assert_eq!(original.matchers, restored.matchers);
130    }
131
132    #[test]
133    fn hex_bytes_roundtrip() {
134        let program = Program {
135            statements: vec![Stmt::Name("Hex test".into()), Stmt::Severity(Severity::Low)],
136        };
137        let bytes = compile_to_bytes(&program).unwrap();
138        let hex = bytes_to_hex(&bytes);
139        let restored = hex_to_bytes(&hex).expect("hex decode");
140        let decoded = decode_bytecode(&restored).expect("bytecode decode");
141        assert_eq!(decoded.spec.metadata.name.as_deref(), Some("Hex test"));
142    }
143}