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/// Error from [`load_program`]: reading the file failed, or its contents did
29/// not parse.
30#[derive(Debug, thiserror::Error)]
31pub enum LoadError {
32    /// The source file could not be read.
33    #[error("failed to read {}: {source}", path.display())]
34    Io {
35        /// The path that failed to read.
36        path: std::path::PathBuf,
37        /// The underlying I/O error.
38        source: std::io::Error,
39    },
40    /// The source file was read but did not parse as RSL.
41    #[error("failed to parse {}: {source}", path.display())]
42    Parse {
43        /// The path that failed to parse.
44        path: std::path::PathBuf,
45        /// The underlying parse error.
46        source: ParseError,
47    },
48}
49
50/// Read and parse an `.rsl` file into a [`Program`] AST.
51pub fn load_program(path: &Path) -> Result<Program, LoadError> {
52    let source = std::fs::read_to_string(path).map_err(|err| LoadError::Io {
53        path: path.to_path_buf(),
54        source: err,
55    })?;
56    parse(&source).map_err(|err| LoadError::Parse {
57        path: path.to_path_buf(),
58        source: err,
59    })
60}
61
62/// Compile a parsed [`Program`] into a [`BytecodeProgram`].
63pub fn compile_program(program: &Program) -> Result<BytecodeProgram, CompileError> {
64    compile(program)
65}
66
67/// Compile a [`Program`] and serialize it to a raw `.rbc` byte buffer.
68pub fn compile_to_bytes(program: &Program) -> Result<Vec<u8>, CompileError> {
69    Ok(encode_bytecode(&compile_program(program)?))
70}
71
72/// Compile a [`Program`] and run it against a single target in one step.
73pub async fn run(
74    program: &Program,
75    config: ruso_runtime::ExecutorConfig,
76) -> Result<ruso_runtime::ExecutionResult, ruso_runtime::RuntimeError> {
77    let bytecode =
78        compile_program(program).map_err(|e| ruso_runtime::RuntimeError::Other(e.to_string()))?;
79    ruso_runtime::Executor::from_bytecode(config, bytecode)?
80        .run()
81        .await
82}
83
84/// Run an already-compiled [`BytecodeProgram`] against a single target.
85pub async fn run_bytecode(
86    bytecode: &BytecodeProgram,
87    config: ruso_runtime::ExecutorConfig,
88) -> Result<ruso_runtime::ExecutionResult, ruso_runtime::RuntimeError> {
89    ruso_runtime::Executor::from_bytecode(config, bytecode.clone())?
90        .run()
91        .await
92}
93
94/// Run a pre-shared `Arc<BytecodeProgram>` against a single target.
95///
96/// Prefer this over [`run_bytecode`] when running the same compiled script
97/// against many targets — the program is cloned via `Arc::clone` (a
98/// reference-count bump) instead of being deep-copied for each run.
99pub async fn run_program(
100    bytecode: std::sync::Arc<BytecodeProgram>,
101    config: ruso_runtime::ExecutorConfig,
102) -> Result<ruso_runtime::ExecutionResult, ruso_runtime::RuntimeError> {
103    ruso_runtime::Executor::from_program(config, bytecode)?
104        .run()
105        .await
106}
107
108/// Decode a raw `.rbc` byte buffer and run it against a single target.
109pub async fn run_bytes(
110    bytes: &[u8],
111    config: ruso_runtime::ExecutorConfig,
112) -> Result<ruso_runtime::ExecutionResult, ruso_runtime::RuntimeError> {
113    ruso_runtime::Executor::from_bytes(config, bytes)?
114        .run()
115        .await
116}
117
118#[cfg(test)]
119mod tests {
120    use std::path::PathBuf;
121
122    use ruso_runtime::{bytes_to_hex, decode_bytecode, hex_to_bytes};
123
124    use super::*;
125    use crate::script::ast::Stmt;
126
127    #[test]
128    fn load_program_valid_example() {
129        let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("examples/http_status_ok.rsl");
130        let program = load_program(&path).expect("example script should parse");
131        assert!(!program.statements.is_empty());
132    }
133
134    #[test]
135    fn bytecode_roundtrip_http_example() {
136        let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("examples/http_status_ok.rsl");
137        let program = load_program(&path).unwrap();
138        let original = compile_program(&program).unwrap();
139        let bytes = encode_bytecode(&original);
140        let restored = decode_bytecode(&bytes).unwrap();
141        assert_eq!(original.code, restored.code);
142        assert_eq!(original.strings, restored.strings);
143        assert_eq!(original.matchers, restored.matchers);
144    }
145
146    #[test]
147    fn hex_bytes_roundtrip() {
148        let program = Program {
149            statements: vec![Stmt::Name("Hex test".into()), Stmt::Severity(Severity::Low)],
150        };
151        let bytes = compile_to_bytes(&program).unwrap();
152        let hex = bytes_to_hex(&bytes);
153        let restored = hex_to_bytes(&hex).expect("hex decode");
154        let decoded = decode_bytecode(&restored).expect("bytecode decode");
155        assert_eq!(decoded.spec.metadata.name.as_deref(), Some("Hex test"));
156    }
157}