From 9a0747aa1357cc15e8a8a020194641b2eff633a9 Mon Sep 17 00:00:00 2001 From: Will Anderson Date: Wed, 29 Apr 2026 18:28:34 -0500 Subject: [PATCH] fix: strip app block before compilation to avoid parse errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The El compiler cannot parse app { config { KEY: Type = value } } syntax — it's a declaration, not an expression. parse_app_block() already extracts config/secrets/flags correctly; the remaining source just needs the block removed before Compiler::compile() runs. strip_app_block() replaces the block with blank lines (preserving line numbers) and correctly skips occurrences inside // comments. Fixes daemon startup: 'compile error: parse error: invalid expression starting with : at 632:23' --- bin/el/src/main.rs | 70 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 65 insertions(+), 5 deletions(-) diff --git a/bin/el/src/main.rs b/bin/el/src/main.rs index e8d31be..f12a331 100644 --- a/bin/el/src/main.rs +++ b/bin/el/src/main.rs @@ -340,6 +340,62 @@ fn redact_secrets(s: &str) -> String { }) } +/// Strip the first `app "..." { ... }` block from El source so the compiler +/// doesn't see the declaration syntax (which it cannot parse as an expression). +/// The block is replaced with an equal number of blank lines to preserve line numbers. +/// Only matches `app "` at the start of a line (optional leading whitespace), to +/// avoid matching the keyword inside comments. +fn strip_app_block(source: &str) -> String { + // Find `app "` that appears at the start of a line (not inside a // comment). + let app_start = { + let mut found: Option = None; + for (i, _) in source.match_indices("app \"") { + // Walk backwards to start of line — if we hit '//' first, this is a comment. + let line_start = source[..i].rfind('\n').map(|p| p + 1).unwrap_or(0); + let prefix = &source[line_start..i]; + if prefix.contains("//") { + continue; // inside a comment + } + // Only accept if prefix is whitespace-only (app block declaration) + if prefix.chars().all(|c| c.is_whitespace()) { + found = Some(i); + break; + } + } + match found { + Some(pos) => pos, + None => return source.to_string(), + } + }; + + // Walk from app_start to find the opening brace (skip name token) + let after_app = &source[app_start..]; + let brace_pos = if let Some(p) = after_app.find('{') { p } else { return source.to_string() }; + + let mut depth = 0usize; + let mut end = app_start + brace_pos; + let chars: Vec = source[app_start + brace_pos..].chars().collect(); + for (i, &ch) in chars.iter().enumerate() { + match ch { + '{' => depth += 1, + '}' => { + depth -= 1; + if depth == 0 { + end = app_start + brace_pos + i + 1; + break; + } + } + _ => {} + } + } + + // Replace the block span with blank lines (preserve line count for error messages). + let removed = &source[app_start..end]; + let newlines = removed.chars().filter(|&c| c == '\n').count(); + let replacement = "\n".repeat(newlines); + format!("{}{}{}", &source[..app_start], replacement, &source[end..]) +} + /// Try to parse and apply app block from source. If parsing succeeds, apply context. /// Prints error and exits on secret resolution failure. fn maybe_apply_app_block(source: &str) { @@ -805,12 +861,16 @@ async fn run(cli: Cli) -> Result<(), Box> { // Parse and apply `app` block before running. maybe_apply_app_block(&entry_source); + // Strip `app { ... }` before compilation — the compiler cannot parse + // the declaration syntax; config/secrets are already extracted above. + let compile_source = strip_app_block(&entry_source); + // Compile via Rust compiler. let opts = CompilerOptions { target: Target::Debug, ..Default::default() }; - let compiled = Compiler::compile(&entry_source, opts) + let compiled = Compiler::compile(&compile_source, opts) .map_err(|e| format!("compile error: {e}"))?; let instructions = el_compiler::Bytecode::deserialize_all(&compiled.artifact) .unwrap_or_default(); @@ -8986,10 +9046,10 @@ thread_local! { "http://localhost:11434/api/chat".to_string(), String::new(), )); - // Neuron (the platform itself) + // Neuron inference — lives at neuron.neurontechnologies.ai, Anthropic Messages format m.insert("neuron".to_string(), ( std::env::var("NEURON_ENDPOINT") - .unwrap_or_else(|_| "https://api.neurontechnologies.ai/v1/chat/completions".to_string()), + .unwrap_or_else(|_| "https://neuron.neurontechnologies.ai/v1/messages".to_string()), std::env::var("NEURON_API_KEY").unwrap_or_default(), )); m @@ -9064,8 +9124,8 @@ fn call_llm_blocking(model: &str, system: Option<&str>, prompt: &str) -> String Err(e) => return format!("llm_call error: {}", e), }; - if endpoint.contains("anthropic.com") { - // Anthropic Messages API + if endpoint.contains("anthropic.com") || endpoint.contains("neuron.neurontechnologies.ai") { + // Anthropic Messages API — also used by neuron.neurontechnologies.ai let body = if let Some(sys) = system { serde_json::json!({ "model": get_anthropic_model(model),