fix: strip app block before compilation to avoid parse errors

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'
This commit is contained in:
Will Anderson
2026-04-29 18:28:34 -05:00
parent cd2bc4e84c
commit 9a0747aa13
+65 -5
View File
@@ -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<usize> = 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<char> = 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<dyn std::error::Error>> {
// 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),