Add readline, color, http_post_auth builtins; fix import resolution in build

Engram can now power the Neuron CLI:

- readline(prompt) -> String: interactive terminal input via stdin
- http_post_auth(url, token, body) -> String: authenticated POST for daemon API
- color_cyan/green/red/yellow/bold/dim(s) -> String: ANSI color output
  All registered in el-types type checker

- el build now resolves import "file.el" directives recursively (was only
  done for el run-file and el check; project builds failed silently)

- Add .gitignore (target/, *.elc, *.sealed, *.map.json)
This commit is contained in:
Will Anderson
2026-04-28 13:46:22 -05:00
parent 094ca39b15
commit b62df85969
4 changed files with 354 additions and 6 deletions
+50 -3
View File
@@ -161,8 +161,9 @@ impl BuildSystem {
});
}
// Read source
let source = std::fs::read_to_string(&entry)?;
// Read source (resolving imports recursively)
let source = resolve_imports_recursive(&entry)
.map_err(|e| BuildError::Io(std::io::Error::new(std::io::ErrorKind::Other, e)))?;
// Build seal config for prod builds
let seal_config = self.build_seal_config()?;
@@ -296,7 +297,8 @@ impl BuildSystem {
return Err(BuildError::EntryNotFound(entry.display().to_string()));
}
let source = std::fs::read_to_string(&entry)?;
let source = resolve_imports_recursive(&entry)
.map_err(|e| BuildError::Io(std::io::Error::new(std::io::ErrorKind::Other, e)))?;
let tokens = el_lexer::tokenize(&source)
.map_err(el_compiler::CompileError::Lex)?;
let program = el_parser::parse(tokens, source.clone())
@@ -340,6 +342,51 @@ impl BuildSystem {
}
}
/// Resolve `import "path.el"` directives by reading and concatenating source files.
/// Imports are resolved relative to the directory of the importing file.
/// Circular imports are detected via a visited set.
fn resolve_imports_recursive(file: &std::path::Path) -> Result<String, String> {
let mut visited = std::collections::HashSet::new();
resolve_imports_inner(file, &mut visited)
}
fn resolve_imports_inner(
file: &std::path::Path,
visited: &mut std::collections::HashSet<std::path::PathBuf>,
) -> Result<String, String> {
let canonical = file.canonicalize().unwrap_or_else(|_| file.to_path_buf());
if visited.contains(&canonical) {
return Ok(String::new()); // circular — skip
}
visited.insert(canonical.clone());
let dir = file.parent().unwrap_or(std::path::Path::new("."));
let source = std::fs::read_to_string(file)
.map_err(|e| format!("cannot read {}: {e}", file.display()))?;
let mut out = String::new();
for line in source.lines() {
let trimmed = line.trim();
if let Some(rest) = trimmed.strip_prefix("import ") {
let rest = rest.trim();
if rest.starts_with('"') && rest.ends_with('"') {
let import_path_str = &rest[1..rest.len() - 1];
let import_path = dir.join(import_path_str);
let imported = resolve_imports_inner(&import_path, visited)?;
out.push_str(&imported);
out.push('\n');
} else {
out.push_str(line);
out.push('\n');
}
} else {
out.push_str(line);
out.push('\n');
}
}
Ok(out)
}
fn artifact_name(
pkg_name: &str,
build_target: &BuildTarget,
+10 -1
View File
@@ -134,9 +134,11 @@ impl TypeEnv {
for name in &["str_len","string_len","list_len","array_length","array_len","map_len","json_array_len"] {
env.functions.insert(name.to_string(), str_fn(vec![u.clone()], i.clone()));
}
for name in &["str_replace","string_replace","string_concat","string_substring","str_slice"] {
for name in &["str_replace","string_replace","string_concat","string_substring"] {
env.functions.insert(name.to_string(), str_fn(vec![s.clone(), s.clone(), s.clone()], s.clone()));
}
// str_slice(s: String, start: Int, end: Int) -> String
env.functions.insert("str_slice".into(), str_fn(vec![s.clone(), i.clone(), i.clone()], s.clone()));
env.functions.insert("str_split".into(), str_fn(vec![s.clone(), s.clone()], Type::Unknown));
env.functions.insert("string_split".into(), str_fn(vec![s.clone(), s.clone()], Type::Unknown));
env.functions.insert("string_split_last".into(), str_fn(vec![s.clone(), s.clone()], Type::Unknown));
@@ -216,6 +218,7 @@ impl TypeEnv {
env.functions.insert("http_delete".into(), str_fn(vec![s.clone()], s.clone()));
env.functions.insert("http_patch".into(), str_fn(vec![s.clone(), s.clone()], s.clone()));
env.functions.insert("http_get_auth".into(), str_fn(vec![s.clone(), s.clone()], s.clone()));
env.functions.insert("http_post_auth".into(), str_fn(vec![s.clone(), s.clone(), s.clone()], s.clone()));
env.functions.insert("http_put_auth".into(), str_fn(vec![s.clone(), s.clone(), s.clone()], s.clone()));
env.functions.insert("http_delete_auth".into(), str_fn(vec![s.clone(), s.clone()], s.clone()));
env.functions.insert("http_serve".into(), str_fn(vec![u.clone()], Type::Void));
@@ -232,6 +235,12 @@ impl TypeEnv {
env.functions.insert("exit".into(), str_fn(vec![i.clone()], Type::Void));
env.functions.insert("sleep_ms".into(), str_fn(vec![i.clone()], Type::Void));
env.functions.insert("timestamp".into(), str_fn(vec![], s.clone()));
env.functions.insert("readline".into(), str_fn(vec![s.clone()], s.clone()));
// ANSI color builtins
for name in &["color_cyan","color_green","color_red","color_yellow","color_bold","color_dim"] {
env.functions.insert(name.to_string(), str_fn(vec![s.clone()], s.clone()));
}
// Math
for name in &["math_abs","math_floor","math_ceil","math_round","math_sqrt"] {