Skip to main content

jzero/
lib.rs

1//! # jzero
2//!
3//! A compiler and bytecode VM for **Jzero** — a small subset of Java.
4//!
5//! Jzero supports classes, methods, control flow, basic types (`int`, `double`,
6//! `boolean`, `String`), arrays, string concatenation, and `System.out.println`.
7//!
8//! ## Quick start
9//!
10//! ```no_run
11//! use jzero::Compiler;
12//!
13//! let output = Compiler::new()
14//!     .source("public class hello {
15//!         public static void main(String argv[]) {
16//!             System.out.println(\"hello, jzero!\");
17//!         }
18//!     }")
19//!     .run(&[])
20//!     .unwrap();
21//!
22//! assert_eq!(output.stdout, "hello, jzero!\n");
23//! ```
24//!
25//! ## Pipeline
26//!
27//! ```text
28//! source code
29//!     → parse_tree()       [jzero-parser]
30//!     → analyze()          [jzero-semantic]
31//!     → generate()         [jzero-codegen]  → TAC IR
32//!     → compile_bytecode() [jzero-codegen]  → .j0 binary
33//!     → run()              [jzero-vm]       → stdout
34//! ```
35
36use jzero_ast::tree::reset_ids;
37
38// ─── Re-exports ───────────────────────────────────────────────────────────────
39
40pub use jzero_semantic::SemanticResult;
41pub use jzero_codegen::pipeline::BytecodeOutput;
42pub use jzero_codegen::CodegenContext;
43
44// ─── CompileOutput ────────────────────────────────────────────────────────────
45
46/// The result of a full compile + execute run.
47#[derive(Debug, Clone)]
48pub struct RunOutput {
49    /// Text written to stdout by the Jzero program.
50    pub stdout: String,
51}
52
53/// The result of compiling to bytecode without executing.
54#[derive(Debug)]
55pub struct CompileOutput {
56    /// The raw `.j0` binary image.
57    pub binary: Vec<u8>,
58    /// Human-readable bytecode assembler listing.
59    pub text: String,
60    /// The TAC assembler listing (intermediate code).
61    pub tac: String,
62}
63
64// ─── Error ────────────────────────────────────────────────────────────────────
65
66/// A Jzero compilation or runtime error.
67#[derive(Debug, Clone)]
68pub struct JzeroError(pub String);
69
70impl std::fmt::Display for JzeroError {
71    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72        write!(f, "{}", self.0)
73    }
74}
75
76impl std::error::Error for JzeroError {}
77
78// ─── Compiler ────────────────────────────────────────────────────────────────
79
80/// The Jzero compiler.
81///
82/// Construct with [`Compiler::new`], provide source code with [`Compiler::source`],
83/// then call [`Compiler::run`], [`Compiler::compile`], or [`Compiler::tac`].
84#[derive(Default)]
85pub struct Compiler {
86    source: String,
87}
88
89impl Compiler {
90    /// Create a new compiler instance.
91    pub fn new() -> Self {
92        Compiler::default()
93    }
94
95    /// Set the Jzero source code to compile.
96    pub fn source(mut self, src: &str) -> Self {
97        self.source = src.to_string();
98        self
99    }
100
101    /// Parse and semantically analyse the source, returning any errors.
102    ///
103    /// This is the first step in the pipeline and is called internally
104    /// by all other methods.
105    fn analyse(&self) -> Result<(jzero_ast::tree::Tree, SemanticResult), JzeroError> {
106        reset_ids();
107        let mut tree = jzero_parser::parse_tree(&self.source)
108            .map_err(|e| JzeroError(e.to_string()))?;
109        let sem = jzero_semantic::analyze(&mut tree);
110        if !sem.errors.is_empty() {
111            let msg = sem.errors.iter()
112                .map(|e| e.to_string())
113                .collect::<Vec<_>>()
114                .join("\n");
115            return Err(JzeroError(msg));
116        }
117        Ok((tree, sem))
118    }
119
120    /// Compile to TAC intermediate code and return the assembler listing.
121    ///
122    /// # Errors
123    /// Returns a [`JzeroError`] if parsing or semantic analysis fails.
124    pub fn tac(&self) -> Result<String, JzeroError> {
125        let (tree, sem) = self.analyse()?;
126        let ctx = jzero_codegen::generate(&tree, &sem);
127        Ok(jzero_codegen::emit::emit(&tree, &ctx))
128    }
129
130    /// Compile to bytecode and return the binary image + assembler listing.
131    ///
132    /// `argc` is the number of command-line arguments to pass to `main()`.
133    ///
134    /// # Errors
135    /// Returns a [`JzeroError`] if parsing or semantic analysis fails.
136    pub fn compile(&self, argc: i64) -> Result<CompileOutput, JzeroError> {
137        let (tree, sem) = self.analyse()?;
138        let ctx    = jzero_codegen::generate(&tree, &sem);
139        let tac    = jzero_codegen::emit::emit(&tree, &ctx);
140        let output = jzero_codegen::pipeline::compile_bytecode(&tree, &ctx, argc);
141        Ok(CompileOutput {
142            binary: output.binary,
143            text:   output.text,
144            tac,
145        })
146    }
147
148    /// Compile and execute in the VM.
149    ///
150    /// `args` are passed as `argv` to the Jzero `main()` method,
151    /// so `args.len()` determines `argv.length`.
152    ///
153    /// # Errors
154    /// Returns a [`JzeroError`] if parsing, semantic analysis, or VM execution fails.
155    pub fn run(&self, args: &[&str]) -> Result<RunOutput, JzeroError> {
156        let owned: Vec<String> = args.iter().map(|s| s.to_string()).collect();
157        let argc = owned.len() as i64;
158        let (tree, sem) = self.analyse()?;
159        let ctx    = jzero_codegen::generate(&tree, &sem);
160        let output = jzero_codegen::pipeline::compile_bytecode(&tree, &ctx, argc);
161        let stdout = jzero_vm::run(&output.binary, &owned)
162            .map_err(|e| JzeroError(e))?;
163        Ok(RunOutput { stdout })
164    }
165}
166
167// ─── Tests ───────────────────────────────────────────────────────────────────
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172
173    const HELLO: &str = r#"
174        public class hello {
175            public static void main(String argv[]) {
176                System.out.println("hello, jzero!");
177            }
178        }
179    "#;
180
181    const HELLO_LOOP: &str = r#"
182        public class hello_loop {
183            public static void main(String argv[]) {
184                int x;
185                x = argv.length;
186                x = x + 2;
187                while (x > 3) {
188                    System.out.println("hello, jzero!");
189                    x = x - 1;
190                }
191            }
192        }
193    "#;
194
195    const CONCAT: &str = r#"
196        public class concat {
197            public static void main(String argv[]) {
198                String s;
199                s = "hello, " + "jzero!";
200                System.out.println(s);
201            }
202        }
203    "#;
204
205    #[test]
206    fn hello_world_runs() {
207        let out = Compiler::new().source(HELLO).run(&[]).unwrap();
208        assert_eq!(out.stdout, "hello, jzero!\n");
209    }
210
211    #[test]
212    fn hello_loop_runs_four_times() {
213        let out = Compiler::new()
214            .source(HELLO_LOOP)
215            .run(&["a", "b", "c", "d", "e"])
216            .unwrap();
217        assert_eq!(out.stdout, "hello, jzero!\n".repeat(4));
218    }
219
220    #[test]
221    fn string_concat_runs() {
222        let out = Compiler::new().source(CONCAT).run(&[]).unwrap();
223        assert_eq!(out.stdout, "hello, jzero!\n");
224    }
225
226    #[test]
227    fn tac_contains_proc_main() {
228        let tac = Compiler::new().source(HELLO).tac().unwrap();
229        assert!(tac.contains("proc main"));
230    }
231
232    #[test]
233    fn compile_binary_has_magic() {
234        let out = Compiler::new().source(HELLO).compile(0).unwrap();
235        assert_eq!(&out.binary[0..8], b"Jzero!!\0");
236    }
237
238    #[test]
239    fn semantic_error_is_reported() {
240        let src = r#"
241            public class bad {
242                public static void main(String argv[]) {
243                    int x;
244                    x = "not an int";
245                }
246            }
247        "#;
248        // Type mismatch: semantic analysis passes (no hard errors for type
249        // mismatches in our current implementation) but the TAC is still produced.
250        // This test just ensures the pipeline doesn't panic.
251        let _ = Compiler::new().source(src).tac();
252    }
253
254    #[test]
255    fn parse_error_returns_err() {
256        let result = Compiler::new().source("this is not valid jzero").run(&[]);
257        assert!(result.is_err());
258    }
259}