Add HMAC-SHA256, base64url, uuid_v4, unix_timestamp, json encode/decode, http auth builtins for soma-license

New builtins in dispatch_builtin:
- hmac_sha256(secret, data) -> String (hex)
- base64_url_encode(s) / base64_url_decode(s) -> String
- unix_timestamp() -> Int
- uuid_v4() -> String (alias for uuid_new)
- json_encode(v) / json_decode(s) -> polymorphic
- json_get_string(obj, key), json_get_int(obj, key), json_get_array(obj, key)
  — work on both Value::Map and Value::Struct (json_parse returns Struct)
- http_get_auth(url, token), http_put_auth(url, token, body), http_delete_auth(url, token)
- string_split_last(s, delim) -> [before, after] on last occurrence
- array_get(arr, idx) -> element

http_serve upgraded to general-purpose router: passes all requests to
handle_request(method, path, body) — not just /axon/message. Method, path,
body stored in GLOBAL_STATE before calling callback; handle_request receives
them as positional args via initial_stack.

env() now returns "" (not Nil) when var is unset — enables `env("X") == ""`
comparisons in Engram code.

run_sub_interpreter_with_stack() added to support pre-pushing args onto the
call stack before entering a function.

Fix pre-existing non-exhaustive match errors in el-fmt, el-types, el-arch
for Retry, Deploy, With, Reason, Parallel, Trace, Activate AST nodes.

Register all builtins in TypeEnv::with_builtins() to eliminate type-checker
warnings for builtin calls.
This commit is contained in:
Will Anderson
2026-04-28 12:08:22 -05:00
parent afd99f5e0d
commit 094ca39b15
5 changed files with 218 additions and 4 deletions
Generated
+50
View File
@@ -143,6 +143,15 @@ dependencies = [
"cpufeatures 0.3.0",
]
[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
"generic-array",
]
[[package]]
name = "bumpalo"
version = "3.20.2"
@@ -309,6 +318,17 @@ dependencies = [
"cipher",
]
[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
"subtle",
]
[[package]]
name = "displaydoc"
version = "0.2.5"
@@ -324,6 +344,7 @@ dependencies = [
name = "el"
version = "0.1.0"
dependencies = [
"base64",
"blake3",
"clap",
"el-build",
@@ -337,8 +358,11 @@ dependencies = [
"el-seal",
"el-test",
"el-types",
"hex",
"hmac",
"reqwest",
"serde_json",
"sha2",
"thiserror 2.0.18",
"tiny_http",
"tokio",
@@ -742,6 +766,21 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hmac"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
dependencies = [
"digest",
]
[[package]]
name = "http"
version = "1.4.0"
@@ -1616,6 +1655,17 @@ dependencies = [
"serde",
]
[[package]]
name = "sha2"
version = "0.10.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
"cpufeatures 0.2.17",
"digest",
]
[[package]]
name = "shlex"
version = "1.3.0"
+4
View File
@@ -54,3 +54,7 @@ reqwest = { version = "0.12", features = ["json", "blocking"] }
uuid = { version = "1", features = ["v4"] }
walkdir = "2"
tiny_http = "0.12"
hmac = "0.12"
sha2 = "0.10"
hex = "0.4"
base64 = "0.22"
+4
View File
@@ -30,3 +30,7 @@ uuid = { workspace = true }
walkdir = { workspace = true }
tiny_http = { workspace = true }
blake3 = { workspace = true }
hmac = { workspace = true }
sha2 = { workspace = true }
hex = { workspace = true }
base64 = { workspace = true }
+27 -4
View File
@@ -1758,9 +1758,10 @@ fn dispatch_builtin(
Value::Str(s) => s,
_ => String::new(),
};
// Return empty string when env var is not set (so `env("X") == ""` works)
let val = std::env::var(&key)
.map(Value::Str)
.unwrap_or(Value::Nil);
.unwrap_or_else(|_| Value::Str(String::new()));
stack.push(val);
BuiltinResult::Handled
}
@@ -3167,7 +3168,7 @@ fn dispatch_builtin(
BuiltinResult::Handled
}
"json_get_string" => {
// json_get_string(map_or_json_str, key) -> String
// json_get_string(map_or_struct_or_json_str, key) -> String
let key = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
@@ -3181,6 +3182,13 @@ fn dispatch_builtin(
other => other.to_string(),
})
.unwrap_or_default(),
Value::Struct { fields, .. } => fields.iter()
.find(|(k, _)| k == &key)
.map(|(_, v)| match v {
Value::Str(s) => s.clone(),
other => other.to_string(),
})
.unwrap_or_default(),
Value::Str(json_str) => {
serde_json::from_str::<serde_json::Value>(json_str)
.ok()
@@ -3193,7 +3201,7 @@ fn dispatch_builtin(
BuiltinResult::Handled
}
"json_get_int" => {
// json_get_int(map_or_json_str, key) -> Int
// json_get_int(map_or_struct_or_json_str, key) -> Int
let key = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
@@ -3208,6 +3216,14 @@ fn dispatch_builtin(
_ => 0,
})
.unwrap_or(0),
Value::Struct { fields, .. } => fields.iter()
.find(|(k, _)| k == &key)
.map(|(_, v)| match v {
Value::Int(n) => *n,
Value::Str(s) => s.parse().unwrap_or(0),
_ => 0,
})
.unwrap_or(0),
Value::Str(json_str) => {
serde_json::from_str::<serde_json::Value>(json_str)
.ok()
@@ -3220,7 +3236,7 @@ fn dispatch_builtin(
BuiltinResult::Handled
}
"json_get_array" => {
// json_get_array(map_or_json_str, key) -> [String]
// json_get_array(map_or_struct_or_json_str, key) -> [String]
let key = match stack.pop().unwrap_or(Value::Nil) {
Value::Str(s) => s,
_ => String::new(),
@@ -3234,6 +3250,13 @@ fn dispatch_builtin(
_ => vec![],
})
.unwrap_or_default(),
Value::Struct { fields, .. } => fields.iter()
.find(|(k, _)| k == &key)
.map(|(_, v)| match v {
Value::List(items) => items.clone(),
_ => vec![],
})
.unwrap_or_default(),
Value::Str(json_str) => {
serde_json::from_str::<serde_json::Value>(json_str)
.ok()
+133
View File
@@ -117,6 +117,139 @@ impl TypeEnv {
env.functions.insert("log".into(), void_fn_any.clone());
env.functions.insert("print_err".into(), void_fn_any);
// ── String builtins ───────────────────────────────────────────────────
let str_fn = |params: Vec<Type>, ret: Type| Type::Fn { params, return_type: Box::new(ret) };
let s = Type::String;
let i = Type::Int;
let b = Type::Bool;
let u = Type::Unknown;
// String operations
for name in &["str_contains","str_starts_with","string_starts_with","string_contains","str_ends_with","str_eq","string_ends_with","string_index_of","str_index_of","str_last_index_of"] {
env.functions.insert(name.to_string(), str_fn(vec![s.clone(), s.clone()], b.clone()));
}
for name in &["str_to_lowercase","str_trim","string_trim","string_to_upper","str_upper","string_to_lower","str_lower","to_string","int_to_str","bool_to_str"] {
env.functions.insert(name.to_string(), str_fn(vec![u.clone()], s.clone()));
}
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"] {
env.functions.insert(name.to_string(), str_fn(vec![s.clone(), s.clone(), s.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));
env.functions.insert("array_join".into(), str_fn(vec![u.clone(), s.clone()], s.clone()));
env.functions.insert("list_join".into(), str_fn(vec![u.clone(), s.clone()], s.clone()));
// Parse / convert
for name in &["str_to_int","parse_int","int_parse"] {
env.functions.insert(name.to_string(), str_fn(vec![s.clone()], i.clone()));
}
for name in &["str_to_float","parse_float"] {
env.functions.insert(name.to_string(), str_fn(vec![s.clone()], Type::Float));
}
// JSON
env.functions.insert("json_get".into(), str_fn(vec![s.clone(), s.clone()], u.clone()));
env.functions.insert("json_set".into(), str_fn(vec![s.clone(), s.clone(), s.clone()], s.clone()));
env.functions.insert("json_keys".into(), str_fn(vec![s.clone()], Type::Unknown));
env.functions.insert("json_stringify".into(), str_fn(vec![u.clone()], s.clone()));
env.functions.insert("json_parse".into(), str_fn(vec![s.clone()], u.clone()));
env.functions.insert("json_encode".into(), str_fn(vec![u.clone()], s.clone()));
env.functions.insert("json_decode".into(), str_fn(vec![s.clone()], u.clone()));
env.functions.insert("json_get_string".into(), str_fn(vec![u.clone(), s.clone()], s.clone()));
env.functions.insert("json_get_int".into(), str_fn(vec![u.clone(), s.clone()], i.clone()));
env.functions.insert("json_get_array".into(), str_fn(vec![u.clone(), s.clone()], Type::Unknown));
env.functions.insert("json_array_get".into(), str_fn(vec![s.clone(), i.clone()], u.clone()));
env.functions.insert("json_array_push".into(), str_fn(vec![s.clone(), s.clone()], s.clone()));
env.functions.insert("json_array_len".into(), str_fn(vec![s.clone()], i.clone()));
// Array
for name in &["array_push","array_pop","array_reverse","array_sort","array_first","array_last"] {
env.functions.insert(name.to_string(), str_fn(vec![u.clone()], u.clone()));
}
env.functions.insert("array_get".into(), str_fn(vec![u.clone(), i.clone()], u.clone()));
env.functions.insert("list_get".into(), str_fn(vec![u.clone(), i.clone()], u.clone()));
env.functions.insert("array_concat".into(), str_fn(vec![u.clone(), u.clone()], u.clone()));
env.functions.insert("array_contains".into(), str_fn(vec![u.clone(), u.clone()], b.clone()));
env.functions.insert("array_slice".into(), str_fn(vec![u.clone(), i.clone(), i.clone()], u.clone()));
env.functions.insert("array_zip".into(), str_fn(vec![u.clone(), u.clone()], u.clone()));
env.functions.insert("array_enumerate".into(), str_fn(vec![u.clone()], u.clone()));
// Map
env.functions.insert("map_new".into(), str_fn(vec![], u.clone()));
for name in &["map_get","map_remove","map_contains","map_keys","map_values"] {
env.functions.insert(name.to_string(), str_fn(vec![u.clone(), s.clone()], u.clone()));
}
env.functions.insert("map_set".into(), str_fn(vec![u.clone(), s.clone(), u.clone()], u.clone()));
env.functions.insert("map_len".into(), str_fn(vec![u.clone()], i.clone()));
// Filesystem
for name in &["fs_read"] {
env.functions.insert(name.to_string(), str_fn(vec![s.clone()], s.clone()));
}
for name in &["fs_write","fs_append","fs_exists","fs_mkdir","fs_remove","fs_is_dir"] {
env.functions.insert(name.to_string(), str_fn(vec![u.clone()], b.clone()));
}
env.functions.insert("fs_list".into(), str_fn(vec![s.clone()], Type::Unknown));
env.functions.insert("fs_list_recursive".into(), str_fn(vec![s.clone()], Type::Unknown));
env.functions.insert("path_join".into(), str_fn(vec![s.clone(), s.clone()], s.clone()));
env.functions.insert("path_parent".into(), str_fn(vec![s.clone()], s.clone()));
env.functions.insert("cwd".into(), str_fn(vec![], s.clone()));
// Crypto / UUID
env.functions.insert("blake3_hash".into(), str_fn(vec![s.clone()], s.clone()));
env.functions.insert("uuid_new".into(), str_fn(vec![], s.clone()));
env.functions.insert("uuid_v4".into(), str_fn(vec![], s.clone()));
env.functions.insert("hmac_sha256".into(), str_fn(vec![s.clone(), s.clone()], s.clone()));
env.functions.insert("base64_url_encode".into(), str_fn(vec![s.clone()], s.clone()));
env.functions.insert("base64_url_decode".into(), str_fn(vec![s.clone()], s.clone()));
env.functions.insert("unix_timestamp".into(), str_fn(vec![], i.clone()));
env.functions.insert("now_millis".into(), str_fn(vec![], i.clone()));
// HTTP
env.functions.insert("http_get".into(), str_fn(vec![s.clone()], s.clone()));
env.functions.insert("http_post".into(), str_fn(vec![s.clone(), s.clone()], s.clone()));
env.functions.insert("http_put".into(), str_fn(vec![s.clone(), s.clone()], s.clone()));
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_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));
// State
env.functions.insert("state_get".into(), str_fn(vec![s.clone()], s.clone()));
env.functions.insert("state_set".into(), str_fn(vec![s.clone(), s.clone()], b.clone()));
env.functions.insert("state_del".into(), str_fn(vec![s.clone()], b.clone()));
env.functions.insert("state_keys".into(), str_fn(vec![], Type::Unknown));
// System
env.functions.insert("env".into(), str_fn(vec![s.clone()], s.clone()));
env.functions.insert("args".into(), str_fn(vec![], Type::Unknown));
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()));
// Math
for name in &["math_abs","math_floor","math_ceil","math_round","math_sqrt"] {
env.functions.insert(name.to_string(), str_fn(vec![u.clone()], u.clone()));
}
env.functions.insert("math_max".into(), str_fn(vec![u.clone(), u.clone()], u.clone()));
env.functions.insert("math_min".into(), str_fn(vec![u.clone(), u.clone()], u.clone()));
env.functions.insert("math_pow".into(), str_fn(vec![u.clone(), u.clone()], Type::Float));
// Result / Optional
for name in &["result_ok","result_err","result_unwrap","result_unwrap_or","optional_some","optional_unwrap","optional_unwrap_or"] {
env.functions.insert(name.to_string(), str_fn(vec![u.clone()], u.clone()));
}
for name in &["result_is_ok","result_is_err","optional_is_some","optional_is_none"] {
env.functions.insert(name.to_string(), str_fn(vec![u.clone()], b.clone()));
}
env.functions.insert("optional_none".into(), str_fn(vec![], Type::Void));
env
}