Skip to main content

probar_js_gen/
lib.rs

1//! NASA/DO-178B-grade Rust DSL for type-safe JavaScript generation.
2//!
3//! # Overview
4//!
5//! `probar-js-gen` provides a type-safe DSL for generating JavaScript code
6//! with compile-time validation, runtime verification, and immutability
7//! enforcement. The generated code is deterministic and verifiable.
8//!
9//! # Key Features
10//!
11//! - **Type Safety**: Invalid JS constructs are unrepresentable
12//! - **Identifier Validation**: Reserved words and invalid chars are rejected
13//! - **Immutability**: Generated files include hash manifests
14//! - **Determinism**: Same input always produces same output
15//!
16//! # Example
17//!
18//! ```rust,no_run
19//! use probar_js_gen::prelude::*;
20//!
21//! // Build a JavaScript module
22//! let module = JsModuleBuilder::new()
23//!     .comment("Generated by probar-js-gen")
24//!     .let_decl("x", Expr::num(42)).unwrap()
25//!     .const_decl("msg", Expr::str("hello")).unwrap()
26//!     .build();
27//!
28//! // Generate JavaScript code
29//! let js = generate(&module);
30//! assert!(js.contains("let x = 42;"));
31//! ```
32//!
33//! # Safety Model
34//!
35//! The crate enforces several safety properties:
36//!
37//! 1. **No Raw JS**: All JavaScript is generated from typed HIR
38//! 2. **No Reserved Words**: Identifier::new() rejects `class`, `function`, etc.
39//! 3. **No Invalid Chars**: Identifiers are validated at construction
40//! 4. **Manifest Verification**: Generated files are hash-verified
41//!
42//! # References
43//!
44//! ## Formal Methods
45//! - McKeeman, W.M. (1998) "Differential Testing for Software"
46//! - Claessen, K. & Hughes, J. (2000) "QuickCheck"
47//! - DeMillo, R.A. et al. (1978) "Hints on Test Data Selection"
48//!
49//! ## JavaScript Semantics
50//! - Maffeis et al. (2008) "An Operational Semantics for JavaScript"
51//! - Guha et al. (2010) "The Essence of JavaScript"
52//! - ECMA-262 (ES2022) Specification
53//!
54//! ## Software Safety
55//! - Leveson, N. (2012) "Engineering a Safer World"
56//! - DO-178C (2011) "Software Considerations in Airborne Systems"
57
58#![warn(missing_docs)]
59#![allow(clippy::doc_markdown)] // Allow citation names without backticks
60
61pub mod builder;
62pub mod codegen;
63pub mod error;
64pub mod hir;
65pub mod manifest;
66
67pub use error::{JsGenError, Result};
68
69/// Prelude module for convenient imports.
70///
71/// # Example
72///
73/// ```rust
74/// use probar_js_gen::prelude::*;
75/// ```
76pub mod prelude {
77    pub use crate::builder::{JsClassBuilder, JsModuleBuilder, JsSwitchBuilder};
78    pub use crate::codegen::generate;
79    pub use crate::error::{JsGenError, Result};
80    pub use crate::hir::{
81        BinOp, Expr, GenerationMetadata, Identifier, JsClass, JsMethod, JsModule, JsSwitch, Stmt,
82        UnaryOp,
83    };
84    pub use crate::manifest::{verify, write_with_manifest, FileManifest};
85}
86
87/// Validator for generated JavaScript.
88///
89/// Checks for patterns that indicate bugs in code generation.
90pub mod validator {
91    /// Patterns that should never appear in generated code.
92    pub const FORBIDDEN_PATTERNS: &[&str] = &[
93        "window.",       // Workers have no window
94        "window)",       // Same
95        "window,",       // Same
96        "document.",     // Workers have no document
97        "importScripts", // Use dynamic import() instead
98        "eval(",         // Security risk
99        "Function(",     // Security risk
100        "with(",         // Deprecated
101        "__proto__",     // Prototype pollution risk
102    ];
103
104    /// Patterns that must appear in Worker code.
105    pub const REQUIRED_WORKER_PATTERNS: &[&str] = &[
106        "self.",   // Worker global
107        "import(", // Dynamic import for modules
108    ];
109
110    /// Validate generated JavaScript for Worker context.
111    ///
112    /// # Errors
113    ///
114    /// Returns list of validation errors found.
115    pub fn validate_worker_js(js: &str) -> Vec<String> {
116        let mut errors = Vec::new();
117
118        // Check forbidden patterns
119        for pattern in FORBIDDEN_PATTERNS {
120            if js.contains(pattern) {
121                errors.push(format!("Forbidden pattern found: '{pattern}'"));
122            }
123        }
124
125        // Check required patterns
126        for pattern in REQUIRED_WORKER_PATTERNS {
127            if !js.contains(pattern) {
128                errors.push(format!("Required pattern missing: '{pattern}'"));
129            }
130        }
131
132        errors
133    }
134
135    /// Validate generated JavaScript for AudioWorklet context.
136    ///
137    /// # Errors
138    ///
139    /// Returns list of validation errors found.
140    pub fn validate_worklet_js(js: &str) -> Vec<String> {
141        let mut errors = Vec::new();
142
143        // Check forbidden patterns
144        for pattern in FORBIDDEN_PATTERNS {
145            if js.contains(pattern) {
146                errors.push(format!("Forbidden pattern found: '{pattern}'"));
147            }
148        }
149
150        // Worklet-specific requirements
151        if !js.contains("extends AudioWorkletProcessor") {
152            errors.push("Missing AudioWorkletProcessor base class".to_string());
153        }
154        if !js.contains("registerProcessor") {
155            errors.push("Missing registerProcessor call".to_string());
156        }
157        if !js.contains("process(") {
158            errors.push("Missing process() method".to_string());
159        }
160
161        errors
162    }
163
164    #[cfg(test)]
165    mod tests {
166        use super::*;
167
168        #[test]
169        fn forbidden_patterns_detected() {
170            let js = "window.alert('test')";
171            let errors = validate_worker_js(js);
172            assert!(!errors.is_empty());
173            assert!(errors[0].contains("window."));
174        }
175
176        #[test]
177        fn valid_worker_js() {
178            let js = r#"
179                self.onmessage = async function(e) {
180                    const mod = await import("./module.js");
181                };
182            "#;
183            let errors = validate_worker_js(js);
184            assert!(errors.is_empty(), "Errors: {:?}", errors);
185        }
186
187        #[test]
188        fn valid_worklet_js() {
189            let js = r#"
190                class MyProcessor extends AudioWorkletProcessor {
191                    process(inputs, outputs, params) {
192                        return true;
193                    }
194                }
195                registerProcessor("my-processor", MyProcessor);
196            "#;
197            // Note: worklet doesn't need self. or import(
198            let errors = validate_worklet_js(js);
199            assert!(errors.is_empty(), "Errors: {:?}", errors);
200        }
201    }
202}
203
204#[cfg(test)]
205#[allow(clippy::unwrap_used)]
206mod integration_tests {
207    use super::prelude::*;
208
209    #[test]
210    fn end_to_end_worker_generation() {
211        let module = JsModuleBuilder::new()
212            .comment("Whisper.apr Transcription Worker (ES Module)")
213            .comment("Generated by probar-js-gen - DO NOT EDIT MANUALLY")
214            .let_decl("wasm", Expr::null())
215            .unwrap()
216            .let_decl("worker", Expr::null())
217            .unwrap()
218            .let_decl("initialized", Expr::bool(false))
219            .unwrap()
220            .stmt(Stmt::on_message(vec![
221                Stmt::const_decl("msg", Expr::ident("e").unwrap().dot("data").unwrap()).unwrap(),
222                Stmt::if_then(
223                    Expr::ident("msg")
224                        .unwrap()
225                        .dot("type")
226                        .unwrap()
227                        .eq(Expr::str("bootstrap")),
228                    vec![Stmt::expr(
229                        Expr::ident("console")
230                            .unwrap()
231                            .dot("log")
232                            .unwrap()
233                            .call(vec![Expr::str("[Worker] Bootstrap received")]),
234                    )],
235                ),
236            ]))
237            .expr(
238                Expr::ident("console")
239                    .unwrap()
240                    .dot("log")
241                    .unwrap()
242                    .call(vec![Expr::str(
243                        "[Worker] Module loaded, waiting for bootstrap",
244                    )]),
245            )
246            .build();
247
248        let js = generate(&module);
249
250        // Must have self (Worker global)
251        assert!(js.contains("self.onmessage"), "Missing self.onmessage");
252
253        // Must NOT have window
254        assert!(!js.contains("window"), "Has window (forbidden)");
255    }
256
257    #[test]
258    fn end_to_end_worklet_generation() {
259        let class = JsClassBuilder::new("WhisperProcessor")
260            .unwrap()
261            .extends("AudioWorkletProcessor")
262            .unwrap()
263            .constructor(vec![Stmt::member_assign(
264                Expr::this(),
265                "buffer",
266                Expr::null(),
267            )
268            .unwrap()])
269            .method(
270                "process",
271                &["inputs", "outputs", "params"],
272                vec![Stmt::ret_val(Expr::bool(true))],
273            )
274            .unwrap()
275            .build();
276
277        let module = JsModuleBuilder::new()
278            .comment("WhisperAudioProcessor - AudioWorklet for real-time audio capture")
279            .class(class)
280            .register_processor("whisper-audio-processor", "WhisperProcessor")
281            .unwrap()
282            .build();
283
284        let js = generate(&module);
285
286        // Must have required elements
287        assert!(js.contains("extends AudioWorkletProcessor"));
288        assert!(js.contains("super();"));
289        assert!(js.contains("process(inputs, outputs, params)"));
290        assert!(js.contains("return true;"));
291        assert!(js.contains("registerProcessor"));
292
293        // Must NOT have forbidden elements
294        assert!(!js.contains("window"));
295        assert!(!js.contains("document"));
296    }
297
298    #[test]
299    fn deterministic_generation() {
300        let build_module = || {
301            JsModuleBuilder::new()
302                .let_decl("x", Expr::num(1))
303                .unwrap()
304                .const_decl("y", Expr::str("test"))
305                .unwrap()
306                .build()
307        };
308
309        let js1 = generate(&build_module());
310        let js2 = generate(&build_module());
311
312        assert_eq!(js1, js2, "Generation must be deterministic");
313    }
314
315    #[test]
316    fn identifier_validation_rejects_reserved() {
317        assert!(Identifier::new("class").is_err());
318        assert!(Identifier::new("function").is_err());
319        assert!(Identifier::new("await").is_err());
320        assert!(Identifier::new("import").is_err());
321    }
322
323    #[test]
324    fn identifier_validation_rejects_invalid() {
325        assert!(Identifier::new("123start").is_err());
326        assert!(Identifier::new("has-dash").is_err());
327        assert!(Identifier::new("").is_err());
328    }
329
330    #[test]
331    fn identifier_validation_accepts_valid() {
332        assert!(Identifier::new("validName").is_ok());
333        assert!(Identifier::new("_private").is_ok());
334        assert!(Identifier::new("$jquery").is_ok());
335        assert!(Identifier::new("name123").is_ok());
336    }
337}