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:
@@ -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,
|
||||
|
||||
@@ -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"] {
|
||||
|
||||
Reference in New Issue
Block a user