Skip to main content

pepl_compiler/
lib.rs

1//! PEPL compiler: orchestrates the full compilation pipeline.
2//!
3//! ```text
4//! PEPL Source → Lexer → Parser → Type Checker → Invariant Checker → WASM Codegen → .wasm
5//! ```
6//!
7//! # Two entry points
8//!
9//! - [`type_check`] — Parse + type-check only, returning structured errors.
10//! - [`compile`] — Full pipeline: parse → type-check → codegen → `.wasm` bytes.
11//! - [`compile_to_result`] — Full pipeline returning a [`CompileResult`] (JSON-serializable).
12
13pub mod checker;
14pub mod env;
15pub mod reference;
16pub mod stdlib;
17pub mod ty;
18
19use pepl_codegen::CodegenError;
20use pepl_types::ast::Program;
21use pepl_types::{CompileErrors, SourceFile};
22use serde::{Deserialize, Serialize};
23use sha2::{Digest, Sha256};
24
25// ── Version constants ─────────────────────────────────────────────────────────
26
27/// PEPL language version (Phase 0).
28pub const PEPL_LANGUAGE_VERSION: &str = "0.1.0";
29
30/// Compiler version (matches Cargo package version).
31pub const PEPL_COMPILER_VERSION: &str = env!("CARGO_PKG_VERSION");
32
33// ── CompileResult ─────────────────────────────────────────────────────────────
34
35/// A declared state field with name and type.
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct FieldInfo {
38    pub name: String,
39    #[serde(rename = "type")]
40    pub ty: String,
41}
42
43/// A declared action with name and parameter types.
44#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct ActionInfo {
46    pub name: String,
47    pub params: Vec<FieldInfo>,
48}
49
50/// The result of a full compilation pipeline.
51///
52/// Serializable to JSON for the host / playground.
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct CompileResult {
55    /// Whether compilation succeeded.
56    pub success: bool,
57    /// The compiled `.wasm` bytes (base64-encoded in JSON), if successful.
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub wasm: Option<Vec<u8>>,
60    /// Structured compile errors, if any.
61    pub errors: CompileErrors,
62
63    // ── Enrichment fields (Phase 10.1) ─────────────────────────────────
64
65    /// Full AST (serializable to JSON).
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub ast: Option<Program>,
68
69    /// SHA-256 hash of the source text (hex-encoded).
70    pub source_hash: String,
71
72    /// SHA-256 hash of the compiled WASM bytes (hex-encoded), if available.
73    #[serde(skip_serializing_if = "Option::is_none")]
74    pub wasm_hash: Option<String>,
75
76    /// State field names and types.
77    pub state_fields: Vec<FieldInfo>,
78
79    /// Action names and parameter types.
80    pub actions: Vec<ActionInfo>,
81
82    /// View names.
83    pub views: Vec<String>,
84
85    /// Declared required capabilities.
86    pub capabilities: Vec<String>,
87
88    /// Declared credentials (name + type).
89    pub credentials: Vec<FieldInfo>,
90
91    /// PEPL language version.
92    pub language_version: String,
93
94    /// Compiler version.
95    pub compiler_version: String,
96
97    /// Warnings from compilation (separate from errors).
98    pub warnings: Vec<pepl_types::PeplError>,
99
100    /// Source map: WASM function index → PEPL source location.
101    #[serde(skip_serializing_if = "Option::is_none")]
102    pub source_map: Option<pepl_codegen::SourceMap>,
103}
104
105// ── type_check ────────────────────────────────────────────────────────────────
106
107/// Type-check a PEPL source file.
108///
109/// Parses the source and runs the type checker, returning any errors found.
110pub fn type_check(source: &str, name: &str) -> CompileErrors {
111    let source_file = SourceFile::new(name.to_string(), source.to_string());
112
113    // 1. Lex
114    let lex_result = pepl_lexer::Lexer::new(&source_file).lex();
115    if lex_result.errors.has_errors() {
116        return lex_result.errors;
117    }
118
119    // 2. Parse
120    let parse_result = pepl_parser::Parser::new(lex_result.tokens, &source_file).parse();
121    if parse_result.errors.has_errors() {
122        return parse_result.errors;
123    }
124
125    let program = match parse_result.program {
126        Some(p) => p,
127        None => return parse_result.errors,
128    };
129
130    // 3. Type-check
131    let mut errors = CompileErrors::empty();
132    let mut tc = checker::TypeChecker::new(&mut errors, &source_file);
133    tc.check(&program);
134
135    errors
136}
137
138// ── compile ───────────────────────────────────────────────────────────────────
139
140/// Full compilation pipeline: source → `.wasm` bytes.
141///
142/// Returns `Ok(wasm_bytes)` on success, or `Err(CompileErrors)` if there are
143/// syntax, type, or invariant errors. Codegen errors are converted to a
144/// single internal error in `CompileErrors`.
145pub fn compile(source: &str, name: &str) -> Result<Vec<u8>, CompileErrors> {
146    let source_file = SourceFile::new(name.to_string(), source.to_string());
147
148    // 1. Lex
149    let lex_result = pepl_lexer::Lexer::new(&source_file).lex();
150    if lex_result.errors.has_errors() {
151        return Err(lex_result.errors);
152    }
153
154    // 2. Parse
155    let parse_result = pepl_parser::Parser::new(lex_result.tokens, &source_file).parse();
156    if parse_result.errors.has_errors() {
157        return Err(parse_result.errors);
158    }
159
160    let program = match parse_result.program {
161        Some(p) => p,
162        None => return Err(parse_result.errors),
163    };
164
165    // 3. Type-check (includes invariant checking)
166    let mut errors = CompileErrors::empty();
167    {
168        let mut tc = checker::TypeChecker::new(&mut errors, &source_file);
169        tc.check(&program);
170    }
171    if errors.has_errors() {
172        return Err(errors);
173    }
174
175    // 4. Codegen → .wasm
176    match pepl_codegen::compile(&program) {
177        Ok(wasm) => Ok(wasm),
178        Err(e) => {
179            let mut errors = CompileErrors::empty();
180            errors.push_error(codegen_error_to_pepl_error(&e, name));
181            Err(errors)
182        }
183    }
184}
185
186/// Full compilation pipeline, returning a [`CompileResult`] (JSON-serializable).
187///
188/// This is the main entry point for the playground / WASM host.
189/// Includes enriched metadata: AST, hashes, state/action/view lists, versions.
190pub fn compile_to_result(source: &str, name: &str) -> CompileResult {
191    let source_hash = sha256_hex(source.as_bytes());
192    let source_file = SourceFile::new(name.to_string(), source.to_string());
193
194    // 1. Lex
195    let lex_result = pepl_lexer::Lexer::new(&source_file).lex();
196    if lex_result.errors.has_errors() {
197        return CompileResult {
198            success: false,
199            wasm: None,
200            errors: lex_result.errors,
201            ast: None,
202            source_hash,
203            wasm_hash: None,
204            state_fields: Vec::new(),
205            actions: Vec::new(),
206            views: Vec::new(),
207            capabilities: Vec::new(),
208            credentials: Vec::new(),
209            language_version: PEPL_LANGUAGE_VERSION.to_string(),
210            compiler_version: PEPL_COMPILER_VERSION.to_string(),
211            warnings: Vec::new(),
212            source_map: None,
213        };
214    }
215
216    // 2. Parse
217    let parse_result = pepl_parser::Parser::new(lex_result.tokens, &source_file).parse();
218    if parse_result.errors.has_errors() {
219        return CompileResult {
220            success: false,
221            wasm: None,
222            errors: parse_result.errors,
223            ast: None,
224            source_hash,
225            wasm_hash: None,
226            state_fields: Vec::new(),
227            actions: Vec::new(),
228            views: Vec::new(),
229            capabilities: Vec::new(),
230            credentials: Vec::new(),
231            language_version: PEPL_LANGUAGE_VERSION.to_string(),
232            compiler_version: PEPL_COMPILER_VERSION.to_string(),
233            warnings: Vec::new(),
234            source_map: None,
235        };
236    }
237
238    let program = match parse_result.program {
239        Some(p) => p,
240        None => {
241            return CompileResult {
242                success: false,
243                wasm: None,
244                errors: parse_result.errors,
245                ast: None,
246                source_hash,
247                wasm_hash: None,
248                state_fields: Vec::new(),
249                actions: Vec::new(),
250                views: Vec::new(),
251                capabilities: Vec::new(),
252                credentials: Vec::new(),
253                language_version: PEPL_LANGUAGE_VERSION.to_string(),
254                compiler_version: PEPL_COMPILER_VERSION.to_string(),
255                warnings: Vec::new(),
256                source_map: None,
257            };
258        }
259    };
260
261    // Extract metadata from AST
262    let metadata = extract_metadata(&program);
263
264    // 3. Type-check
265    let mut errors = CompileErrors::empty();
266    {
267        let mut tc = checker::TypeChecker::new(&mut errors, &source_file);
268        tc.check(&program);
269    }
270
271    let warnings = errors.warnings.clone();
272
273    if errors.has_errors() {
274        return CompileResult {
275            success: false,
276            wasm: None,
277            errors,
278            ast: Some(program),
279            source_hash,
280            wasm_hash: None,
281            state_fields: metadata.state_fields,
282            actions: metadata.actions,
283            views: metadata.views,
284            capabilities: metadata.capabilities,
285            credentials: metadata.credentials,
286            language_version: PEPL_LANGUAGE_VERSION.to_string(),
287            compiler_version: PEPL_COMPILER_VERSION.to_string(),
288            warnings,
289            source_map: None,
290        };
291    }
292
293    // 4. Codegen → .wasm
294    match pepl_codegen::compile_with_source_map(&program) {
295        Ok((wasm, source_map)) => {
296            let wasm_hash = sha256_hex(&wasm);
297            CompileResult {
298                success: true,
299                wasm: Some(wasm),
300                errors: CompileErrors::empty(),
301                ast: Some(program),
302                source_hash,
303                wasm_hash: Some(wasm_hash),
304                state_fields: metadata.state_fields,
305                actions: metadata.actions,
306                views: metadata.views,
307                capabilities: metadata.capabilities,
308                credentials: metadata.credentials,
309                language_version: PEPL_LANGUAGE_VERSION.to_string(),
310                compiler_version: PEPL_COMPILER_VERSION.to_string(),
311                warnings,
312                source_map: Some(source_map),
313            }
314        }
315        Err(e) => {
316            let mut errors = CompileErrors::empty();
317            errors.push_error(codegen_error_to_pepl_error(&e, name));
318            CompileResult {
319                success: false,
320                wasm: None,
321                errors,
322                ast: Some(program),
323                source_hash,
324                wasm_hash: None,
325                state_fields: metadata.state_fields,
326                actions: metadata.actions,
327                views: metadata.views,
328                capabilities: metadata.capabilities,
329                credentials: metadata.credentials,
330                language_version: PEPL_LANGUAGE_VERSION.to_string(),
331                compiler_version: PEPL_COMPILER_VERSION.to_string(),
332                warnings,
333                source_map: None,
334            }
335        }
336    }
337}
338
339// ── Metadata extraction ───────────────────────────────────────────────────────
340
341struct SpaceMetadata {
342    state_fields: Vec<FieldInfo>,
343    actions: Vec<ActionInfo>,
344    views: Vec<String>,
345    capabilities: Vec<String>,
346    credentials: Vec<FieldInfo>,
347}
348
349fn extract_metadata(program: &Program) -> SpaceMetadata {
350    let body = &program.space.body;
351
352    let state_fields = body
353        .state
354        .fields
355        .iter()
356        .map(|f| FieldInfo {
357            name: f.name.name.clone(),
358            ty: format!("{}", f.type_ann),
359        })
360        .collect();
361
362    let actions = body
363        .actions
364        .iter()
365        .map(|a| ActionInfo {
366            name: a.name.name.clone(),
367            params: a
368                .params
369                .iter()
370                .map(|p| FieldInfo {
371                    name: p.name.name.clone(),
372                    ty: format!("{}", p.type_ann),
373                })
374                .collect(),
375        })
376        .collect();
377
378    let views = body.views.iter().map(|v| v.name.name.clone()).collect();
379
380    let capabilities = body
381        .capabilities
382        .as_ref()
383        .map(|c| {
384            c.required
385                .iter()
386                .chain(c.optional.iter())
387                .map(|i| i.name.clone())
388                .collect()
389        })
390        .unwrap_or_default();
391
392    let credentials = body
393        .credentials
394        .as_ref()
395        .map(|c| {
396            c.fields
397                .iter()
398                .map(|f| FieldInfo {
399                    name: f.name.name.clone(),
400                    ty: format!("{}", f.type_ann),
401                })
402                .collect()
403        })
404        .unwrap_or_default();
405
406    SpaceMetadata {
407        state_fields,
408        actions,
409        views,
410        capabilities,
411        credentials,
412    }
413}
414
415// ── Hashing ───────────────────────────────────────────────────────────────────
416
417fn sha256_hex(data: &[u8]) -> String {
418    let mut hasher = Sha256::new();
419    hasher.update(data);
420    let result = hasher.finalize();
421    hex_encode(&result)
422}
423
424fn hex_encode(bytes: &[u8]) -> String {
425    let mut s = String::with_capacity(bytes.len() * 2);
426    for b in bytes {
427        use std::fmt::Write;
428        write!(s, "{:02x}", b).unwrap();
429    }
430    s
431}
432
433/// Convert a codegen error to a PeplError for structured output.
434fn codegen_error_to_pepl_error(e: &CodegenError, file: &str) -> pepl_types::PeplError {
435    use pepl_types::{ErrorCode, Span};
436
437    let (code, message) = match e {
438        CodegenError::Unsupported(msg) => (ErrorCode(700), format!("Unsupported: {}", msg)),
439        CodegenError::Internal(msg) => (ErrorCode(701), format!("Internal: {}", msg)),
440        CodegenError::ValidationFailed(msg) => {
441            (ErrorCode(702), format!("WASM validation failed: {}", msg))
442        }
443        CodegenError::UnresolvedSymbol(msg) => {
444            (ErrorCode(703), format!("Unresolved symbol: {}", msg))
445        }
446        CodegenError::LimitExceeded(msg) => {
447            (ErrorCode(704), format!("Limit exceeded: {}", msg))
448        }
449    };
450
451    pepl_types::PeplError::new(file, code, message, Span::new(1, 1, 1, 1), "")
452}